VVbuys Blog

standalone Linux lover

本文首发于我的知乎专栏 The Little Programmer,转载请保留链接 ;)

2017 年 1 月 9 号凌晨,看完《星战》回家,发现朋友圈都炸了……原来是「小程序」如约公测(以下简称小程序)。果然贵圈人都睡得晚啊,一个个大半夜了精神得不行。

截图推荐什么的已经漫天都是了,而且连 「推荐小程序的小程序」都已经出现了,我们就直入正题吧,今天笔者不跟你们聊情怀,就聊体验:

小程序的体验比 Web 更好吗?

体验完利益相关微票儿的「电影演出赛事」后,我在朋友圈里怒发了一条「实际体验小程序的感觉就是完全没有比普通的 web 体验更好…」,感觉评论里 @Cheeeee 都受惊了 > <。不过在多体验了几个小程序之后,我觉得我应该尝试更客观的回答这个问题。

1. 在「微信」里,小程序的 Engagement 比 Web 更好。

这当然是毋庸置疑的,我的博客在微信里打开至今都是「非微信官方网页,继续访问将转换成手机预览模式」,然后点击「继续访问」就是「params is error」,我 &^*#.%…

而对于其他在微信中可访问的 web 应用来说,小程序有着自己的搜索入口、抽屉(历史记录),还可以「显示在聊天顶部」,这其实分别对应着 「拉新」、「包活」 与一定的「多任务」支持。尤其是后两者,与 PWA 的「添加至主屏」与「出现在 Task Switcher」里异曲同工。

正如 微信小程序和网页版程序的区别在哪里? - 冯雨的回答 - 知乎 里所说的,「订阅号、服务号、小程序,就是一个个静态或动态的 Web站点;二维码和消息气泡,一个现实一个虚拟,就是微信提供的超级链接。」

World Wide Web 在微信里是残废的,取而代之的是 Weixin/Wechat Wide Web。

值得一提的是,现在微信只会对特定小程序支持模糊搜索,而且据我目测都是诸如京东、滴滴这样的「国家队」。喏,在我地盘这你就得听我的~ ? ?

2. 在「微信」里,小程序的 Capability 比 Web 更好。

当我们在说「小程序的体验是否能比 Web 更接近原生应用」时,我们通常指的就是它的 capability。

先说 UI 性能,截止目前为止,小程序的大部分组件都还是使用 WebView 渲染的,这意味着在大多数组件场景下,小程序的 UI 性能不可能比 Web 更高。但是:

  1. 小程序团队非常 tricky 地把力气都用在了刀刃上:每一个使用原生 UI 渲染、或在自定义 WebView 中优化过的组件都对应着 Mobile Web 中的一个老大难问题。比如在 iOS 上让顶部或底部的 Tab Bar “Fixed”,比如视频的自动播放与控制力,比如地图、textarea 等,可以说利用有限的资源显著提高了小程序的可用性。

  2. 由于 Web 前端开发者的良莠不齐,小程序通过限定一组 Web 技术的子集,可以很好的约束开发者写出性能与体验不低于基线的代码,这与 Google 的 AMP 异曲同工。(其实这是大家觉得小程序体验比 Web 好的很大一个原因)

  3. 由于小程序中的 wxml 与 wxss 都是比较 high-level 的抽象,所以微信团队可以在不影响开发者源代码的情况下,通过升级 Runtime 与组件的实现不断优化小程序的性能,比如完全迁移到类似 React Native 或 Weex 这样的 JS-to-Native 方案。

再说启动性能,这是让大家觉得小程序感知体验比 Web 好的第二个大因素:

  1. 由于小程序是打包部署并「安装」的,可以从文件系统中直接启动。以此解决 web 带来的网络延迟与离线时不可访问问题。

最后是 Integration。通过私有的 JS SDK,小程序可以借助微信这座桥梁实现很多以往 Web 并不容易实现的体验。同样,这些改进也非常 tricky,只解决痛点问题:

  1. 设备访问能力,文件、系统、网络、GPS、加速计、罗盘……
  2. 「第一公民」能力,最明显的莫过于设置导航条和页与页之间的动画。还有 Android 设备上的「添加小程序到桌面」,其实就是个快捷方式。

(图为猫眼 App 与小程序,因为长得像…感觉不小心给老东家竞争对手打广告了?)

可惜的是,这些技术里面没有一项是「小程序」首创的,且大都有超过两年的历史:百度的 Blend UI、阿里的 Hybrid 容器、Google 的 PWA/AMP、Phonegap/Cordova、React Native/Weex……这也是很多技术从业人吐槽小程序在技术上毫无创新的原因。

但平心而论,崇尚「技术服务产品」的腾讯系在产品化上做的真心出色。这也是我为什么在 9 月 21 日知道小程序技术方案时夸赞「兼容并蓄 博采众长 且可持续性发展」的原因,并不是站在技术创新的角度,而是站在微信的角度上,这个决策拿捏在了 sweet point 上。

3. 在「微信」里,小程序不一定比 Web 更好的。

目前我所了解到的(截止 2017 年 1 月 9 日):

  1. 小程序对比 Web,只能通过摄像头扫码,不能分享朋友圈,营销难做,这是 Reach。

  2. 小程序中没有真正的超链接与 WebView,完全不能外链,这是 Linkability。如果知乎要做小程序,所有答案里的超链接都只能报废。或者只能像轻芒杂志那样,做一层转码,美其名曰阅读模式。

  3. 小程序目前的组件虽然 cover 了大部分场景,但是也明显有很多不能 cover 到的 case,这是 Scalability。

这三点都是可以直接影响到目前小程序的产品形态与设计的。当然,对于微信来说,这三点更多的是决策问题。作为 Weixin Wide Web 这个封闭生态的唯一「浏览器」,微信便是生杀大权。手起刀落之间,小程序的缺点随时可以被弥补,而 Web 的优点也随时可以被抹杀。

