CommonJS模块化浏览器端实践

概述

CommonJS是JavaScript模块化的主流规范之一,NodeJS中的模块就是采用的该规范。为了更好的理解CommonJS,我们可以在浏览器端实现一个简单的符合其规范的小工具。

JS模块

CommonJS规范简介

在CommonJS中,每一个文件就是一个模块,在模块中的变量,函数,类等都是私有的,除了指定方式以外,不能在其他模块中被访问。

每个模块中都有几个重要的模块变量,分别是module,exports和require。

  1. module对象代表当前模块,里面存储了当前模块的相关信息;
  2. exports是module的属性之一,也是模块变量,它用于在模块中导出内容(对象,函数,类等),它作为对外输出的接口,如果有其他模块引如了当前模块,exports中的内容就能被获取;
  3. require用于引入其他模块的内容,通过require可以引入其他模块导出的内容。

以上就是CommonJS中最重要的几个模块变量,除此之外还有一些其他变量,但是最常用的就是这些,实际上,一个模块系统实现这部分的规范就能满足绝大多数应用场景的要求了。

其他模块规范

除了CommonJS之外,还有一些其他的模块规范,包括AMD规范(常用于浏览器端异步加载),CMD规范(SeaJS遵循的规范),感兴趣的同学可以自己去了解这些规范。

实现和简单应用

简单了解了CommonJS规范之后,我们可以考虑实现一个在浏览器端的简单的雏形模块库。

对于模块而言,最终的要是能将各个模块分离开来,我们很容易就能想到,JavaScript的函数作用域可以屏蔽相互间的访问,为了实现相关的效果,我们必须用上函数作用域。下面整个CommonJS部分规范的基本模块库的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const require = (path) => {
const module = require.module[path];
if (!module) {
throw new Error(`Module ${path} not registered.`);
}
if (module.exports) {
return module.exports;
}
module.exports = {};
module.func.call(null, module, module.exports, require);
return module.exports;
};

require.module = {};

require.register = (path, func) => {
require.module[path] = {
func,
exports: null
};
};

require.init = fn => {
fn.call(null, require);
};

require函数,接收一个参数,用于在模块中引入其他模块,如果被引入的模块不存在, 则抛出错误,如果模块已经被调用过一次,则直接返回缓存的模块返回结果,否则,执行模块内容。

require.module用于缓存每一个模块的相关信息,包括模块的导出对象,模块的导入路径等。

require.register用于注册模块,所有的模块都要通过这个函数进行注册。

require.init用于指定程序执行的入口。

可以看到整个实现所需的源码并不多,使用起来也非常的简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注册foo模块
require.register('foo', function(module, exports, require) {
const foo = function() {
console.log('hello, this message is from FOO.');
};
module.exports = foo;
});
// 注册bar模块
require.register('bar', function(module. exports, require) {
const foo = require('foo');
module.exports = () => {
console.log('hello, this message if from BAR.');
foo.call();
};
});
// 程序执行的入口模块
require.init(function(require) {
const foo = require('foo');
const bar = require('bar');
foo.call();
bar.call();
});

当需要注册新的模块时,使用require.register方法。

综合require的源码可知,require.register接收两个参数,第一个是path,这里的path用于模拟真正模块中的文件路径,为了简便起见,源码只处理了简单的字符串,也就是说’foo’和’/foo’和’./foo’是不同的三个模块,因为我们并没有对path进行解析。第二个参数是模块所在的函数,该函数接受三个参数,分别是我们上文中介绍的三个重要的模块变量(module,exports和require)。

模块内的用法就是正常的CommonJS的用法,习惯NodeJS的同学一定不会陌生。

最后,require.init接收一个参数,该参数是一个函数,该函数和其他模块函数有些不一样,只有一个require,可以引入不可导出(这里的require.init并非标准,只是为了方便而实现的)。

总结

CommonJS基本规范实现的就是这样,可以看到并不复杂,但是手动实现一遍这样的一个小库有助于理解JS模块化的原理,之前我还有一篇描述Webpack生成的bundle文件的原理的文章,可以发现其实bundle文件的原理和本文的require其实也很类似。

参考

  1. tiny-browser-require - 阮一峰 - GitHub
  2. webpack输出文件浅析 - 传不习乎