24 January 2014

原文:Authoring UMD modules

如果是在浏览器中运行代码,那么 AMD 模块 是个非常好的选择。如果运行在服务端环境,例如 RingoJSnode.js,那么 CommonJS 模块 是最简单的选择。

怎么才能写出可以同时运行在浏览器和服务端的代码呢?

也许你从没打算让代码运行在服务端环境中,但是在 node.js 中执行测试却是非常方便的。所以,为什么不同时支持两种环境呢?

问:怎么编写 JavaScript 模块,才能让它在多个环境中执行?

答:Universal Module Definition,UMD!

UMD 模式可以兼容多种环境。为了实现这一点,通常,但不总是,UMD 模式会把模块代码包裹进一个 立即调用的函数表达式(Immediately Invoked Function Expression,IIFE) 中。通过模拟或注入变量,IIFE 内部的环境最终被规格化为模块代码所期望的特定环境。IIFE 之外的代码则桥接了 IIFE 的内部环境和外部环境。规格化后的 IIFE 内部环境非常类似于 AMD 或 CommonJS,具体类似于哪种则取决于所采用的 UMD 风格。

UMD 有很多“野生”的变种。你可以在 UMD 仓库 以及 强大的 UMD 模式 找到一些示例。

许多模式还提供了一种方式使模块暴漏为全局变量(或全局变量上的属性)。不要这么干!应用代码不应该声明任何全局变量,因为全局变量会导致代码难以重用和难以测试。此外,模块化是 JavaScript 的趋势,为什么要和趋势对着干呢?

我们已经挑选了一些最喜欢的 UMD 模式,以帮助简化你的选择。下面章节中的通用模式可以运行在几乎所有的环境中:Netscape 7.2、IE6、Firefox 和 Chromium,以及服务端环境,例如 RingoJS 和 node.js。


如果你不知道我在说些什么,说明你尚未准备好,请先温习下教程 编写 AMD 模块编写 CommonJS 模块,然后再继续这篇教程。


重构为 AMD 经典格式

define(dependencyIds, factoryFunction(dependencyModule1, dependencyModule2 ...))

如果你正在换转带有依赖列表的 AMD 模块,那么只需要做很少量的重构。

// app/CachingStore
(function (define) {

    // dependencies are listed in the dependency array
    define(['./store', 'meld'], function (store, meld) {
        "use strict";
        var cache = {};

        // create the module
        meld.around(store, 'get', function (jp) {
            var key = jp.args.join('|');
            return key in cache ? cache[key] : cache[key] = jp.proceed();
        };

        // return your module's exports
        return store;
    });

}(
    typeof define == 'function' && define.amd
        ? define
        : function (ids, factory) {
            // note: the lambda function cannot be removed in some CJS environments
            var deps = ids.map(function (id) { return require(id); });
            module.exports = factory.apply(null, deps);
        }
));

整个模块被包裹在一个 IIFE 中,并且函数 define 被作为一个参数传入。文件最后的代码片段 typeof define == 'function' && define.amd 是嗅探 AMD 环境的标准方式。如果检测结果为 true,则说明当前环境是 AMD,可以把全局函数 define 传入 IIFE。通过由工厂函数返回一个值,模块以正常的 AMD 方式输出。

如果 AMD 环境嗅探的结果为 false,代码则模拟一个类似 node.js 的 CommonJS 环境。为了使 AMD 代码能够运行,IIFE 注入了一个行为类似于 AMD define 的函数:把所有的 ids 加载为模块,并把它们作为参数注入工厂函数。然后,函数 define 获取到工厂函数的返回值,并以经典的 node.js 方式赋值给 module.exports

为 AMD 工厂函数注入 require()

define(factoryFunction(require))

如果已经通过 AMD 的“require”指定了依赖关系,那么重构过程也不需要太大的变化。

// app/CachingStore
(function (define) {

    // using the define signature that triggers AMD-wrapped CommonJS
    define(function (require) {
        "use strict";
        var store, meld, cache = {};

        // use the injected require() to specify dependencies
        store = Object.create(require('./store'));
        meld = require('meld');

        // create the module
        meld.around(store, 'get', function (jp) {
            var key = jp.args.join('|');
            return key in cache ? cache[key] : cache[key] = jp.proceed();
        };

        // return your module's exports
        return store;
    });

}(
    typeof define == 'function' && define.amd
        ? define
        : function (factory) { module.exports = factory(require); }
));

整个组件再次被包裹在一个 IIFE 中,并且函数 define 作为参数被注入。这次,IIFE 底部的代码稍微简单一些,因为在这种情况下,AMD 的参数签名原本就与 CommonJS 很相似。事实上,它只是注入了 CommonJS 的 require,来替换掉 AMD 的 require。最后,module.exports 被赋予了工厂函数的返回值。

对于这种模式,cujo.js 使用的很频繁。

把 CommonJS 包裹为 AMD

define(factoryFunction(require, exports, module))

如果模块代码已经是 node.js 或 CommonJS 格式,可以用下面的包裹方式来格式化。

// app/CachingStore
(function (define) {

    // note: we're injecting all three CommonJS scoped variables
    define(function (require, exports, module) {
        "use strict";
        var store, meld, cache = {};

        // use CommonJS patterns to require() dependencies
        store = Object.create(require('./store'));
        meld = require('meld');

        // create the module
        meld.around(store, 'get', function (jp) {
            var key = jp.args.join('|');
            return key in cache ? cache[key] : cache[key] = jp.proceed();
        };

        // you can use node.js and/or pure CommonJS export patterns!
        exports.store = store;
    });

}(
    typeof define == 'function' && define.amd
        ? define
        : function (factory) { factory(require, exports, module); }
));

这一次,三个模块作用域变量(requireexportsmodule)都被注入。IIFE 中的环境模仿了 CommonJS 环境,适用于所有不访问特定环境变量(例如 node.js 的 __dirname)的模块。

需要注意的是,在这个变种中,工厂函数没有返回 exports。它期望你动态的将功能附加到对象 exports 到上,或者赋值给 module.exports



blog comments powered by Disqus