但是,现实可能并不会这么简单。我们发现,大部分小程序都只提供了其原生应用或 web 应用功能的一个子集。比如文章最早提到的微票儿的「电影演出赛事」小程序,与钱包里的 web 版本相比,UI 体验好了一点,但是功能远没有 web 版本来得丰富,也没有了 web 版本可以分享评论到朋友圈的能力。

(微票儿小程序与其钱包中的内嵌 web 应用对比,web 版的功能要丰富得多。)

微票儿(娱票儿)作为一家在微信里内嵌 web 服务起家的公司,一是证明了微信流量红利的可怕,二其实也证明了原有 web 的能力。作为「亲腾讯亲微信」的公司之一,其小程序比不上 web 应用可能只是时间关系。但是对于其他公司呢,尤其是未被腾讯「临幸」过的公司?而这其实对应着另一个更难回答的问题:

小程序值得接入商花多大的力气?

笔者自知无法回答这个问题,所以只能抛砖引玉一下:

N/A 简单体验(页面) 中等体验(mini-app) 核心体验(app)
纯微信流 公众号 小程序 小程序 我想要个原生啊
创业公司 公众号 Web 小程序 Web 小程序 拉回原生啊
中大公司 公众号 Web 小程序 Web 小程序 拉回原生啊
巨头公司 公众号 Web 不跟微信玩 不跟微信玩

具体到每一个 Web 与小程序 PK 的场景:

  • 对于简单体验,小程序的一点点体验提升对比 Web 的跨平台与传播能力没有优势
  • 对于中等体验,小程序体验更好,但需要付出额外的人力资源与开发维护成本
  • 对于核心体验,大家的目标都是拉回自己的主场

如果说阿里的「让天下没有难做的生意」是把话说开来「双赢」,微信「开放」平台和接入商之间的资源互换关系则更像是「权力的游戏」了:微信想借接入商来建立自己的垄断帝国,接入商却想玩暗度陈仓。某种程度上来说,Web 应用是自己的领地,值得在上面建立完整的体验。而小程序,可能会如小程序诞生前的「weixin-specific web」一样,很大程度上沦为拉新立牌坊的工具。

所以我们不妨再加一条:

4. 对于用户来说,小程序可能并不会「够用」,这是 Feature Set。

4. 不在「微信」里,小程序……

5. 结论?

回到问题「小程序的体验比 Web 更好吗?」,我觉得各位看官心里应该会有自己的答案。对于不同的公司,不同的业务场景,不同的盈利方式,不同的团队,我相信这个答案都是不一样的。

But if you trade something off, make sure you get something in return.
如果你需要妥协掉一些东西,请务必换回点好处来。

作为一篇「试图做到客观(且非常难)」的文章,如果能对你有帮助,那就算没有白写了。

6. 题外话(这段主观!)

最后说两句题外话吧,上个月给《程序员》杂志交了拖了 N 久的稿,大概在本月底会发吧?

在那篇文章最后我写到,「笔者奢望着本文能对推动 PWA 的国内环境有一定的贡献」。眼见小程序在某种意义上 “polyfill” (大雾)了 PWA,作为一个在技术上略有 理想主义 的程序员,笔者也只能叹一句了:

「这不是我想要的未来。」

会是你们的吗?

这篇文章转载自我在知乎上的回答

谢邀。

首先要理解的是 DOM 是 API,是一组无关编程语言的接口(Interfaces)而非实现(Implementation)。前端平时常说的 DOM 其实只是浏览器通过 ECMAScript(JavaScript)对 DOM 接口的一种实现。

其次要知道的是,DOM 既是为 HTML 制定的,也是为 XML 制定的。而两者各有一些特异的部分,所以作为 DOM 标准基石的 DOM Level 1 其实分为 Core 与 HTML 两个部分。Core 定义了 fundamental interfaces 与 extended interfaces,分别是共用的基础接口与 「XML 拓展包」,而 HTML 部分则全都是「HTML 拓展包」。题主所问到的 Document 接口被定义在 Core 的 fundamental interfaces 中,而 HTMLDocument 接口则定义在 HTML 部分中,且「接口继承」于 Document。

这种继承关系当然是可以在 JavaScript 的 DOM 实现中体现出来的:

1
2
3
4
5
6
7
8
9
// document 是 HTMLDocument 的实例
document instanceof HTMLDocument // true

// document 的 [[prototype]] 指向 HTMLDocument 的原型
document.__proto__ === HTMLDocument.prototype // true

// HTMLDocument 伪类继承于 Document
HTMLDocument.prototype instanceof Document // true
HTMLDocument.prototype.__proto__ === Document.prototype // true

至于 Document 与 HTMLDocument 这两个构造函数,跟 Array、Object 一样都是 built-in 的:

1
2
3
4
> Document
< function Document() { [native code] }
> HTMLDocument
< function HTMLDocument() { [native code] }

虽然是 native code,但一个有意思的现象是,这两个构造函数之间也是存在原型链的:

1
2
3
4
5
6
// HTMLDocument 的 [[prototype]] 是指向 Document 的
HTMLDocument.__proto__ == Document

// 同理
Document.__proto__ == Node
Node.__proto__ == EventTarget

其作用是实现对静态成员的继承。( ES6 Class 的行为与此完全一致,但这个行为在更早之前就是这样了。)

好了扯远了,总结一下,在 JavaScript 的 DOM 实现中

  • document 是 HTMLDocument 的实例
  • HTMLDocument 继承于 Document

留一个课后作业,有兴趣的话可以看看 Document.prototype 与 HTMLDocument.prototype 里分别都有什么?在不同浏览器里都试试。

以上。

SW-Precache is a great Service Worker tool from Google. It is a node module designed to be integrated into your build process and to generate a service worker for you. Though you can use sw-precache out of the box, you might still wonder what happens under the hood. There you go, this article is written for you!

