岁月如歌

用开放的心态,打造专业的人生。

模块加载器使用场景、设计与实现的进一步思考

with 3 comments

不知道上下文的,请先阅读这篇博客:模块加载器获取 URL 的原理

使用场景

为了便于讨论,先做一些约定:

模块原始代码为 S(ource)
压缩打包后的模块代码为 C(ompressed)
独立文件为 F(ile)
内嵌到页面中为 P(page)
独立文件中是原始代码时,记为 SF;独立文件中是压缩打包后的代码时,记为 CF
内嵌的原始代码记为 SP;内嵌的压缩打包后的代码记为 CP

压缩打包是指代码压缩、模块信息补全、编译、合并等操作。

使用场景可概况为一句话:

SF, CF, SP 和 CP 可直接在浏览器中运行,并可同时存在。

几点说明:

  1. 可直接在浏览器中运行,这点很重要。对前端开发来说,浏览器即编译器。增加一层服务端来做编译、转换等工作,是很大的诱惑,但这会增加使用者的复杂度。好想法要变成好产品,在给用户带去新功能、去除用户坏习惯的同时,还得尽可能避免剥夺用户现有的便利。
  2. 允许内嵌,这是脚本代码的现有特性:模块代码与存储位置无关。可存储为文件,也可内嵌到页面。保持该特性,除了能延续旧有的便利,还可使得通过内嵌来减少 HTTP 链接数的性能优化方案继续可用。
  3. 源码和压缩打包后的代码可并存。这一点的背后,是模块代码与存储形式无关。做到这一点,可以让代码调试和新功能的开发等工作更简单。

第一点决定模块加载器的战场在浏览器端,要尽量避免增加对服务端的依赖。第二点决定不能采用 XHR + eval 等方式来实现,而需要采用包裹形式来定义模块。第三点决定模块转换格式要尽可能和模块定义格式兼容,最好的一种兼容方式是:模块转换格式就是模块定义格式。这种一致性还可以给优化打包等工具带来方便和简洁性。

模块定义格式

模块的定义格式为:

define(id?, dependencies?, factory)

 
Read the rest of this entry »

Advertisements

Written by lifesinger

November 15, 2011 at 10:13

Posted in Articles

模块加载器获取URL的原理

with 4 comments

浏览器端的模块管理

JavaScript 构建的应用越来越复杂,为了提高代码的可维护性,第一步是拆分为多个文件:

a.js
b.js
c.js
...

 
文件拆开是第一步,为了彼此能互相调用,但又不污染全局造成潜在冲突,于是聪明的程序员们想出了用对象来模拟命名空间:

// a.js:
X = {};
X.a = {...};

// b.js:
X.b = {...};

// c.js:
X.c = {...};
...

 
这种命名空间得自己维护,当层级达到三层及其以上时,维护起来并不轻松,比如 YAHOO.widget.TreeView。于是出现了类似 YUI3 的扁平方式:

// 定义模块
YUI.add('a', factory);

// 加载模块
YUI().use('a', function(Y) {
  // 调用模块
  Y.a.doSomething();
});

 
这种方式解决了不少问题,比如依赖加载、模块沙箱、命名空间。但使用时,用户的记忆负担还是比较重,比如:

YUI().use('dd-drop', function(Y) {
  Y.DD...
});

 
得记住 dd-drop 和 Y.DD 的对应关系。当模块很多时,经常要查文档。
Read the rest of this entry »

Written by lifesinger

November 11, 2011 at 22:05

Posted in Articles

A Module Loader for the Web

with 3 comments

SeaJS is a module loader focusing on the web. Different from RequireJS, the module authoring format of SeaJS is more similar to CommonJS. With SeaJS, require, exports, module can be used just as you do in the environment of NodeJS:

define(function(require, exports, module) {
  var foo = require('./foo');
  ...
  exports.bar = foo.doSomething();
});

 

For more details, please go to http://seajs.com/

I’ll be highly appreciated if you would review the code or give some feedback to me.

