JS模块加载

CommonJS,RequireJS,SeaJS 归纳笔记

Posted by Damon on 2017-03-23

Foreword

Here comes Module!

随着网站逐渐变成「互联网应用程序」,嵌入网页的 JavaScript 代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试……我们不得不使用软件工程的方法,来管理网页的业务逻辑。

于是,JavaScript 的模块化成为迫切需求。在 ES6 Module 来临之前,JavaScript 社区提供了强大支持,尝试在现有的运行环境下,实现模块的效果。


Catalog

  1. CommonJS & Node
  2. History
  3. RequireJS & AMD
  4. SeaJS & CMD
  5. AMD vs CMD
  6. WebPack
1
2
3
function autoDetect(){
console.log('ye, it is written in JavaScript!')
}

CommonJS & Node

Javascript: not just for browsers any more! —— CommonJS Slogen

前端模块化的事实标准之一,2009 年 8 月,CommonJS 诞生。

CommonJS 本质上只是一套规范(API 定义),而 Node.js 采用并实现了部分规范,CommonJS Module 的写法也因此广泛流行。

让我们看看 Node 中的实现:

1
2
3
4
5
6
7
8
9
// 由于 Node 原生支持模块的作用域,并不需要额外的 wrapper
// "as though the module was wrapped in a function"
var a = require('./a') // 加载模块(同步加载)
a.doSomething() // 等上一句执行完才会执行
exports.b = function(){ // 暴露 b 函数接口
// do something
}

exports是一个内置对象,就像require是一个内置加载函数一样。如果你希望直接赋值一个完整的对象或者构造函数,覆写module.exports就可以了。

CommonJS 前身叫 ServerJS ,后来希望能更加 COMMON,成为通吃各种环境的模块规范,改名为 CommonJS 。CommonJS 最初只专注于 Server-side 而非浏览器环境,因此它采用了同步加载的机制,这对服务器环境(硬盘 I/O 速度)不是问题,而对浏览器环境(网速)来说并不合适。

因此,各种适用于浏览器环境的模块框架与标准逐个诞生,他们的共同点是:

  • 采用异步加载(预先加载所有依赖的模块后回调执行,符合浏览器的网络环境)
  • 虽然代码风格不同,但其实都可以看作 CommonJS Modules 语法的变体。
  • 都在向着 COMMON 的方向进化:兼容不同风格,兼容浏览器和服务器两种环境

本文接下来要讨论的典例是:

  • RequireJS & AMD(异步加载,预执行,依赖前置。默认推荐 AMD 写法)
  • SeaJS & CMD(异步加载,懒执行,依赖就近,默认推荐 CommonJS 写法)

History

此段落参考自玉伯的 前端模块化开发那点历史

09-10 年间,CommonJS(那时还叫 ServerJS) 社区推出 Modules/1.0 规范,并且在 Node.js 等环境下取得了很不错的实践。

09年下半年这帮充满干劲的小伙子们想把 ServerJS 的成功经验进一步推广到浏览器端,于是将社区改名叫 CommonJS,同时激烈争论 Modules 的下一版规范。分歧和冲突由此诞生,逐步形成了三大流派:

  1. Modules/1.x 流派。这个观点觉得 1.x 规范已经够用,只要移植到浏览器端就好。要做的是新增 Modules/Transport 规范,即在浏览器上运行前,先通过转换工具将模块转换为符合 Transport 规范的代码。主流代表是服务端的开发人员。现在值得关注的有两个实现:越来越火的 component 和走在前沿的 es6 module transpiler。
  2. Modules/Async 流派。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范。这个观点下的典型代表是 AMD 规范及其实现 RequireJS。这个稍后再细说。
  3. 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)下线了。这个观点在本文中的典型代表就是 SeaJS 和 CMD 了

补一嘴:阿里 KISSY 的 KMD 其实跟 AMD 非常类似,只是用 adduse 两个源自于 YUI Modules 的函数名替换了 definerequire ,但其原理更接近 RequireJS ,与 YUI Modules 的 Y 沙箱 Attach 机制并不相同

RequireJS & AMD

AMD (Async Module Definition) 是 RequireJS 在推广过程中对模块定义的规范化产出。

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments

RequireJS 主要解决的还是 CommonJS 同步加载脚本不适合浏览器 这个问题:

As the comment indicates above, if require() is async, this code will not work. However, loading scripts synchronously in the browser kills performance. So, what to do?

所以我们需要 Function Wrapping 来获取依赖并且提前通过 script tag 提前加载进来

