下面是老生常谈。JavaScript 语言的创造者,Brendan Eich,花了几周不到的时间就把 js 给搞出来了。
最初的目标是让网页开发者能够写点小脚本,让网页互动性好一点。因此,在大规模应用的时候,这个语言被发现有好多不适合工程化的缺陷。
老道(Douglas Crockford)在他的书《JavaScript 语言精粹》中,总结了其中的大部分。
而缺少模块系统,正是它的缺陷之一。可能你已经听说过或者尝试过 Node.js,有模块化支持的 js 语言,就该是那样的。
不过,在浏览器的 js 世界里头,有两个很有趣的事实:
你可以在 DOM 中插入 script
标签,让浏览器动态加载 @src
所指定的 js;
你可以监听那个 script
标签的 onload
、onerror
或者 oncomplete
事件。
呃,浏览器对第二点的支持上不尽相同,不过 SeaJS 已经搞定这个问题啦!……几乎是搞定啦!
基于这两个事实,我们可以这么搞:
加载基础 js;
根据业务需求不同,加载不同的 js 模块;
模块加载完毕,执行相应的业务逻辑。
那个基础 js,正是 SeaJS 了。
我们先加载:
<!-- the library and your app -->
<script src= "sea.js" ></script>
<script src= "app.js" ></script>
你还可以使用快捷方式,通过给 sea.js
的 script
标签加 @data-main
属性,来指定页面的初始脚本。
<!-- more compact way -->
<script src= "sea.js" data-main= "./app" ></script>
./
是指相对于当前页面路径。假定页面的 URL 是 http://foo.com/hello/world.html
,
那么 SeaJS 会去加载 http://foo.com/hello/app.js
.
./app
在 SeaJS 中被称作模块 ID。相对路径之外,你还可以用绝对路径。
呃,没那么绝对。在此例中,如果你用 hello/app
来指定模块,SeaJS 将会使用
base
路径 + 模块 ID + .js
这一规则来拼模块的实际 js 地址。
等等,base
路径是神马?
如果模块 ID 不以 .
起始,SeaJS 使用 base
作为基本路径来拼模块的实际地址。
可以通过以下方式来配置 base
。
seajs . config ({
base : '/'
});
如果没有配置,则默认为当前页面的上级目录,即 http://foo.com/hello/
。
配置好了 base
,hello/app
就可以被解析为 http://foo.com/hello/app.js
啦。
在模块中 require
其他模块的时候,规则也是相同的。相对路径将会相对与当前模块的 uri 来解析。
// http://foo.com/worker/carpenter.js
define ( function ( require , exports ) {
var hammer = require ( '../util/hammer' );
exports . nail = function () {
hammer . smash ();
};
});
当前模块的 uri 是 http://foo.com/worker/carpenter.js
,于是 ../util/hammer
就被解析为
http://foo.com/util/hammer.js
。
与 Node.js 的模块机制唯一的不同,就是你的模块代码需要用 define
回调包起来:
define ( id , dependencies , function ( require , exports , module ) {
// module code.
});
id
与 dependencies
参数是可以省略的。
id
用来显式指定模块 ID。当你的项目上线,所有的模块都合并到了一个文件中,如果不显示指定,
SeaJS 就无从知道哪个模块是哪个了。在开发的时候,一般用不到它。
dependencies
也是如此。它列出了当前模块所依赖的模块,在开发的时候是不需要写明的。
SeaJS 会检查你的模块回调函数,找到所有的 require
语句,从而得到你的模块的所有依赖。
在真正 require
当前模块时,会先去请求这个模块的依赖,加载完毕,再去初始化当前的模块。
而到了线上,代码压缩、合并之后,则会提供此参数,以省却模块解析的时间,提高效率。
模块依赖解析,靠的是三个重要的规则:
不能重命名 require
不能覆盖 require
require
的参数必须是字符串字面量,不可以 require(foo())
或者 require(bar)
,
也不可以是 require(should_be_a ? 'a' : 'b')
。
前两点,把 requrie
当做 js 语言中的一个关键字,就容易理解了。我们不会这么做:
// 错误!
var func = function ;
var function = 'aloha' ;
// 真的是错误!
第三点,则受限于 js 自身。如果我们需要用这样的功能:
// 是迈克尔·乔丹,还是迈克尔·杰克逊?
var name = prefer_singer () ? 'jackson' : 'jordan' ;
var celebrity = require ( name );
// 我们要他们的签名!
celebrity . signature ();
require
是满足不了需求的,可以从两个方面来理解:
如果动态去请求模块,那么 celebrity.signature()
这一步必须在模块请求完毕之后执行,然而动态请求天生就是异步的,没法直接串行执行;
SeaJS 的动态解析模块依赖并预加载依赖的机制在这里也行不通,因为 SeaJS 在解析 define
的回调,
即模块函数体的时候,无从知晓 name
的值,这也是为何 require
的参数必须是字符串字面量。
但如果你真要这么用(因为这么用很爽),也是有办法的。
其一是把依赖的模块都在 define
头部手工声明,不再仰仗 SeaJS 的自动解析功能:
define ([ 'jordan' , 'jackson' ], function ( require , exports ) {
var celebrity = require ( prefer_singer () ? 'jackson' : 'jordan' );
console . log ( celebrity . height ());
});
而另一种方法,则是使用 require.async
:
define ( function ( require , exports ) {
require . async ( prefer_singer () ? 'jackson' : 'jordan' , function ( celebrity ) {
// 偷窥别人的年收入是不对的!
console . log ( celebrity . anual_income ());
});
});
具体使用哪一种,则完全视项目需求而定了。这两种的区别有两个:
require.async
方式加载的模块,不能打包工具找到,自然也不能被打包进上线的 js 中;而前一种方式可以。
如果需要在 require
模块之后串行执行代码,仰仗那个模块的返回值,require.async
就做不到了;而前一种可以。
那就专门用前一种?也未尽然,如果需要执行时动态加载的模块很大(比如大量 json 数据),则使用 require.async
才是好选择。
如果只是为了能够在执行时通过反射模式取得模块,并且这些模块都可能被反射到,则不如直接手工写入依赖,即前一种使用方式。