Best regards,
Frank Wang

Written by lifesinger

November 2, 2011 at 22:49

Posted in Articles

SeaJS 1.0.2 final 以及下一步计划

leave a comment »

10 月 20 日发布 v1.0.2 后,不少朋友在试用过程中发现了一些 bug, 并建议增加 text 等默认插件。经过一周多时间的完善,已经在三个正式项目中顺利升级。

推荐使用 seajs 的项目,毫不犹豫地升级。在之前下载过 v1.0.2 的朋友,请重新下载今天构建的 final 版本。

官方网站:http://seajs.com/

下一步计划

接下来的工作,除了 bug fix, 主要为:

  1. 文档完善,推出《SeaJS 从入门到精通》系列。
  2. SPM 0.4 的开发。
  3. modules 的完善。

SeaJS 本身将进入 v1.1.0 的需求收集阶段。
欢迎有想法的同行们提供建议,回复博文或提交到这里:issues

Written by lifesinger

November 2, 2011 at 20:41

Posted in Articles

从 RequireJS 到 SeaJS(5)

with one comment

这是该系列的最后一篇。

作为模块加载器,最核心的是:模块书写格式。从表层看,RequireJS 和 SeaJS 最大的差异就是各自默认推荐的模块书写格式不同。

由于 RequireJS 支持 Simplified CommonJS Wrapper, 一定程度上可以说 SeaJS 是 RequireJS 的一个子集。甚至可以认为:SeaJS 是 RequireJS 中的 The Good Parts.(RequireJS 作者肯定不会同意,不过从社区中可以看出很多人很喜欢 Simplified CommonJS Wrapper 格式,这与 SeaJS 遵循的 Wrappings 基本一致。)

功能差异

从功能上讲,RequireJS 很丰富。下面这些功能是 RequireJS 有,而 SeaJS 没有的:

  1. domReady 插件
  2. i18n 插件
  3. order 插件
  4. 通过 r.js 支持 packages
  5. 支持 Web Worker
  6. 支持 Rhino

这些功能,SeaJS 现在没有,以后也不打算提供。因为 SeaJS 的定位是:浏览器端的模块加载器

SeaJS 也有一些非常好用的功能,RequireJS 却没有的:

  1. 映射插件,可用来做 在线本地调试
  2. 通过 map 配置,对版本和时间戳提供更方便的管理
  3. 通过 preload 配置,可以预加载模块
  4. 更开放的插件机制,并默认提供 plugin-json 和 plugin-less
  5. 支持 css 文件的加载
  6. 由于延迟执行机制,可以很便捷地探测 404 等错误请求

场景差异

在使用场景上,两者之间的差异不小。

RequireJS 的例子一直是单项目,类库和业务脚本都放在一起:

project
-- scripts
   -- require-jquery.js
   -- main.js
   -- sub
      -- some.js
   ...
-- project.html

 
这种使用场景下,RequireJS 和 SeaJS 半斤八两,彼此彼此。

但上面的组织方式不是 SeaJS 推荐的。SeaJS 推荐通用类库独立存放:

libs
--seajs/1.0.2/sea.js
--jquery/1.6.4/jquery.js
--underscore/1.2.1/underscore.js
...

project-assets
-- main.js
-- sub
   -- some.js
...

project
-- project.html

 
这样,不同项目之间,可共用 libs. libs 的版本维护也很方便。比如:在 requirejs 里,如果用到 jquery, 当 jquery 更新时,得重新打包 require-jquery.js. 在 SeaJS 里则不需要更新 sea.js, 只需要更新配置中的 alias.

SeaJS 很适合大型网站,伸缩性很好,能适应的场景很广。这受益于 SeaJS 的广义定位:浏览器端的 NodeJS.

写在最后

RequireJS 我不是很喜欢,主要是因为其定位不纯粹,以及 API 在我看来不够优雅。但 RequireJS 的作者依旧是很让人敬佩的一个牛人。从在 CommonJS 积极讨论,到非常有激情地开发 RequireJS,以及后续坚持不懈布道,这些工作,看起来不难,实际上非常不易,很佩服这份激情、执着和毅力

