一个简易的 CommonJS 实现

JavaScript CommonJS DOM HTML HMVC Node.js 模块化 Brick.JS

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.com';
exports.log = console.log.bind(console);

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

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

客户端 JS 模块化

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

CommonJS.register('foo', function(require, exports, module){
    exports.author = 'harttle.com';
    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.com

详情见: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中单独赋值。

Harttle

致力于简单的、一致的、高效的前端开发

看看这个?