当依赖模块非常多时,这种依赖前置的写法会显得有点奇怪,所以 AMD 给了一个语法糖, simplified CommonJS wrapping,借鉴了 CommonJS 的 require 就近风格,也更方便对 CommonJS 模块的兼容:

1
2
3
4
5
6
define(function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
return function () {};
});

The AMD loader will parse out the require('') calls by using Function.prototype.toString(), then internally convert the above define call into this:

1
2
3
4
5
6
define(['require', 'dependency1', 'dependency2'], function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
return function () {};
});

出于Function.prototype.toString()兼容性和性能的考虑,最好的做法还是做一次 optimized build

AMD 和 CommonJS 的核心争议如下:

1. 执行时机

Modules/1.0:

1
var a = require("./a") // 执行到此时,a.js 才同步下载并执行

AMD: (使用 require 的语法糖时)

1
2
3
4
5
define(["require"],function(require)){
// 在这里,a.js 已经下载并且执行好了
// 使用 require() 并不是 AMD 的推荐写法
var a = require("./a") // 此处仅仅是取模块 a 的 exports
})

AMD 里提前下载 a.js 是出于对浏览器环境的考虑,只能采取异步下载,这个社区都认可(Sea.js 也是这么做的)

但是 AMD 的执行是 Early Executing,而 Modules/1.0 是第一次 require 时才执行。这个差异很多人不能接受,包括持 Modules/2.0 观点的人也不能接受。

2. 书写风格

AMD 推荐的风格并不使用require,而是通过参数传入,破坏了依赖就近

1
2
3
4
5
define(["a", "b", "c"],function(a, b, c){
// 提前申明了并初始化了所有模块
true || b.foo(); //即便根本没用到模块 b,但 b 还是提前执行了。
})

不过,在笔者看来,风格喜好因人而异,主要还是预执行懒执行的差异。

另外,require 2.0 也开始思考异步处理软依赖(区别于一定需要的硬依赖)的问题,提出了这样的方案:

1
2
3
4
5
6
// 函数体内:
if(status){
async(['a'],function(a){
a.doSomething()
})
}

SeaJS & CMD

CMD (Common Module Definition) 是 SeaJS 在推广过程中对模块定义的规范化产出,是 Modules/2.0 流派的支持者,因此 SeaJS 的模块写法尽可能与 Modules/1.x 规范保持一致。

不过目前国外的该流派都死得差不多了,RequireJS 目前成为浏览器端模块的事实标准,国内最有名气的就是玉伯的 Sea.js ,不过对国际的推广力度不够。

CMD 主要有 define, factory, require, export 这么几个东西

  • define define(id?, deps?, factory)
  • factory factory(require, exports, module)
  • require require(id)
  • exports Object

CMD 推荐的 Code Style 是使用 CommonJS 风格的 require

  • 这个 require 实际上是一个全局函数,用于加载模块,这里实际就是传入而已
1
2
3
4
5
6
7
8
9
10
11
12
13
define(function(require, exports) {
// 获取模块 a 的接口
var a = require('./a');
// 调用模块 a 的方法
a.doSomething();
// 对外提供 foo 属性
exports.foo = 'bar';
// 对外提供 doSomething 方法
exports.doSomething = function() {};
});

但是你也可以使用 AMD 风格,或者使用 return 来进行模块暴露

1
2
3
4
5
6
7
8
9
10
11
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码...
// 直接通过 return 暴露接口
return {
foo: 'bar',
doSomething: function() {}
};
});

Sea.js 借鉴了 RequireJS 的不少东西,比如将 FlyScript 中的 module.declare 改名为 define 等。Sea.js 更多地来自 Modules/2.0 的观点,但尽可能去掉了学院派的东西,加入了不少实战派的理念。

AMD vs CMD

虽然两者目前都兼容各种风格,但其底层原理并不相同,从其分别推荐的写法就可以看出两者背后原理的不同:

  1. 对于依赖的模块,AMD 是提前执行,CMD 是懒执行。(都是先加载)
  • CMD 推崇依赖就近,AMD 推崇依赖前置

看代码:

1
2
3
4
5
6
7
8
// AMD 默认推荐
define(['./a', './b'], function(a, b) { // 依赖前置,提前执行
a.doSomething()
b.doSomething()
})
1
2
3
4
5
6
7
8
9
10
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b') // 依赖就近,延迟执行
b.doSomething()
})

WebPack

后续文章,展开补充…