遇到的问题
来看示例:
// math.js
Math = {};
Math.add = function(n, m) { return n + m; };
// increment.js
function increment(val) { return Math.add(val, 1); }
// program.js
alert(increment(1));
假设 math.js 是数学静态方法库,increment.js 是具体业务代码,program.js 是执行入口。在 html 页面,最直接的引入方式:
<script src="math.js"></script>
<script src="increment.js"></script>
<script src="program.js"></script>
在真实场景下,上面的方式有以下问题:
- js 文件的下载是串行和阻塞的。
- 全局空间污染,暴露了 Math, increment 全局变量。在真实场景下,有可能会更多。
- js 文件的引入顺序不能调换,开发者必须知道文件之间的依赖关系。
- HTTP 链接数过多。
针对上面的问题,典型的解决方法有:
- 用 script injection 的方式并行异步下载文件,比如 LabJS, HeadJS, ControlJS 等 script loaders.
- 模拟 namespace 来减少全局污染,比如上面的示例代码可放入
X.Math, X.Biz
等命名空间。 - 采用一定的机制,将 script 的文件下载和模块代码的执行分开,自动管理依赖信息,比如 YUI3 的
YUI.add
和YUI.use
- 采用 Ant 等打包工具,或 cdn combo 服务,按需合并文件,减少 HTTP 链接数。
YUI3 的解决方案
我们重点分析 YUI3 的解决方案:
// math.js
YUI.add('math', function(Y) {
Y.Math = {};
Y.Math.add = function(n, m) { return n + m; };
});
// increment.js
YUI.add('increment', function(Y) {
var Math = Y.Math;
Y.increment = function (val) { return Math.add(val, 1); }
}, '1.0', { requires: ['math'] });
// program.js
YUI.add('program', function(Y) {
alert(Y.increment(1));
}, '1.0', { requires: ['increment'] });
页面中的引入方式如下:
<script src="yui-min.js"></script>
<script>
YUI({
modules: {
'math': {
fullpath: 'math.js'
},
'increment': {
fullpath: 'increment.js',
requires: ['math']
},
'program': {
fullpath: 'program.js',
requires: ['increment']
}
}
}).use('program');
</script>
online demo: math/yui3/test.html
YUI3 的解决方案已经很不错,但以下几点依旧有改进空间:
- use 多个 js 文件时,无论是否有依赖关系,下载都是串行的。注:可以通过配置 base 和 combine 来开启 combo 合并下载。对于内置模块来说,这样做效果不错。但对于非内置模块,需要有类 YAHOO! CDN combo 服务,不是很方便。另外,也并不是所有模块都需要 combo 起来下载。更推荐的做法是,根据访问频率和更新频率等信息,计算出缓存利用率,以此来决定最佳打包策略。
- 无论是内置模块还是外部模块,use 前都需要预先添加好配置信息。内部模块的巨大配置文件:yui3.js, 修改了模块的依赖关系时,还得维护该文件,不够方便。外部模块的配置信息,就是示例
YUI({modules:{...}})
中的代码,也比较恼人。
进一步考虑普适性和 DRY 原则,还可以分析出以下不足:
- 信息冗余。math.js 文件名已经表明了该文件是 math 模块,但 YUI.add 里,还得显式指明模块名称。示例代码中的依赖信息也有冗余。
- 种子过大。yui-min.js + loader-min.js, gzip 前 53.3k, gzip 后依旧有 18k. 对于 seed 来说,这个大小是不能用“小巧”来形容的,特别是在国内网速下。
- add 的功能是纯注册;use 的功能,不仅包括下载,还管理了模块 factory 的执行,这有悖职责单一原则,不能延迟执行。
- 所有模块都依附到 Y 实例上。有部分模块的模块名和 Y 上命名空间不对应,比如
YUI().use('dd-drop', function(Y) { /*Y.DD*/ })
, 这种不对应会加重使用者的记忆负担,不够直观。 - YUI3 的模块,仅能用于 YUI3, 普适性不是很好。
总之,在模块加载上,YUI3 很重,不光文件重,配置也重。在 CommonJS 时代,我们或许有更好的选择。
SeaJS 的尝试
SeaJS 的灵感来自 CommonJS 规范。Modules/2.0 规范目前还在讨论中:CommonJS Moules/2.0. SeaJS 遵循了规范里的大部分规定。其核心理念有:
- 职责单一原则:模块的声明、下载和执行是三个不同的步骤,在 API 的设计和实现上,应当尽量分离。
- 约定优于配置:模块所在路径和文件名,就是模块的标识,无需另行指定。这也是 DRY 原则的体现。
- 懒懒原则:能不执行的就先不执行,确实需要时才执行。
在 CommonJS Modules/2.0 里,我们这样写代码:
// math.js
module.declare([], function(require, exports) {
exports.add = function(n, m) {
return n + m;
};
});
// increment.js
module.declare(['math'], function(require, exports) {
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
}
});
// program.js
module.declare(['increment'], function(require) {
var inc = require('increment').increment;
alert(inc(1));
});
html 页面里的写法也很简单:
<script src="module.js"></script>
<script>
module.provide(['increment'], function(require) {
var inc = require('increment').increment;
alert(inc(1));
});
</script>
或者更简明的写成:
<script src="module.js" data-main="program"></script>
online demo: math/seajs/test.html
上面的示例,已经将 SeaJS Module Loader 的 API 都演示完了:
- 声明模块:
module.declare(id?, deps, factory)
- 提供模块:
module.provide(ids, callback)
- 获取模块:在 factory 里调用
require(id)
表面上看起来, 和 YUI3 很类似。但从设计上分析,存在质的区别:
- YUI3 的模块信息基于配置,SeaJS 则基于约定;
- YUI3 将模块都依附到 Y 上,依旧存在命名空间;SeaJS 的设计里,通过
var xx = require('xx-id')
来获取,彻底脱离了对命名空间的依赖; - exports 的设计,使得模块接口的暴露和模块之间的协作简单可靠;
- YUI3 的模块只能用于 YUI 类库,SeaJS 的模块可以用于 NodeJS, CouchDB 等环境下,更具有普适性。
从实现上看,SeaJS 还有以下优点:
- 代码非常小巧,目前 gzip 前 3.3k, gzip 后只有 1.7k.
- 非依赖项的模块文件是并行下载的;
- 模块代码,在第一次
require(id)
时才执行,这能节省初始 cpu 消耗,甚至整体消耗; - 还有 data-main 等设置,使得页面里,只需引入 seajs/module.js 即可;
- 还有循环依赖的处理、相对路径的支持等等,遵循了 CommonJS 规范,普适性和灵活性上都很好。
更多功能可以看测试用例:Test Suite for module.js
目前 SeaJS 还在开发中,对于 packages 的支持、模块版本、时间戳、子模块等等,依旧还存在设计和摸索中,如果你有兴趣,非常欢迎 fork 以下代码: