CommonJS 规范定义了如何编写 JavaScript 模块,以及模块间如何相互依赖。 支持模块化的 JavaScript 开发过程,浏览器端已有 RequireJS 和 Sea.JS 等具体实现。 最近 Brick.JS 也实现了 CommonJS 规范来实现客户端 JavaScript 的模块化。 该实现与 Node.js 风格兼容,这意味着在不考虑内置 package 的情况下, 实现了客户端与服务器共用代码。

Brick.JS 是一个基于 Node.js 的 HMVC 风格的 Web 应用开发框架,意图最大化代码复用和项目特性的伸缩性。

本文简要介绍 Brick.JS 中 CommonJS 的实现过程。 CommonJS 实现起来并不困难,主要精力需要用在错误处理、网络和鲁棒性上。 在此之前先来看一个 CommonJS 的例子:

// file: foo/client.js
exports.author = 'harttle.land';
exports.log = console.log.bind(console);

// file: bar/client.js
var foo = require('foo');
foo.log(foo.author);        // harttle.land

这两个文件的依赖方式符合 CommonJS 规范,既可以在 Node.js 下运行, 现在 Brick.JS 要让它们在浏览器端运行!请看下文。

客户端 JS 模块化

Brick.JS 会对客户端脚本进行模块化并注册为 CommonJS 模块,上述两个文件在 Brick.JS 模块化后会生成这样的代码:

CommonJS.register('foo', function(require, exports, module){
    exports.author = 'harttle.land';
    exports.log = console.log.bind(console);
});
CommonJS.register('bar', function(require, exports, module){
    var foo = require('foo');
    foo.log(foo.author);
});

在 HTML 页面载入时,Brick.JS 会根据当前页面组件自动加载对应的入口模块。 比如当前存在一个页面组件 .brk-bar,上述 bar 模块就会被执行,最终输出:

harttle.land

详情见:https://github.com/brick-js/brick.js/wiki/JS-Modularization

CommonJS 对象

为了实现上述的模块注册(register)、依赖(require)和执行(exec), Brick.JS 实现了简易的 CommonJS 对象。

var CommonJS = {
    require: function(id) {
        var mod = CommonJS.moduleCache[id];
        if (!mod) throw ('required module not found: ' + id);
        return (mod.loaded || CommonJS.pending[id]) ? 
            mod : CommonJS.exec(mod);
    },
    register: function(id, define) {
        if (typeof define !== 'function')
            throw ('Invalid CommonJS module: ' + define);
        var mod = factory(id);
        CommonJS.defines[id] = define;
    },
    exec: function(module) {
        CommonJS.pending[module.id] = true;

        var define = CommonJS.defines[module.id];
        define(module.require.bind(module), module.exports, module);
        module.loaded = true;

        CommonJS.pending[module.id] = false;
        return module;
    },
    moduleCache : {},
    defines : {},
    pending : {} 
};

其中 moduleCache 对象用来存储已注册的模块(包括载入的和未载入的), defines 对象用来存储已注册的模块定义函数 function(require, exports, module)pending 对象用来标识每个模块当前的运行状态,帮助解决环状依赖。

模块注册

通过 register 方法注册模块,注册时便会调用 factory(见下文)生成一个模块,同时保存模块定义函数(define)。 这里有一个类型检测,检查模块定义函数是否为 function 类型。

模块执行

为了解决环状依赖,需要保存模块的执行状态在 pending 对象中。 之所以不放在 module 对象中,是为了避免将该状态暴露给客户使用(module 对象在模块定义函数中可访问)。

执行结束之后应当设置 module.loaded = true

模块依赖

CommonJS 对象提供了 require 通用方法来依赖一个模块,该方法用于从缓存获取或直接执行一个模块, 并获取其 module 对象。

注意该方法不同于 module.requiremodule.require 需要处理模块父子关系,并返回 module.exports 对象。

当被依赖模块未被注册时抛出错误,当被依赖模块已载入时直接将其返回,当被依赖模块正在载入时返回当前的 exports 对象(这一点很重要,借此实现了 Node.js 风格的环状依赖解决)。

环状依赖

对于正常的树状依赖关系,pending[mid] 始终为 false。 被 require 模块的 pending[mid] === true 时,说明该模块在依赖树祖先节点上已经被引用了。 此时应当返回被依赖模块当前的 module.exports 对象,该处理策略与 Node.js Cycles 兼容。

请看示例:

// Module "main"
exports.foo = 'foo';
var dep = require('dep');
console.log(dep.foo);     // 'foo'
console.log(dep.bar);     // 'bar'
exports.bar = 'bar';

// Module "dep"
exports.foo = 'foo';
var main = require('main');
console.log(main.foo);    // 'foo'
console.log(main.bar);    // undefined
exports.bar = 'bar';

入口模块为 main,在设置 exports.foo = 'foo' 后进行 require('dep')dep 模块开始执行,执行到 require('main').foo 时产生了环状依赖。

这时 Brick.JS 不抛出错误,而是将当前 mainexports 对象返回: 该对象只包含 foo 属性,bar 属性的定义还未执行到。

dep 执行结束后,main 继续执行 require('dep') 之后的语句,正确地输出了 'foo''bar'。 最后再给 exports.bar 赋值。

Module Factory

Module Factory 用来生成 Node.js 兼容的 CommonJS 模块(module)对象。在 Node.js 中,module 具有这些属性:

  • id(<String>):The identifier for the module. Typically this is the fully resolved filename.
  • children(<Array>): The module objects required by this one.
  • filename(<String>): The fully resolved filename to the module.
  • loaded(<Boolean>): Whether or not the module is done loading, or is in the process of loading.
  • parent(<Object>): The module that first required this one.
  • require(id)(Function): module.exports from the resolved module.
  • exports(<Object>): The module.exports object is created by the Module system, and will be returned when this one is "required".

参见:https://nodejs.org/api/modules.html

Brick.JS 支持除 filename 外(Brick.JS 模块运行在浏览器中,不需要该属性)的所有属性。 除此之外,由于 Brick.JS 模块主要用于 DOM 操作,提供了 module.elements 属性来访问当前模块对应的 DOM 节点集合。

var module = {
    require: function(mid) {
        var dep = CommonJS.require(mid);
        dep.parent = this;
        this.children.push(dep);
        return dep.exports;
    },
    loaded: false,
    parent: null
};

function factory(id) {
    var mod = Object.create(module);
    mod.id = id;
    mod.children = [];
    mod.exports = {};
    mod.elements = document.querySelectorAll('.brk-' + id);
    return CommonJS.moduleCache[id] = mod;
}

module 提供了模块对象 原型factory 方法用来生成一个模块实例。 需要注意的是,childrenexports 属性是对象类型, 放在 module 原型中会导致子对象(相当于面向对象中的实例)间共享。 因此需要在 factory 中单独赋值。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/04/25/commonjs.html。学识粗浅写作仓促,如有错误辛苦评论或 邮件 指出。