This post was first published at Medium

Overview

The core files involving in sw-precache are mainly three:

1
2
3
4
service-worker.tmpl  
lib/
├ sw-precache.js
└ functions.js

sw-precache.js is the main entry of the module. It reads the configuration, processes parameters, populates the service-worker.tmpl template and writes the result into specified file. Andfunctions.js is just a module containing bunch of external functions which would be all injected into the generated service worker file as helpers.

Since the end effect of sw-precache is performed by the generated service worker file in the runtime, a easy way to get an idea of what happens is by checking out source code inside service-worker.tmpl . It’s not hard to understand the essentials and I will help you.

Initialization

The generated service worker file (let’s call it sw.js for instance) get configuration by text interpolation when sw-precache.js populating service-worker.tmpl .

1
2
3
4
5
6
7
8
// service-worker.tmpl  
var precacheConfig = <%= precacheConfig %>;

// sw.js
var precacheConfig = [
["js/a.js", "3cb4f0"],
["css/b.css", "c5a951"]
]

It’s not difficult to see that it’s a list of relative urls and MD5 hashes. In fact, one thing that sw-precache.js do in the build time is to calculate hash of each file that it asked to “precache” from staticFileGlobs parameter.

In sw.js, precacheConfig would be transformed into a ES6 Map with structure Map {absoluteUrl => cacheKey} as below. Noticed that I omit the origin part (e.g. http://localhost) for short.

1
2
3
4
5
> urlToCacheKeys  
< Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

Instead of using raw URL as the cache key, sw-precache append a _sw-precache=[hash] to the end of each URL when populating, updating its cache and even fetching these subresouces. Those _sw-precache=[hash] are what we called cache-busting parameter*. It can prevent service worker from responding and caching out-of-date responses found in browsers’ HTTP cache indefinitely.

Because each build would re-calculate hashes and re-generate a new sw.js with new precacheConfig containing those new hashes, sw.js can now determine the version of each subresources thus decide what part of its cache needs a update. This is pretty similar with what we commonly do when realizing long-term caching with webpack or gulp-rev, to do a byte-diff ahead of runtime.

*: Developer can opt out this behaviour with dontCacheBustUrlsMatching option if they set HTTP caching headers right. More details on Jake’s Post.

On Install

ServiceWorker gives you an install event. You can use this to get stuff ready, stuff that must be ready before you handle other events.

During the install lifecycle, sw.js open the cache and get started to populate its cache. One cool thing that it does for you is its incremental update mechanism.

Sw-precache would search each cache key (the values of urlsToCacheKeys) in the cachedUrls, a ES6 Set containing URLs of all requests indexed from current version of cache, and only fetch and cache.put resources couldn’t be found in cache, i.e, never be cached before, thus reuse cached resources as much as possible.

If you can not fully understand it, don’t worry. We will recap it later, now let’s move on.

On Activate

Once a new ServiceWorker has installed & a previous version isn’t being used, the new one activates, and you get an activate event. Because the old version is out of the way, it’s a good time to handle schema migrations in IndexedDB and also delete unused caches.

During activation phase, sw.js would compare all existing requests in the cache, named existingRequests (noticed that it now contains resources just cached on installation phase) with setOfExpectedUrls, a ES6 Set from the values of urlsToCacheKeys. And delete any requests not matching from cache.

1
2
3
4
5
6
// sw.js
existingRequests.map(function(existingRequest) {
if (!setOfExpectedUrls.has(existingRequest.url)) {
return cache.delete(existingRequest);
}
})

On Fetch

Although the comments in source code have elaborated everything well, I wanna highlight some points during the request intercepting duration.

Should Respond?

Firstly, we need to determine whether this request was included in our “pre-caching list”. If it was, this request should have been pre-fetched and pre-cached thus we can respond it directly from cache.

1
2
3
// sw.js*  
var url = event.request.url
shouldRespond = urlsToCacheKeys.has(url);

Noticed that we are matching raw URLs (e.g. http://localhost/js/a.js) instead of the hashed ones. It prevent us from calculating hashes at runtime, which would have a significant cost. And since we have kept the relationship in urlToCacheKeys it’s easy to index the hashed one out.

* In real cases, sw-precache would take ignoreUrlParametersMatching and directoryIndex options into consideration.

One interesting feature that sw-precache provided is navigationFallback(previously defaultRoute), which detect navigation request and respond a preset fallback HTML document when the URL of navigation request did not exist in urlsToCacheKeys.

It is presented for SPA using History API based routing, allowing responding arbitrary URLs with one single HTML entry defined in navigationFallback, kinda reimplementing a Nginx rewrite in service worker*. Do noticed that service worker only intercept document (navigation request) inside its scope (and any resources referenced in those documents of course). So navigation towards outside scope would not be effected.

* navigateFallbackWhitelist can be provided to limit the “rewrite” scope.

Respond from Cache

Finally, we get the appropriate cache key (the hashed URL) by raw URL with urlsToCacheKeys and invoke event.respondWith() to respond requests from cache directly. Done!

1
2
3
4
5
6
7
8
9
// sw.js*
event.respondWith(
caches.open(cacheName).then(cache => {
return cache.match(urlsToCacheKeys.get(url))
.then(response => {
if (response) return response;
});
})
);

* The code was “ES6-fied” with error handling part removed.

Cache Management Recap

That’s recap the cache management part with a full lifecycle simulation.

The first build

Supposed we are in the very first load, the cachedUrls would be a empty set thus all subresources listed to be pre-cached would be fetched and put into cache on SW install time.

1
2
3
4
5
6
7
8
9
10
11
12
// cachedUrls  
Set(0) {}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js?_sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css?_sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=3cb4f0
[sw] GET b.css?_sw-precache=c5a951

After that, it will start to control the page immediately because the sw.js would call clients.claim() by default. It means the sw.js will start to intercept and try to serve future fetches from caches, so it’s good for performance.

In the second load, all subresouces have been cached and will be served directly from cache. So none requests are sent from sw.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
// Empty

The second build

Once we create a byte-diff of our subresouces (e.g., we modify a.js to a new version with hash value d6420f) and re-run the build process, a new version of sw.js would be also generated.

The new sw.js would run alongside with the existing one, and start its own installation phase.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// cachedUrls  
Set(2) {
"http.../js/a.js? _sw-precache=3cb4f0",
"http.../css/b.css? _sw-precache=c5a951"
}

// urlToCacheKeys
Map(2) {
"http.../js/a.js" => "http.../js/a.js? _sw-precache=d6420f",
"http.../css/b.js" => "http.../css/b.css? _sw-precache=c5a951"
}

// SW Network Logs
[sw] GET a.js?_sw-precache=d6420f

This time, sw.js see that there is a new version of a.js requested, so it fetch /js/a.js?_sw-precache=d6420f and put the response into cache. In fact, we have two versions of a.js in cache at the same time in this moment.

1
2
3
4
// what's in cache?
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

By default, sw.js generated by sw-precache would call self.skipWaiting so it would take over the page and move onto activating phase immediately.

1
2
3
4
5
6
7
8
9
10
11
12
13
// existingRequests
http.../js/a.js?_sw-precache=3cb4f0
http.../js/a.js?_sw-precache=d6420f
http.../css/b.css?_sw-precache=c5a951

// setOfExpectedUrls
Set(2) {
"http.../js/a.js?_sw-precache=d6420f",
"http.../css/b.css?_sw-precache=c5a951"
}

// the one deleted
http.../js/a.js?_sw-precache=3cb4f0

By comparing existing requests in the cache with set of expected ones, the old version of a.js would be deleted from cache. This ensure there is only one version of our site’s resources each time.

That’s it! We finish the simulation successfully.

Conclusions

As its name implied, sw-precache is designed specifically for the needs of precaching some critical static resources. It only does one thing but does it well. I’d love to give you some opinionated suggestions but you decide whether your requirements suit it or not.

Precaching is NOT free

So don’t precached everything. Sw-precache use a “On Install — as a dependency” strategy for your precache configs. A huge list of requests would delay the time service worker finishing installing and, in addition, wastes users’ bandwidth and disk space.

For instance, if you wanna build a offline-capable blogs. You had better not include things like 'posts/*.html in staticFileGlobs. It would be a huge disaster to data-sensitive people if you have hundreds of posts. Use a Runtime Caching instead.

“App Shell”

A helpful analogy is to think of your App Shell as the code and resources that would be published to an app store for a native iOS or Android application.

Though I always consider that the term “App Shell” is too narrow to cover its actual usages now, It is widely used and commonly known. I personally prefer calling them “Web Installation Package” straightforward because they can be truly installed into users’ disks and our web app can boot up directly from them in any network environments. The only difference between “Web Installation Package” and iOS/Android App is that we need strive to limit it within a reasonable size.

Precaching is perfect for this kinda resources such as entry html, visual placeholders, offline pages etc., because they can be static in one version, small-sized, and most importantly, part of critical rendering path. We wanna put first meaningful paint ASAP to our user thus we precache them to eliminate HTTP roundtrip time.

BTW, if you are using HTML5 Application Cache before, sw-precache is really a perfect replacement because it can cover nearly all use cases the App Cache provide.

This is not the end

Sw-precache is just one of awesome tools that can help you build service worker. If you are planing to add some service worker power into your website, Don’t hesitate to checkout sw-toolbox, sw-helper (a new tool Google is working on) and many more from communities.

That’s all. Wish you enjoy!

本文首发于我的知乎专栏 The Little Programmer,转载请保留链接 ;)

一年半前,我曾和 Flash 作过一次告别。那一次,Adobe Flash Professional CC 被重新命名为了 Adobe Animate CC,宣告着 Flash 作为一个创作工具走到了尽头。

而今天,通过 Chromium 博客 So long, and thanks for all the Flash 我才得知,Adobe 官博在 Flash & The Future of Interactive Content 一文中,宣布将在 2020 年底时停止发布与更新 Flash Player。这一次,意味着 Flash 作为一个平台走到了尽头。

在不少人眼里,Flash 与 HTML5 是纯粹的竞争关系,我们应该为 HTML5 与 Open Web 标准的胜利欢呼,而将 Flash 狠狠的咒骂在黄泉之下。但其实,大多数人都忘记了,或是从不曾知道:HTML5(严谨的来说,其 marketing 含义中所涵盖的那些 Web APIs),有很大一部分正是 Flash 平台、Flash 社区对 web 标准做出的贡献。

正如 Flash & The Future of Interactive Content 所说:

Adobe has long played a leadership role in advancing interactivity and creative content – from video, to games and more – on the web. Where we’ve seen a need to push content and interactivity forward, we’ve innovated to meet those needs. Where a format didn’t exist, we invented one – such as with Flash and Shockwave. And over time, as the web evolved, these new formats were adopted by the community, in some cases formed the basis for open standards, and became an essential part of the web.

当我们(企业、用户)需要 web 平台承载包括视频、游戏在内的各种富交互内容而 web 平台本身还不具备这样的能力时,我们通过给予这个平台一种新的格式,以满足大家的需求,这就是 Flash Player,作为一种私有平台与浏览器插件,却能一度成为 web 事实标准的客观原因。

而时至今日,这些 web 平台所欠缺的能力,在得到市场与社区的认可之后,逐渐被从 Flash 中吸收与扬弃,成为了诸如 HTML5 Video/Audio/Canvas、WebGL 这些真正的 Open Web 标准。这时候,这些在诞生之初颇为创新的,作为了一种「过渡手段」、「Shim」的私有平台,便自然而然的,慢慢的不再被需要了。

这并不应该理解为一种失败,而应该说,它们「功成身退」了。


ActionScript 3.0,Flash 中的御用编程语言,作为 ES4 的唯一实现,推动了 ECMAScript 标准的发展,深远得影响着现代 JavaScript

Adobe Flex,Flash 平台的企业开发框架,在今年和 @徐飞 老师聊到时,还一起怀念并认可其相比现代 web 前端/客户端开发在工具链、协作、兼容性、UI 组件等方面的先进与成熟;
Adobe AIR,作为最早借鉴 JRT 将 web 相关技术的 Runtime 植入操作系统或捆绑在可执行文件内的跨平台开发方案,或许可以视作 Cordova、Electron、NodeWebkit、ReactNative 这些方案的一个前身与成功先例;

Microsoft IE 私有技术 ActiveX 中的 XMLHTTP,作为 XMLHTTPRequest 的前身,促进了 Ajax 的诞生与 Web 2.0 时代的来临;

Google Gears 作为 2008 年时为了增强 web 应用的浏览器插件,其私有 API 分别是 App Cache、Web Worker、WebSQL 等标准或标准未遂的前身;

Cordova/Phonegap 作为第一个面向移动端的 Hybrid 方案,成为了 web 开发与移动设备的 polyfill 与桥梁,加速了 Web 平台 Device APIs 的发展,并与 WebOS、FirefoxOS、Chrome Apps、Windows Runtime Apps 等一同影响了 Progressive Web App 的出现;

Google Extension 中 Background Page 与 Event Page 多年对 web 平台后台持续计算的尝试,直接帮助了 Service Worker 的 API 设计;

Google 的 NativeClient、Mozilla 的 asm.js 对于 web 追逐 native 性能的极致追求,则奠定了 Web Assembly 的诞生……

你看,在这条道路上,Flash 与它的朋友们,其实并不孤单。

「看到你长大了,我也就可以心满意足的离开了。」

就像是, web 技术发展的必然规律一样,

而 Open Web 则因此不朽。


我很高兴,Google Chrome、Mozilla Firefox、Microsoft Edge 都能这么写到:

Flash helped make the web a rich, dynamic experience, and shaped the modern set of web standards.

— “So long, and thanks for all the Flash“ Chromium Blog

Over the years, Flash has helped bring the Web to greatness with innovations in media and animation, which ultimately have been added to the core web platform.

— “Firefox Roadmap for Flash End-of-Life“ Mozilla Blog

Flash led the way on the web for rich content, gaming, animations, and media of all kinds, and inspired many of the current web standards powering HTML5.

— “The End of an Era – Next Steps for Adobe Flash“ Windows Blog

感谢你,Flash。

感谢你们,那些「功成身退」的你们。

这篇文章转载自我在知乎上的回答

对我来说,CSS 难学以及烦人是因为它「出乎我意料之外的复杂」且让我觉得「定位矛盾」

@方应杭 老师的答案我赞了:CSS 的属性互不正交,大量的依赖与耦合难以记忆。

@顾轶灵 @王成 说得也没错:CSS 的很多规则是贯彻整个体系的,而且都记在规范里了,是有规律的,你应该好好读文档而不是去瞎试。

CSS是一门正儿八经的编程语言,请拿出你学C++或者Java的态度对待它

但是问题就在这了,无论从我刚学习前端还是到现在,我都没有把 CSS 作为一门正儿八经的编程语言(而且显然图灵不完全的它也不是),CSS 在我眼里一直就是一个布局、定义视觉样式用的 DSL,与 HTML 一样就是一个标记语言。

写 CSS 很有趣,CSS 中像继承、类、伪类这样的设计确实非常迎合程序员的思路,各种排列组合带来了很多表达上的灵活性。但如果可以选择,在生产环境里我更愿意像 iOS/Android/Windows 开发那样,把这门 DSL 作为 IDE WYSIWYG 编辑器的编译目标就可以了,当然你可以直接编辑生成的代码,但我希望「对于同一种效果,有比较确定的 CSS 表达方式」

因为我并不在 CSS 里处理数据结构,写算法、业务逻辑啊,我就是希望我能很精确得表达我想要的视觉效果就可以了。如果我需要更复杂的灵活性和控制,你可以用真正的编程语言来给我暴露 API,而不是在 CSS 里给我更多的「表达能力」

CSS 语言本身的表达能力对于布局 DSL 来说是过剩的,所以你仅仅用 CSS 的一个很小的子集就可以在 React Native 里搞定 iOS/Android 的布局了。你会发现各个社区(典型如 React)、团队都要花很多时间去找自己项目适合的那个 CSS 子集(so called 最佳实践)。而且 CSS 的这种复杂度其实还挺严重得影响了浏览器的渲染性能,很多优化变得很难做。

而 CSS 的表达能力对于编程语言来说又严重不够,一是语言特性不够,所以社区才会青睐 Less、Sass 这些编译到 CSS 的语言,然后 CSS 自己也在加不痛不痒的 Variable。二是 API 不够,就算你把规范读了,你会发现底层 CSSOM 的 Layout、Rendering 的东西你都只能强行用声明式的方式去 hack(比如用 transform 开新的 composition layer)而没有真正的 API 可以用,所以 W3C 才会去搞 Houdini 出来。

这种不上不下的感觉就让我觉得很「矛盾」,你既没法把 CSS 当一个很简单的布局标记语言去使用,又没办法把它作为一个像样的编程语言去学习和使用。

在写 CSS 和 debug CSS 的时候我经常处在一种「MD 就这样吧反正下次还要改」和「MD 这里凭什么是这样的我要研究下」的精分状态,可是明明我写 CSS 最有成就感的时候是看到漂亮的 UI 啊。

以上。

这篇文章转载自我在知乎上的回答

我用 Python 伪代码来解释下,我觉得对这个问题有兴趣的应该都是有点编程基础的,所以直接上 code 应该是最容易的。

背景知识

「停机问题」研究的是:是否存在一个「程序」,能够判断另外一个「程序」在特定的「输入」下,是会给出结果(停机),还是会无限执行下去(不停机)。

在下文中,我们用「函数」来表示「程序」,「函数返回」即表示给出了结果。

正文

我们假设存在这么一个「停机程序」,不管它是怎么实现的,但是它能够回答「停机问题」:它接受一个「程序」和一个「输入」,然后判断这个「程序」在这个「输入」下是否能给出结果:

1
2
3
def is_halt(program, input) -> bool:
# 返回 True 如果 program(input) 会返回
# 返回 False 如果 program(input) 不返回

(在这里,我们通过把一个函数作为另一个函数的输入来描述一个「程序」作为另一个「程序」的「输入」,如果你不熟悉「头等函数」的概念,你可以把所有文中的函数对应为一个具备该函数的对象。)

为了帮助大家理解这个「停机程序」的功能,我们举个使用它的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from halt import is_halt

def loop():
while(True):
pass

# 如果输入是 0,返回,否则无限循环
def foo(input):
if input == 0:
return
else:
loop()

is_halt(foo, 0) # 返回 True
is_halt(foo, 1) # 返回 False

是不是很棒?

不过,如果这个「停机程序」真的存在,那么我就可以写出这么一个「hack 程序」:

1
2
3
4
5
6
7
8
9
10
11
from halt import is_halt

def loop():
while(True):
pass

def hack(program):
if is_halt(program, program):
loop()
else:
return

这个程序说,如果你 is_haltprogram(program) 判停了,我就无限循环;如果你判它不停,我就立刻返回。

那么,如果我们把「hack 程序」同时当做「程序」和「输入」喂给「停机程序」,会怎么样呢?

1
is_halt(hack, hack)

你会发现,如果「停机程序」认为 hack(hack) 会给出结果,即 is_halt(hack, hack)) 返回 True) ,那么实际上 hack(hack) 会进入无限循环。而如果「停机程序」认为 hack(hack) 不会给出结果,即 is_halt(hack, hack) 返回 ,那么实际上 hack(hack) 会立刻返回结果……

