Modern Modular JavaScript

顾轶灵

模块化

  • 封装(解决冲突)
  • 依赖(模块间的相互引用)
  • 加载(运行环境影响加载方式)

math.js

function add(x, y) {
    return x + y;
}

main.js

var a = add(1, 2);
alert(a);

index.html

<script src="math.js"></script>
<script src="main.js"></script>

math.js

var math = {};
math.add = function(x, y) {
    return x + y;
}

main.js

var a = math.add(1, 2);
alert(a);

记忆/输入代价 vs. 冲突概率

math.js

(function() {
    var math = {};
    math.add = function(x, y) {
        return x + y;
    };
    this.math = math;
})();

main.js

(function() {
    var a = math.add(1, 2);
    alert(a);
})();

动态加载

  • XHR + eval
    跨域、调试
  • document.write()
    阻塞页面渲染、页面加载完毕后无法再用
  • head.appendChild(script)

模块加载顺序?

依赖

  • 模块通过显示声明(eg. goog.provide('mod');或不声明(eg. 与文件路径对应)
  • 某种 include/import/require 声明依赖
    eg. goog.require('mod');

问题

模块不执行,如何获取依赖决定执行顺序?

  • 服务端静态分析后顺序加载执行
    eg. deps.js for Closure
  • 将依赖声明和模块执行分开(利用 function wrapping)

    define(['math'], function(math) {
        var a = math.add(1, 2);
        alert(a);
    });

CommonJS Modules

Module Context

  • require
  • exports
  • module

(详细定义 →)

math.js

exports.add = function(x, y) {
    return x + y;
}

main.js

var add = require('math').add;
var a = add(1, 2);

Server-oriented

  • 模块运行环境天然隔离
  • 同步读取执行模块依赖
  • 无法直接在浏览器环境下使用
    可以通过服务端自动构建为遵循 Wrappings 或 Transport 规范的代码来满足浏览器运行条件

RequireJS 与 AMD

RequireJS (repo →)

  • 前身为 RunJS
  • 试图解决老版本 Dojo Loader(XHR + eval)的缺陷
  • 加载执行遵循 AMD 规范的模块

AMD

  • “CommonJS Transport/C”
  • RequireJS 的规范化产出
  • define(id?, dependencies?, factory);

(详细定义 →)

module id

  • "/" 分隔的 "terms"
  • 不得包含后缀如 ".js"
  • "relative" & "top-level"
  • 使用加载器插件时,"!" 用来分隔资源类型 ID 与模块名
baidu/dom./langcss!./normalize.css

dependencies

  • 缺省值为 ["require", "exports", "module"]
  • 依赖模块的导出接口将会对应到 factory 的参数列表

factory

  • 函数/对象
  • 若为函数,仅运行一次,且返回值作为模块的导出接口
  • 若为对象,直接作为模块的导出接口

require (local)

传入 AMD define factory 的 require 函数

define(['require'], function(require) {
    //the require in here is a local require.
});

define(function(require, exports, module) {
    //the require in here is a local require.
});

require (global)

用来启动模块加载的全局函数

require(['foo', 'bar'], function (foo, bar) {
    foo.doSomething();
});

require

require(String)
同步返回指定模块接口

define(function(require) {
    var a = require('a');
});
Function.prototype.toString()

require

require(Array, Function)
异步读取多个依赖模块后执行回调

define(function(require) {
    require(['a', 'b'], function(a, b) {
        //modules a and b are now available for use.
    });
});

math.js

define(function() {
    var math = {};
    math.add = function(x, y) {
        return x + y;
    };
    return math;
});

main.js

require(['math'], function(math) {
    var a = math.add(1, 2);
    alert(a);
});

Simplified CommonJS wrapping

math.js

define(function(require, exports) {
    exports.add = function(x, y) {
        return x + y;
    }
});

main.js

define(function(require) {
    var add = require('math').add;
    var a = add(1, 2);
});

实现

AMD 特点

  • 可以在浏览器中运行
  • 封装性好
  • 同时支持 exports/返回值,相比 CommonJS 模块,导出接口可以是函数等更丰富的类型 eg. 模块直接导出一个构造函数
  • 不支持动态计算同步依赖
    var mod = require(someCondition ? 'a' : 'b');
    
    if (someCondition) {
        var a = require('a');
    } else {
        var a = require('b');
    }

AMD 特点(cont.)

  • 便于调试 相对于 XHR + eval
  • 加载器插件丰富灵活

SeaJS 与 CMD

SeaJS

  • 在浏览器上运行 CommonJS 模块
  • 模块的快速迁移
  • 加载遵循 CMD 规范的模块
  • SPM

CMD

  • SeaJS 的规范化产出
  • define(factory);
  • define(id?, deps?, factory);
    非 CMD 一部分,而作为 Transport 规范存在。推荐不写 iddeps,在构建时用工具生成

(详细定义 →)

factory

  • 与 AMD 类似,不同之处在于可以是字符串
    define('I am a template. My name is {{name}}.');
  • 还可以通过 module.exports 进行导出(兼容 node.js 模块)
    define(function(require, exports, module) {
        module.exports = new SomeClass();
    });

require

  • 只有 local require
  • require(String) 同步加载
  • require.async(id, callback?) 异步加载
    类似于 AMD 的 require(Array, Function)
  • SeaJS 下的入口为 seajs.use()

SeaJS vs. RequreJS, CMD vs. AMD

@lifesinger

  • 依赖延迟执行 vs. 提前执行
  • 依赖就近 vs. 前置
  • CMD 更为贴近 CommonJS Modules/1.1 和 Node Modules
  • 明显没有 bug vs. 没有明显的 bug
  • 调试、插件机制、...

参考文档

  1. amdjs-api wiki
  2. Modules/1.1.1
  3. AMD is better for the web than CommonJS modules
  4. AMD is Not the Answer
  5. Reply to Tom on AMD
  6. Why Web Modules?
  7. Why AMD?
  8. RequireJS History

参考文档(cont.)

  1. CMD 模块定义规范
  2. 从 CommonJS 到 SeaJS
  3. 从 RequireJS 到 SeaJS (1), (2), (3), (4), (5)
  4. 模块加载器获取URL的原理
  5. AMD 和 CMD 的区别有哪些?
  6. 使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript

Q&A

- EOF -