作为 SeaJS 的作者,我尽可能公平公正评价 RequireJS. 如果上面的文字有失偏颇,还希望你热心指正,欢迎所有真心诚恳的交流讨论。

(完)

Written by lifesinger

October 30, 2011 at 23:03

Posted in Articles

从 RequireJS 到 SeaJS(4)

with 2 comments

理解了 ID 规则,再来看 RequireJS API 文档 就轻松多了。

模块书写格式

作为模块加载器,需要明确模块应该怎样写,这就是模块书写格式(Module Authoring Format)
对于文件加载器来说,约定非常少,比如 LABjs 只约定文件里不能有 document.write 等语句。

RequireJS 遵守的是 AMD 规范,SeaJS 遵守的是 Simple Wrappings 规范。

从表面上看,AMD 规范和 Wrappings 规范最大的不同是 factory 函数的参数不一样:

// 两者的基本格式都是:
define(id?, denpendencies?, factory);

// 在 AMD 中,factory 的参数由 dependencies 指定:
define(['a'], function(a) {
});

// 在 Wrappings 中,factory 的参数始终是 require, exports, module 三个:
define(function(require, exports, module) {
  var a = require('a');
});

 

factory 的参数差异,直接导致 AMD 中的模块是立刻执行的,而 Wrappings 中的模块可以等到第一次 require 时才执行。这是当初 CommonJS 社区讨论最为激烈的争执点,彼此形成了不同的派系。理念上的差异,加上其他一些因素,最后直接导致 RequireJS 从 CommonJS 中脱离,自立门户成为了独立的社区。

理念上无对错。对于 SeaJS 来说,选择的是延迟执行,尽量与 CommonJS 以及 NodeJS 的模型保持一致。有兴趣的可以阅读我之前的博文:SeaJS 和 RequireJS 的异同

补充:每次想起这个话题,当初 CommonJS 社区的激烈讨论就历历在目。也正是因为理念上的差异,让我有了实现 SeaJS 的想法。当初 RequireJS 作者非常强势,虽然后来也支持了 Simplified CommonJS Wrapper, 但 CommonJS 原社区的不少人都不太认可 RequireJS 遵循的规范,并开始推出 BravoJS, FlyScript 等 loader, 可惜的是这些 loader 更新非常缓慢,目前 SeaJS 还算活跃的。

加载启动

对于安装了操作系统的计算机,最常用的启动方法是摁一下开机键。
对于浏览器,加载页面的普适方式是在地址栏上输入 url 并回车。

对于 NodeJS, 是在命令行中输入:

$ node xxx.js

 

所有这些都是“启动”。在 RequireJS 和 SeaJS 中,最便捷的启动是:

<script src="loader.js" data-main="main"></script>

 

RequireJS 可以通过全局 require 方法来启动:

require(['path/to/main.js']);

 
注意:作为启动用的 require, 参数必须是数组,即便只加载一个文件。这是 require 的陷阱之一,是由其设计导致的(在 RequireJS 里,require 还承担了获取模块接口的功能),这种不纯粹的设计是 SeaJS 不认可的。

SeaJS 里,普适的启动方式是:

seajs.use('path/to/main');

 

可以认为 seajs.use('xx') 就是 $ node xx. 启动是和具体加载器相关的,启动之后,模块代码里就不必再出现加载器相关的东西了。比如 node 的模块代码里不会再出现 node. seajs 里,也不推荐在模块代码里出现 seajs (除了初始模块里可以用 seajs.config 进行配置)。

优化工具

RequireJS 的优化工具是 r.js
SeaJS 的是打包部署工具 spm

两者设计理念差异很大,在此就不比较了。

Written by lifesinger

October 30, 2011 at 18:31

Posted in Articles

从 RequireJS 到 SeaJS(3)

with 2 comments