这里就出现了矛盾和悖论,所以我们只能认为,我们最开始的假设是错的:

这个「停机程序」是不存在的。

意义

简单的说,「停机问题」说明了现代计算机并不是无所不能的。

上面的例子看上去是刻意使用「自我指涉」来进行反证的,但这只是为了证明方便。实际上,现实中与「停机问题」一样是现代计算机「不可解」的问题还有很多,比如所有「判断一个程序是否会在某输入下怎么样?」的算法、Hilbert 第十问题等等,wikipedia 甚至有一个 List of undecidable problems

漫谈

如果你觉得只是看懂了这个反证法没什么意思:

  1. 最初图灵提出「停机问题」只是针对「图灵机」本身的,但是其意义可以被推广到所有「算法」、「程序」、「现代计算机」甚至是「量子计算机」。
  2. 实际上「图灵机」只能接受(纸带上的)字符串,所以在图灵机编程中,无论是「输入」还是另一个「图灵机」,都是通过编码来表示的。
  3. 「图灵机的计算能力和现代计算机是等价的」,更严谨一些,由于图灵机作为一个假象的计算模型,其储存空间是无限大的,而真实计算机则有硬件限制,所以我们只能说「不存在比图灵机计算能力更强的真实计算机」。
  4. 这里的「计算能力」(power)指的是「能够计算怎样的问题」(capablity)而非「计算效率」(efficiency),比如我们说「上下文无关文法」比「正则表达式」的「计算能力」强因为它能解决更多的计算问题。
  5. 「图灵机」作为一种计算模型形式化了「什么是算法」这个问题(邱奇-图灵论题)。但图灵机并不是唯一的计算模型,其他计算模型包括「Lambda 算子」、μ-递归函数」等,它们在计算能力上都是与「图灵机」等价的。因此,我们可以用「图灵机」来证明「可计算函数」的上界。也因此可以证明哪些计算问题是超出上界的(即不可解的)。
  6. 需要知道的是,只有「可计算的」才叫做「算法」。
  7. 「停机问题」响应了「哥德尔的不完备性定理」。

拓展阅读:

中文:

英文:

这篇文章转载自我在知乎上的回答

严谨的证明的话,可以使用「形式语言」(Formal language)来证明:

在可计算理论和计算复杂度理论中,每个「计算问题」都被描述为一个一个「形式语言」,即字符串的集合。比如对于判断一个图是否是无向连通图这个问题:我们可以写为一个描述所有无向连通图的集合:

$$
A = { \langle G \rangle \vert G \text{ is a connected undirected graph}}
$$

由于图灵机只能接受字符串,所以这里的尖括号表示对图的「编码」。出于简单,我们全部使用现实计算机所使用的字母表
$\Sigma = \{0, 1\}$,所以「编码」即一个对象的二进制字符串描述。

如果我们能构造出一个图灵机来「决定」这个「形式语言」,即可以判断一个「输入」是否属于这个集合(membership 与 non-membership),那么我们可以说我们用「图灵机」描述了一个「算法」来计算这个问题,而这个「计算问题」所对应的函数是「可计算的」,否则是「不可计算的」。(注 1)

那么,如果我们有一个包含了所有「可计算函数」的集合,这个集合会有多大呢?


由于

  • 所有「可计算函数」总有一个对应的「图灵机」来计算它
  • 每一个「图灵机」都可以被「编码」为一个不同的 0、1 序列,比如 000,010…
  • 0、1 序列、即二进制,总是可以被转换为一个十进制数的

