怎样实现一个 CommonJS 框架?
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.require
,module.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 不抛出错误,而是将当前 main
的 exports
对象返回:
该对象只包含 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>
): Themodule
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>
): Themodule.exports
object is created by the Module system, and will be returned when this one is "required".
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
方法用来生成一个模块实例。
需要注意的是,children
和 exports
属性是对象类型,
放在 module
原型中会导致子对象(相当于面向对象中的实例)间共享。
因此需要在 factory
中单独赋值。
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/04/25/commonjs.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。