打造最出色的模块加载框架:SeaJS Module Loader v0.3.0 预览版

作者: nick 分类: js 发布时间: 2011-01-18 04:40 ė 61条评论

遇到的问题

来看示例:

// 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>

在真实场景下,上面的方式有以下问题:

  1. js 文件的下载是串行和阻塞的。
  2. 全局空间污染,暴露了 Math, increment 全局变量。在真实场景下,有可能会更多。
  3. js 文件的引入顺序不能调换,开发者必须知道文件之间的依赖关系。
  4. HTTP 链接数过多。

针对上面的问题,典型的解决方法有:

  1. 用 script injection 的方式并行异步下载文件,比如 LabJS, HeadJS, ControlJS 等 script loaders.
  2. 模拟 namespace 来减少全局污染,比如上面的示例代码可放入 X.Math, X.Biz 等命名空间。
  3. 采用一定的机制,将 script 的文件下载和模块代码的执行分开,自动管理依赖信息,比如 YUI3 的 YUI.addYUI.use
  4. 采用 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 的解决方案已经很不错,但以下几点依旧有改进空间:

  1. use 多个 js 文件时,无论是否有依赖关系,下载都是串行的。注:可以通过配置 base 和 combine 来开启 combo 合并下载。对于内置模块来说,这样做效果不错。但对于非内置模块,需要有类 YAHOO! CDN combo 服务,不是很方便。另外,也并不是所有模块都需要 combo 起来下载。更推荐的做法是,根据访问频率和更新频率等信息,计算出缓存利用率,以此来决定最佳打包策略。
  2. 无论是内置模块还是外部模块,use 前都需要预先添加好配置信息。内部模块的巨大配置文件:yui3.js, 修改了模块的依赖关系时,还得维护该文件,不够方便。外部模块的配置信息,就是示例 YUI({modules:{...}}) 中的代码,也比较恼人。

进一步考虑普适性和 DRY 原则,还可以分析出以下不足:

  1. 信息冗余。math.js 文件名已经表明了该文件是 math 模块,但 YUI.add 里,还得显式指明模块名称。示例代码中的依赖信息也有冗余。
  2. 种子过大。yui-min.js + loader-min.js, gzip 前 53.3k, gzip 后依旧有 18k. 对于 seed 来说,这个大小是不能用“小巧”来形容的,特别是在国内网速下。
  3. add 的功能是纯注册;use 的功能,不仅包括下载,还管理了模块 factory 的执行,这有悖职责单一原则,不能延迟执行。
  4. 所有模块都依附到 Y 实例上。有部分模块的模块名和 Y 上命名空间不对应,比如 YUI().use('dd-drop', function(Y) { /*Y.DD*/ }), 这种不对应会加重使用者的记忆负担,不够直观。
  5. YUI3 的模块,仅能用于 YUI3, 普适性不是很好。

总之,在模块加载上,YUI3 很重,不光文件重,配置也重。在 CommonJS 时代,我们或许有更好的选择。

SeaJS 的尝试

SeaJS 的灵感来自 CommonJS 规范。Modules/2.0 规范目前还在讨论中:CommonJS Moules/2.0. SeaJS 遵循了规范里的大部分规定。其核心理念有:

  1. 职责单一原则:模块的声明、下载和执行是三个不同的步骤,在 API 的设计和实现上,应当尽量分离。
  2. 约定优于配置:模块所在路径和文件名,就是模块的标识,无需另行指定。这也是 DRY 原则的体现。
  3. 懒懒原则:能不执行的就先不执行,确实需要时才执行。

在 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 都演示完了:

  1. 声明模块:module.declare(id?, deps, factory)
  2. 提供模块:module.provide(ids, callback)
  3. 获取模块:在 factory 里调用 require(id)

表面上看起来, 和 YUI3 很类似。但从设计上分析,存在质的区别:

  1. YUI3 的模块信息基于配置,SeaJS 则基于约定;
  2. YUI3 将模块都依附到 Y 上,依旧存在命名空间;SeaJS 的设计里,通过 var xx = require('xx-id') 来获取,彻底脱离了对命名空间的依赖;
  3. exports 的设计,使得模块接口的暴露和模块之间的协作简单可靠;
  4. YUI3 的模块只能用于 YUI 类库,SeaJS 的模块可以用于 NodeJS, CouchDB 等环境下,更具有普适性。

从实现上看,SeaJS 还有以下优点:

  1. 代码非常小巧,目前 gzip 前 3.3k, gzip 后只有 1.7k.
  2. 非依赖项的模块文件是并行下载的;
  3. 模块代码,在第一次 require(id) 时才执行,这能节省初始 cpu 消耗,甚至整体消耗;
  4. 还有 data-main 等设置,使得页面里,只需引入 seajs/module.js 即可;
  5. 还有循环依赖的处理、相对路径的支持等等,遵循了 CommonJS 规范,普适性和灵活性上都很好。

更多功能可以看测试用例:Test Suite for module.js

目前 SeaJS 还在开发中,对于 packages 的支持、模块版本、时间戳、子模块等等,依旧还存在设计和摸索中,如果你有兴趣,非常欢迎 fork 以下代码:

seajs on github

本文出自 传播、沟通、分享,转载时请注明出处及相应链接。

本文永久链接: https://www.nickdd.cn/?p=1358

发表评论

您的电子邮箱地址不会被公开。

Ɣ回顶部