所以,我们这个集合实际上是与整数集 $Z$ 一样大(等势)的,我们把这个集合表示为 $\Sigma^{*}$。 易知 $Z$ 是「无穷可数(countably infinite)」的,所以我们有无穷可数个「可计算函数」(注 2)。


而「计算问题」有多少个呢?

这个问题可以等同于,我们有多少个形如 $\{000, 010\}$ 这样的 0,1 序列的集合?即 $\Sigma^{*}$ 这个集合有多少个子集?用数学语言描述就是求 $\Sigma^{*}$ 的幂集的势 $| P(\Sigma^{*})|$ 。

由于 $\Sigma^{*}$ 与 $Z$ 是等势的,所以这个问题等价于求 $|P(Z)|$ 的大小。根据 Cantor’s theorem,一个「无穷可数」的集合的幂集是「无穷不可数(uncountably infinite)」的。(注 3)


根据 Cantor’s theorem,「无穷不可数集」要比「无穷可数集」大。

同时,「无穷不可数集」减去「无穷可数集」后仍然是「无穷不可数集」。(注 4)

所以,「不可计算函数集」,即「计算问题集」与「可计算函数集」的差,仍是「无穷不可数集」,仍比是为「无穷可数集」的「可计算函数集」大。

因此,「不可计算的函数」比「可计算的函数」多。