来看 RequireJS 的 API 页面:api.html, 该篇博客着重分析 RequireJS 和 SeaJS 在 ID 规则上的异同。

RequireJS 的 ID 规则

对于模块加载器来说,模块 id 遵守的规则非常重要。

首先来定义几个概念。假设当前页面路径为 http://t.com/a/b/c.html, 则:

  1. pageRoot = http://t.com
  2. pageUrl = http://t.com/a/b
  3. baseUrl: 在 RequireJS 里,默认情况下,baseUrl 就是 pageUrl, 除非:
    • 通过 require.config 更改了 baseUrl
    • 指定了 data-main, 比如
      <script src="require.js" data-main="scripts/main"></script> 
      

      这时 baseUrl 为 pageUrl/scripts

  4. paths: 表示路径的缩写,通过 config 来配置:
    require.config({ paths: { 'some': 'xxx/zzz' } })
    

    这时,some/a 实际上代表 xxx/zzz/a

  5. moduleUrl: 如果模块路径为 http://t.com/path/to/a.js, 则 moduleUrl = http://t.com/path/to

RequireJS 的 id 规则为:首先,会通过 paths 解析,解析完成后:

  1. Page ID
    • .js 结尾的,比如 some/a/b.js
    • / 开头的,比如 /a/b
    • http(s):// 开头的

    Page ID 的解析规则与 script src="xxx" 中的 xxx 一样。

  2. some/module 这种 Top-Level ID, 会解析成 baseUrl/some/module.js
  3. ./some/module 这种 Relative ID, 会解析成 moduleUrl/some/module.js

SeaJS 的 ID 规则

文档:module-identifier.html

SeaJS 的 ID 规则可以总结为:

  1. 省略约定.js 后缀可以省略。 require('path/to/a.js')require('path/to/a') 是一样的。
  2. 环境相关: 除了 Top-Level ID 始终相对 baseUrl 来定位,其他 ID 都是相对当前环境来定位。

举例如下:

// 在 http://t.com/test.html 中:
seajs.use(['./a', 'b', 'c.js', '/d', 'http://x.com/e']);

// 会下载
// http://t.com/a.js
// baseUrl/b.js
// baseUrl/c.js
// http://t.com/d.js
// http://x.com/e.js

// 在 http://cdn.com/path/to/t.js 中:
define(function(require) {
  require('./a');
  require('b');
  require('c.js');
  require('/d');
  require('http://x.com/e');
});

// 会下载
// http://cdn.com/path/to/a.js
// baseUrl/b.js
// baseUrl/c.js
// http://cdn.com/d.js
// http://x.com/e.js

 

可以看出,在 SeaJS 里,ID 可以分成两类:

  1. Top-Level ID: some/module 这种,会根据 baseUrl 来定位。
  2. Context ID: 除了 some/module 之外的所有形式,会根据当前环境来定位。

ID 规则对使用体验的影响

RequireJS 里有 Page ID 的概念,和浏览器解析 src 的规则保持一致。
SeaJS 里则有省缺 .js 后缀的约定,和 CommonJS 以及 NodeJS 的约定保持一致。

这个取舍,使得 RequireJS 的 ID 更接近文件路径,SeaJS 的 ID 则更接近模块标识。
这其实是由两者的定位决定的:RequireJS 想同时是文件和模块加载器,SeaJS 则只专注于模块加载器。

当采用 Simplified CommonJS Wrapper 格式时,RequireJS 和 SeaJS 的解析规则基本一致:

// xxx/to/a.js
define(function(require, exports, module) {
  require('a'); // baseUrl/a.js
  require('./b'); // xxx/to/b.js
  require('./b.js'); // xxx/to/b.js
});

RequireJS 的 ID 规则是比较容易让人混淆的,特别对于新手来说。有没有 .js 后缀,以及采用的模块书写格式,都会影响解析规则。

SeaJS 的 ID 规则更简单纯粹,和 CommonJS 以及 NodeJS 尽量保持一致。

Written by lifesinger

October 30, 2011 at 16:11

Posted in Articles