Web 平台能从 Node.js 学到什么

  • 张龙

2015 年 12 月 24 日

话题:语言 & 开发架构

Will Binns-Smith是一位热爱 JavaScript 的全栈工程师,喜欢通过技术来解决实际问题。他开发了 Bonobos.com 的前端购物车功能。Will 喜欢与设计师一对一工作,将 PC 网站转换为针对更小的触摸设备的站点。近日,Will 撰写了一篇文章,谈到了 Node.js 有哪些做法和特性值得 Web 平台学习。

作为一名 Web 开发者,我们会非常感激 jQuery 之类的库,因为他们消除了底层平台的各种不一致与笨拙的情况。曾几何时,构建一个 XMLHTTPRequest 对象需要好几行代码,但现在只需一行 $.ajax 调用即可;通过 jQuery 与 DOM 交互很少需要我们针对特定的平台采取一些非常规手段,因为这一切 jQuery 都帮我们做好了。

使用过 Python 或是 Java 语言的开发者都知道这些语言自带了标准库。在浏览器端,jQuery 就是这样的标准库,此外还有诸如 underscore 之类的工具集。就像大多数标准库一样,我们所编写的代码都会严重依赖于他们。当越来越多的代码开始依赖这些 APIs 时,我们就很难在不破坏既有 Web 的情况下向前迈进了。不要妄图为了兼容性而包含进这些库的多个版本,他们常常会有 30KB 的大小,而且默认情况下会向全局的 window 对象写入。

过去,我认为这是软件开发不可避免的一个问题。但现在一切都变了,至少对于我来说是这样的,因为 Node.js 生态圈出现了。

小模块与组合的出现

Node 是一个构建在 Chrome V8 引擎之上的 JavaScript 运行时,它几乎没有多少标准库。相反,其他生态圈中的那些标准库都不在其核心平台中,而是通过 npm 获取的。

在 npm 中,小模块已经成为了标准,像是substackSindre Sorhus这样的用户分别已经发布了 685 和 760 个模块,这些模块都遵循着 UNIX 一次只做一件事,并将这件事做好的原则。像是 array-union(返回两个数组的并集)与 svg-create-element(提供了用于在 DOM 中创建 SVG 的优秀 API)这样的模块都是非常小的,看起来应该与语言或是平台一同发布。

Sindre 甚至还有一个名为 negative-zero 的模块,它只是判断一个值是否是 -0,实现只有一行代码。看起来为这样简单的功能创建一个包有些极端,因为用户可以在自己的代码中实现这样的功能,不过通过这种方式,我们可以集中修改代码,而不必重复实现细节。Sindre 对此有个很详尽的介绍,感兴趣的读者可以看看。

甚至连非常流行的Express Web 框架也只提供了 Web 应用的核心功能而已。与诸如 Ruby on Rails 或是 Django 这样的大型框架不同(本身带有模板、ORMs、csrf 防护,以及其他特性),Express 本身只提供了托管静态文件的中间件。相反,应用开发者可以自由使用他们喜欢的这些特性的实现,并将其组合起来创建应用。随着想法的不断改进,很多中间件包创建又消亡,一些发展起来了,另一些则消失了。这就是 Node 的哲学。

因此,小模块(以及由这些模块所构成的应用)会拥有非常庞大的依赖图(比如说,Bitbucket 的前端包含了 1000 多个 JavaScript 模块,其中一些是内部模块,另外一些则来自于 npm)。

这些模块最棒的一点就是他们并没有紧密绑定到平台上:他们不会受到标准库的影响,借助于语义版本化,他们可以对其 API 进行迭代而不会对依赖他们的用户造成困扰。

流介绍

Node 包含了流,它是异步流动数据的一个抽象,常常用于连接和转换 I/O 源。Node 对流的初始实现(随 Node 0.4 发布)并不完善,使用不当会导致数据丢失。为了解决这一问题,Node 0.10 对流进行了修正(也叫做 Streams2)。不过,Streams2 并不是对流模式的一个简单迭代,实际上在 Node 0.10 发布前经历了很多的变化。在发布时,它与运行在 Node 0.8 上的应用兼容。

这怎么可能呢?Streams2 源自于readable-stream模块,一开始它就是 Isaac Schlueter在 2012 年 7 月发布的独立模块,这距离2013 年 3 月它随着 Node 0.10 的发布已经相隔很长时间了。它经历了多次迭代,在这个过程中 API 与功能也不断成熟,Node 社区发现它非常适合于作为流的实现。

时至今日,readable-stream 的最新实现也是作为用户模块在 npm 中维护的,可用于 Node 0.8 及之前的版本中。很多用户都喜欢使用用户模块而不是绑定的模块,这样可以实现生态圈的兼容性。

一系列不幸的 APIs

与此相反,JavaScript 与 Web 平台中现有的 APIs 都是很难改变的。对 JavaScript 语言的迭代变更(不能有任何的向后不兼容变化以防止现有系统出现问题)必须要通过添加新特性来实现。比如说,在发现 Mutation Events API 的性能问题后,人们引入了 Mutation Observers 来解决这个问题;废弃 WebSQL,拥抱更加底层但使用起来却略显笨重的 IndexedDB;逐步淘汰 Application Cache,拥抱更加底层但更通用的 ServiceWorker。Object.observe 是 ES2016 的一个提案,通过它可以观测对象的属性,不过在 React 的单向数据流逐步流行起来并为主流所采用后,Object.observe 提案则被撤回了。

对这些感到困惑么?我们不应该期待着一下子就将事情做对,不过我们需要一个平台,通过这个平台可以试错,然后逐步向好的方向迭代。

平台演化

可扩展的 Web 宣言的支持者们希望 Web 能够像 Node 那样提供弹性的用户试错空间。其使命是让平台提供尽可能多的底层构建块,这样浏览器之外的库就可以自由尝试,从而避免正式的标准化流程所经历的代价高昂且冗长的过程。其中一种这样的底层原语就是Web Components的 APIs,它向开发者提供了通过 JavaScript 来创建动态自定义元素与属性的能力,这一切都是通过封装来实现的。一个库无论大小都实现了对话框功能,不过 API 的迭代可以交给用户来完成,一开始无需放在平台内部实现。借助于底层原语,用户可以通过小模块的形式自由探索更高层次的抽象。换句话说,我们不再需要 AppCache了。

幸好,我们已经看到了这种做法的好处。我们无需再陷入诸如 AppCache 这种实际使用很少的特性了。相反,我们有了一个Promises A+ Specification的标准化实现,npm 中的Q已经证明了这一点,并且在年初已经成为了 ES2015 的一部分。WHATWG 也在制订流的规范,在很大程度上它受到了来源于 Node 的流的演化的影响。

就像平台的其他方面一样,这些新标准是很难改变的,不过幸运的是,其想法与 APIs 都是通过用户来实现的,这一切都要归功于 Node!

语言 & 开发架构