证毕。


注:

  1. 可计算函数」是算法的直觉说法,「邱奇-图灵论题」猜想任何在算法上可计算的问题同样可以由图灵机计算。但图灵机并不是唯一的计算模型,其他计算模型包括「Lambda 算子」、「$\mu$ - 递归函数」等,它们在计算能力上都是与「图灵机」等价的。
  2. 证明「所有可计算函数」的集合是「无穷可数集」的方式有很多,只要找到任意一个与「自然数集」的「双射」即可
  3. 也可以直接用康托的对角线法(Cantor’s diagonal argument)证明「所有计算问题」的集合是「无穷不可数集」
  4. 可以用反证法得证
  5. 知乎能用 LaTex 了好评
  6. Aleph Number - Wikipedia

本文首发于我的知乎专栏 The Little Programmer,转载请保留链接 ;)

有一类程序员是 visionary 型的,为了实现一些超前的 idea,绕过某些技术的限制,他们写的 code 晦涩高深得只有他们自己能懂,做出来的 tool 看上去很美好结果处处是坑出了 bug 根本没法查,但正是这类人不断创造出新的东西,在洗礼之后成为一个个 big thing。

我每周都要被 infra 的坑 block 得无法工作几次搞得非常沮丧,后来我发现这个锅除了要扔给 FB 外,还有一大半要扔给我周围这群 visionary 的同事们,我工作直接需要接触到的区区五六个人,发起/创造了 Infer, React, Reason, ReasonReact, BuckleScript…

所以这大概就是见证/参与这些 idea 成长的代价吧,也意识到这些东西不是在刚开始就像后来大家接受流行时那么美好的。React 发布 5 周年生日时回放 Jordan/Tom 2013 年第一次对外发布 React/JSX 的视频。我问 Jordan 说你后来怎么没再去分享了。他说你不知道我那天讲完下来被所有听众指着批评。React 第一次在内部使用是 2011 年在 news feed,然后是 2012 年 instagram (pete hunt),所以这个时间其实很长很长。

很多人(包括我)都会经常觉得 XYZ 新事物跟老东西比太新、太不成熟、体验太不好、想要解决的问题太多、解决方案太 overkill、然后就没有然后了,但其实说不定你在看的这个就是 next big thing 呢。这些梦想家们 vision 里的 big picture 太大了,有的人可能在半个 picture 出来的时候就可以看出来了,有的人则可能要等到整个 picture 都快填满了才看得出来。

如果不是因为 Ads/Messenger 的坑深 React/Reason/Flux 也就不会在这里诞生了,

如果不是因为 Facebook 的坑深 GraphQL/Infer/Hack/Flow/Buck 也就不会在这里诞生了。

正是有一群开垦者不怕坑深才使得各种 idea 成为了大家手上好用的 tool 啊。

梦想家程序员们的工作价值于实干主义的程序员,总是很容易在过程中被低估、忽视,或是得不到尊重。而又在流行之后被神化,仿佛是那个人早已洞察一切一样。其实梦想家的工作,也是一点点累加,一点点迭代起来的。他们也需要伯乐和追随者的支持和帮助。

Chenglou 这个人总是在巨兴奋与巨沮丧之间切换,这段时间下来,我开始能感受这种情绪的来源了。

他总是用一句话来总结他回答我的吐槽、抱怨、疑问、惊叹,我就用这句话来结尾好了:

“Welcome to the producer side!”

“Avoiding success at all cost” is the informal motto behinds Haskell. It could be parenthesized in two ways, either “Avoiding (success at all cost)” or “(Avoiding sucess) (at all cost)”.

I’m not going to interpret them directly but rather to share some thoughts on “the success vs. costs” basing on my very own understanding and experience.

The success vs. cost of language design

There’re always trade offs (or compromises) in any software design, and programming language design has no exceptions.

In other words, all language design decision that made them “successful” i.e. being popular and widely-used in industry or education for some reasons, all comes with their own “costs”: being unsafe, limited expressiveness, or having bad performance, etc.

Whether or not the “cost” is a problem really depends on scenarios, or their goals. For instances, Python/JavaScript are both very expressive and beginner-friendly by being dynamically-typed, sacrifing the type safety and performance. Java, in constrast, uses a much safer and optimization-friendly type system but being much less expressive. Another typicial comparison would be memory management in programming languages, where languages that are “managed” (by either ARC or Gabage Collector) could be much easier and safer (in terms of memory) for most programmers but also considerred slower than languages that are “closer to the metal”.

None of these “costs”, or “differences”, really prevent them from being immortally popular.

For Haskell, the story becomes quite different: being research-oriented means the goal of this language is to pursue some “ultimate” things: the “ultimate” simplicity of intermediate representation, the “ultimate” type system where safety and expressiveness can coexist, the “ultimate” compilation speed and runtime performance, the “ultimate” concise and elegant concrete syntax, the “ultimate”…I don’t know. But it has to be some “ultimate” things that is very difficult, probably endless and impossible, to achieve.

