js模块化历程
无模块时代
这个时候并没有前端工程师,服务端工程师只需在页面上随便写写js就能搞定需求。那个时候的前端代码大概像这样:
if (xx) { //....... } else { //xxxxxxxxxxx } for (var i = 0; i < 10; i++) { //........ } element.onclick = function() { //....... }
模块萌芽时代
b.js依赖a.js,标签的书写顺序必须是
<script type="text/javascript" src="a.js"></script><script type="text/javascript" src="b.js"></script>
1. 用自执行函数来包装代码
modA = function() { var a, b; //变量a、b外部不可见 return { add: function(c) { a + b + c; }, format: function() { //...... } } }()
为了避免全局变量造成的冲突,人们想到或许可以用多级命名空间来进行管理,于是,代码就变成了这个风格:
app.util.modA = xxx; app.tools.modA = xxx; app.tools.modA.format = xxx;
Yahoo的YUI早期就是这么做的,调用的时候不得不这么写:
app.tools.modA.format();
3. jQuery风格的匿名自执行函数
(function(window){ //代码 window.jQuery = window.$ = jQuery;//通过给window添加属性而暴漏到全局 })(window);
模块化面临什么问题
源自nodejs的规范CommonJs
1. 模块的标识应遵循的规则(书写规范)2. 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API3. 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖4. 如果引入模块失败,那么require函数应该报一个异常5. 模块通过变量exports来向往暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。
遵循commonjs规范的代码看起来是这样的:(来自官方的例子)
//math.js exports.add = function() { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; }; //increment.js var add = require('math') .add; exports.increment = function(val) { return add(val, 1); }; //program.js var inc = require('increment').increment;var a = 1; inc(a); // 2
服务端向前端进军
1. 全局有一个module变量,用来定义模块2. 通过module.declare方法来定义一个模块3. module.declare方法只接收一个参数,那就是模块的factory,次factory可以是函数也可以是对象,如果是对象,那么模块输出就是此对象。4. 模块的factory函数传入三个参数:require,exports,module,用来引入其他依赖和导出本模块API5. 如果factory函数最后明确写有return数据(js函数中不写return默认返回undefined),那么return的内容即为模块的输出。
使用该规范的例子看起来像这样:
//可以使用exprots来对外暴漏APImodule.declare(function(require, exports, module) { exports.foo = "bar"; }); //也可以直接return来对外暴漏数据 module.declare(function(require) { return { foo: "bar" }; });
AMD/RequireJs的崛起与妥协
1. 用全局函数define来定义模块,用法为:define(id?, dependencies?, factory);2. id为模块标识,遵从CommonJS Module Identifiers规范3. dependencies为依赖的模块数组,在factory中需传入形参与之一一对应4. 如果dependencies的值中有"require"、"exports"或"module",则与commonjs中的实现保持一致5. 如果dependencies省略不写,则默认为["require", "exports", "module"],factory中也会默认传入require,exports,module6. 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx7. 如果factory为对象,则该对象即为模块的返回值
基于以上几点基本规范,我们便可以用这样的方式来进行模块化组织代码了:
//a.js define(function(){ console.log('a.js执行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(){ console.log('b.js执行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js require(['a', 'b'], function(a, b){ console.log('main.js执行'); a.hello(); $('#b').click(function(){ b.hello(); }); })
b.js执行
main.js执行
hello, a.js
另一点被吐槽的是,在定义模块的时候,要把所有依赖模块都罗列一遍,而且还要在factory中作为形参传进去,要写两遍很大一串模块名称,像这样:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
好的一点是,AMD保留了commonjs中的require、exprots、module这三个功能(上面提到的第4条)。你也可以不把依赖罗列在dependencies数组中。而是在代码中用require来引入,如下:
define(function(){ console.log('main2.js执行'); require(['a'], function(a){ a.hello(); }); $('#b').click(function(){ require(['b'], function(b){ b.hello(); }); }); });
a.js执行
hello, a.js
在AMD的阵营中,也有一部分人提出这样的观点,代码里写一堆回调实在是太恶心了,他们更喜欢这样来使用模块:
var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); });
作了此兼容后,使用requirejs就可以这么写代码了:
//d.js define(function(require, exports, module){ console.log('d.js执行'); return { helloA: function(){ var a = require('a'); a.hello(); }, run: function(){ $('#b').click(function(){ var b = require('b'); b.hello(); }); } } });
我们把上面的代码命名为d.js,在别的地方使用它:
require(['d'], function(d){ });
b.js执行
d.js执行
兼容并包的CMD/seajs
既然requirejs有上述种种不甚优雅的地方,所以必然会有新东西来完善它,这就是后起之秀seajs,seajs的作者是国内大牛淘宝前端布道者玉伯。seajs全面拥抱Modules/Wrappings规范,不用requirejs那样回调的方式来编写模块。而它也不是完全按照Modules/Wrappings规范,seajs并没有使用declare来定义模块,而是使用和requirejs一样的define,或许作者本人更喜欢这个名字吧。(然而这或多或少又会给人们造成理解上的混淆),用seajs定义模块的写法如下:
//a.js define(function(require, exports, module){ console.log('a.js执行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(require, exports, module){ console.log('b.js执行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js define(function(require, exports, module){ console.log('main.js执行'); var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); }); });
a.js执行
hello, a.js
hello, b.js
如果你一定要挑出一点不爽的话,那就是b.js的预先下载了。你可能不太想一开始就下载好所有的资源,希望像requirejs那样,等点击按钮的时候再开始下载b.js。本着兼容并包的思想,seajs也实现了这一功能,提供require.async API,在点击按钮的时候,只需这样写:
var b = require.async('b'); b.hello();
面向未来的ES6模块标准
定义一个模块不需要专门的工作,因为一个模块的作用就是对外提供API,所以只需用exoprt导出就可以了:
//方式一, a.js export var a = 1; export var obj = {name: 'abc', age: 20}; export function run(){....} //方式二, b.js var a = 1;var obj = {name: 'abc', age: 20};function run(){....} export {a, obj, run}
使用模块的时候用import关键字,如:
import {run as go} from 'a'run()
如果想要使用模块中的全部API,也可以不必把每个都列一遍,使用module关键字可以全部引入,用法:
module foo from 'a'console.log(foo.obj); a.run();