转载自:https://github.com/seajs/seajs/issues/588 感谢原作者分析与共享。
最近不断有人问及,想起前些天跟 @dexteryy 等人的讨论:dexteryy/OzJS#10 当时有过简单总结,重新梳理如下。
写在前面
- 不谈什么:传统的模块化开发方式,比如文件拆分、全局变量、命名空间,以及 YUI3 式的模块化开发方式。有兴趣的可阅读:#547
- 谈什么: 关于 CommonJS、AMD、Node.js、CMD 等相关的故事与未来趋势,很有意思。
- 不一定精准:本文是基于史实的扯淡,因此部分文字特别是时间都是模糊记忆,不一定精准。关于流派、趋势则是个人在社区的感受,不代表客观看法。(看法都是主观的,呵呵)
CommonJS 社区
大概 09 年 - 10 年期间,CommonJS 社区大牛云集。CommonJS 原来叫 ServerJS,推出 Modules/1.0 规范后,在 Node.js 等环境下取得了很不错的实践。
09年下半年这帮充满干劲的小伙子们想把 ServerJS 的成功经验进一步推广到浏览器端,于是将社区改名叫 CommonJS,同时激烈争论 Modules 的下一版规范。分歧和冲突由此诞生,逐步形成了三大流派:
- Modules/1.x 流派。这个观点觉得 1.x 规范已经够用,只要移植到浏览器端就好。要做的是新增 Modules/Transport 规范,即在浏览器上运行前,先通过转换工具将模块转换为符合 Transport 规范的代码。主流代表是服务端的开发人员。现在值得关注的有两个实现:越来越火的 component 和走在前沿的 es6 module transpiler。
- Modules/Async 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范。这个观点下的典型代表是 AMD 规范及其实现 RequireJS。这个稍后再细说。
- Modules/2.0 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范,但应该尽可能与 Modules/1.x 规范保持一致。这个观点下的典型代表是 BravoJS 和 FlyScript 的作者。BravoJS 作者对 CommonJS 的社区的贡献很大,这份 Modules/2.0-draft 规范花了很多心思。FlyScript 的作者提出了 Modules/Wrappings 规范,这规范是 CMD 规范的前身。可惜的是 BravoJS 太学院派,FlyScript 后来做了自我阉割,将整个网站(flyscript.org)下线了。这个故事有点悲壮,下文细说。
AMD 与 RequireJS
再来说 AMD 规范。真正的 AMD 规范在这里:Modules/AsynchronousDefinition。AMD 规范一直没有被 CommonJS 社区认同,核心争议点如下:
执行时机有异议
看代码
Modules/1.0:
var a = require("./a") // 执行到此处时,a.js 才同步下载并执行
AMD:
define(["require"], function(require) {
// 在这里,模块 a 已经下载并执行好
// ...
var a = require("./a") // 此处仅仅是取模块 a 的 exports
})
AMD 里提前下载 a.js 是浏览器的限制,没办法做到同步下载,这个社区都认可。
但执行,AMD 里是 Early Executing,Modules/1.0 里是第一次 require 时才执行。这个差异很多人不能接受,包括持 Modules/2.0 观点的也不能接受。
这个差异,也导致实质上 Node 的模块与 AMD 模块是无法共享的,存在潜在冲突。
模块书写风格有争议
AMD 风格下,通过参数传入依赖模块,破坏了 就近声明 原则。比如:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面申明并初始化了要用到的所有模块
if (false) {
// 即便压根儿没用到某个模块 b,但 b 还是提前执行了
b.foo()
}
})
还有就是 AMD 下 require 的用法,以及增加了全局变量 define 等细节,当时在社区被很多人不认可。
最后,AMD 从 CommonJS 社区独立了出去,单独成为了 AMD 社区。有阵子,CommonJS 社区还要求 RequireJS 的文档里,不能再打 CommonJS 的旗帜(这个 CommonJS 社区做得有点小气)。
脱离了 CommonJS 社区的 AMD 规范,实质上演化成了 RequireJS 的附属品。比如
- AMD 规范里增加了对 Simplified CommonJS Wrapper 格式的支持。这个背后是因为 RequireJS 社区有很多人反馈想用 require 的方式,最后 RequireJS 作者妥协,才有了这个半残的 CJS 格式支持。(注意这个是伪支持,背后依旧是 AMD 的运行逻辑,比如提前执行。)
- AMD 规范的演进,离不开 RequireJS。这有点像 IE…… 可能是我的偏见。
AMD 的流行,很大程度上取决于 RequireJS 作者的推广,这有点像 less 因 Bootstrap 而火起来一样。但火起来的东西未必好,比如个人觉得 stylus 就比 less 更优雅好用。
关于 AMD 和 RequireJS,暂且按下不表。来看另一条暗流:Modules/2.0 流派。
Modules/2.0
BravoJS 的作者 Wes Garland 有很深厚的程序功底,在 CommonJS 社区也非常受人尊敬。但 BravoJS 本身非常学院派,是为了论证 Modules/2.0-draft 规范而写的一个项目。学院派的 BravoJS 在实用派的 RequireJS 面前不堪一击,现在基本上只留存了一些美好的回忆。
这时,Modules/2.0 阵营也有一个实战派:FlyScript。FlyScript 抛去了 Modules/2.0 中的学究气,提出了非常简洁的 Modules/Wrappings 规范:
module.declare(function(require, exports, module)
{
var a = require("a");
exports.foo = a.name;
});
这个简洁的规范考虑了浏览器的特殊性,同时也尽可能兼容了 Modules/1.0 规范。悲催的是,FlyScript 在推出正式版和官网之后,RequireJS 当时正直红火。期间 FlyScript 作者 khs4473 和 RequireJS 作者 James Burke 有过一些争论。再后来,FlyScript 作者做了自我阉割,将 GitHub 上的项目和官网都清空了,官网上当时留了一句话,模糊中记得是
我会回来的,带着更好的东西。
这中间究竟发生了什么,不得而知。后来我有发邮件给 @khs4473 询问,khs 给了两点挺让我尊重的理由,大意是
- 我并非前端出身,RequireJS 的作者 James Burke 比我更懂浏览器。
- 我们应该协同起来推动一个社区的发展,即便它不是你喜欢的。
这两句话对我影响很大。也是那之后,开始仔细研究 RequireJS,并通过邮件等方式给 RequireJS 提出过不少建议。
再后来,在实际使用 RequireJS 的过程中,遇到了很多坑。那时 RequireJS 虽然很火,但真不够完善。期间也在寻思着 FlyScript 离开时的那句话:“我会回来的,带着更好的东西”
我没 FlyScript 的作者那么伟大,在不断给 RequireJS 提建议,但不断不被采纳后,开始萌生了自己写一个 loader 的念头。
这就是 Sea.js。
Sea.js 借鉴了 RequireJS 的不少东西,比如将 FlyScript 中的 module.declare
改名为 define
等。Sea.js 更多地来自 Modules/2.0 的观点,但尽可能去掉了学院派的东西,加入了不少实战派的理念。
最后
写着写着,有点沧桑感,不想写了。
历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。