This, as a result, made all language decisions in Haskell became very hard and slow, because almost nothing can be scarified. That’s why Haskell insisted to be lazy to “guard” the purity regardless of some problems of being “call-by-need”; a decent IO mechanisms is missing in the first 4 yrs after the project’s start until P Walder found Monad; and the Type Class, which is first proposed in P Walder’s 1989 paper, spent yrs long to implement and popularize.

As a side note though, it doesn’t mean there is no compromise in Haskell at all. It’s just as minimized as it could be during its progress. When one audience asking why we have Haskell and OCaml, which’re quite similar in very high level, both survived, SPJ replies:

There’s just a different set of compromises.

The success vs. cost of language design process

Another common but extremely controversial (if not the most) topics of programming language design is about its design process: Would you prefer dictatorship or a committee (in other words, a dictatorship of many?)? Would you prefer being proprietary or standardized? In which form would you write the standards, in human nature language, pseudo code, or formal semantics? How many and how frequently breaking changes dare you make? Would you let open source community involve in?

Again, I think there is no THE answer for all those questions. Majority of popular programming languages came and are still on going with very different paths.

Python, whose creater, Guido van Rossum, known as the “Benevolent Dictator For Life” (BDFL), i.e. good kind of dictator, still play the central role (until July 2018) of the Python’s development after Python getting popular and adapt a open source and community-based development model. This factor direcly contribute to the fact that Python 3, as a breaking (not completely backward-compatible and not easy to port) but good (in terms of language design and consistency) revision of the language can still be landed, despite of many communities’ pressures. There’re many language (Ruby, Perl, Elm) also choose to follow this route.

JavaScript, widely known as being created by Brendan Eich in 10 days, in comparision, quickly involved into a committee (TC39) and standardized (ECMAScript) language due to both the open nature of the Web and fast adoption of itself. But Brendan, as the creater, wasn’t even powerful enough to push the committee landing ES4, which is also a breaking but much better revision, but ended up with the ES5 (Harmony), a backward-compatible, yet much less ambitious version due to many political “fights” between different parties (e.g. Mozilla, Microsoft, Yahoo etc.) thus the history wasn’t changed. Even the latest rising and yearly releasing of the “modern” JavaScript (ES6 or ES2015, 2016, 2017…) are mainly driven by the new generation of committee parties (+ Google, Facebook, Airbnb etc.) and still in a very open and standardized way.

As you can see here, even the history and progress of two rather similar languages can be so different, not to mention more proprietary languages such as Java from Sun/Oracle, C# from Microsoft, OC/Swift from Apple (though the latter was open sourced) or more academia and standardized language like SML and Scheme which both has a standard written in formal semantics.

So it’s not not obvious that Haskell, also chose its own unique process to suit its unique goal. Although it backs on academia, it chose a rather practical/less-formal approach to define the language, i.e. the compiler implementation over standardization (plus many “formal” fragments among papers though), which is more like C++/OCaml from this point of view. It has a committee, but instead of being very open and conservative, it’s more dictatorial (in terms of average users) and super aggressive in terms of making breaking changes. As a result however, it trained a group of very change-tolerant people in its community…All of these quirks and odds combined works very well and avoid the Haskell “becoming too success too quickly”.

End thoughts

To be fair, Haskell has alreay been very “successful” nowdays, in particular academia (for education, sexy type laboratory etc.) but also industry, either being used in real business or being very reputable among programmers (as being both hard and fun).

I am not confident and qualified to say Haskell is success in the right degree at the right time. But it’s great to see it, after more than 20 and now almost 30 yrs, slowly figure out its very own way, to “Escape from the Ivory Tower”, and keep going beyond.

Update: 我最后还是放弃把 Vim 作为主要编辑器来输入中文了,整体使用下来 mental model 的 cost 太重了。记笔记时用用中文呀或者改改博客时偶尔用一下还蛮去,这个时候这个功能至少能帮助你 Esc 之后不煞笔,所以也不算完全没有价值吧……


我相信很多中文世界的 Vimer 都遇到过这个烦恼,在 vim 的 insert 模式时可能突然想输个中文,输完之后会本能的直接 esc 接 normal 模式操作,结果发现跳出来的是中文输入法……对于 vscode,我一般会在几次错误之后被逼到退出 vscode vim 模式,而对于终端中用的 neovim,就只能尽量不输入中文了。

为了满足我 1% 用 vim 输入中文的场景(比如写博客),我还是想看看有没有什么解决方案,Google 出来的解决方案基本是:在退出 insert 模式时记住当时的输入法,并自动切换到默认输入法(一般是英文)给 normal 模式用,并且在下一次进入 insert 模式时再切换回来。

原生 vim 的话,可以使用 smartim 插件,原理是调用 im-select 这个 CLI 工具来切换输入法。

对于 VSCode-vim 的话,smartim 的移植也在近期的 PR 中被 merge 到了插件里,详情见文档的这部分配置,需要指定一下默认输入法和 im-select 的 binary 路径就好。



不过实话说,在 vim 中编辑中文的效率和体验和英文比都是大打折扣的。因为中文分词难度太高,不像英文可以简单依靠一个 split " " 搞定。所以其实无论 vim(word,begin,end),emacs 还是操作系统自带的(比如 macOS 中的 alt + 箭头) 「按词移动」功能对于中文都仅仅是跳转到下一个空格处而已,对于中文来说基本就是下一句了……其他常用操作诸如 f/, replace, till 也都无法很好的工作,基本只能靠 hjkl 爬行……

不过也算聊胜于无吧,由于我的主力外置键盘是 HHKB,能用 vim 操作的一个子集(hjkl, o, A, I, v etc.)可能也比按住 Fn 的方向键好用……

0%