Commonjs 标准的提出弥补 Javascript 对于模块化没有统一标准的缺陷。nodejs 借鉴了 Commonjs 规范,实现了良好的模块化管理。
Commonjs 规范
- 每个文件就是一个模块,称之为 module,所有代码运行在模块作用域中,不会污染全局作用域;
- nodejs 中的 Commonjs 模块加载采用同步加载方式,模块的加载顺序,按照其在代码中出现的顺序进行加载;
- 模块可以加载多次,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中读取输出模块;
- 通过 require 加载模块,通过 exports 或 module.exports 输出模块;
commonjs 基本使用
// hello.js
let name = 'xiaoming'
module.exports = function sayName (){
return name
}
// home.js
const sayName = require('./hello.js')
module.exports = function say() {
return {
name: sayName(),
author: 'xiaoming'
}
}
如上就是 Commonjs 最简单的实现,那么就必须解决两个问题:
- 如何解决变量私有化的问题?
- module.exports,exports,require 三者是如何工作的?
解决变量私有化
我们知道每个模块文件上存在 module,exports,require三个变量,它们分别表示:
- module: 记录当前模块信息;
- require: 引入模块的方法;
- exports: 当前模块导出的属性;
然而这三个变量并没有被定义的,但是我们可以在 Commonjs 规范下每一个 js 模块上直接使用它们,这是为什么呢?
这是因为 node 在编译的过程中对 js 的代码块进行首尾包装,以上述的 home.js 为例子,它被包装之后的样子如下:
(function(module, exports, require, __filename, __dirname){
const sayName = require('./hello.js')
module.exports = function say() {
return {
name: sayName(),
author: 'xiaoming'
}
}
})
可以看到,require
,exports
,module
是通过形参的方式传递到包装函数中的。那么包装函数本质上是什么样子的呢?
function wrapper(script) {
return (
'(function (module, exports, require, __filename, __dirname) {' +
script +
'\n})'
)
}
模块的代码script
(脚本),是以形参的形式传递到包装函数中去的。
const modulefunction = wrapper(`
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'我不是外星人'
}
}
`)
上面模拟了一个包装函数功能, script
为我们在 js 模块中写的内容,最后返回的就是包装之后的函数,当然这个函数暂且是一个字符串。
在模块加载的时候,会通过 runInThisContext (可以理解成eval) 执行 modulefunction ,传入require,exports,module 等参数,最终我们写的 nodejs 文件就这么执行了。
runInThisContext(modulefunction)(module, exports, require, __filename, __dirname)
至止,就完成了整个模块执行的原理,同时利用IIFE
解决了变量私有化的问题。
require
require 加载文件类型
require 加载文件有三种类型:
const fs = require('fs') // 核心模块
const sayName = require('./hello.js') // 文件模块
const crypto = require('crypto-js') // 第三方自定义模块
当 require 方法执行的时候,接收的唯一参数作为一个标识符,Commonjs 下对不同的标识符,处理流程不同,但是目的相同,都是找到对应的模块。
核心模块处理
核心模块的优先级仅次于缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载核心模块,加载过程中速度最快。
文件模块处理
以 ./
, ../
和 /
开始的标识符,会被当作文件模块处理。require() 方法会将路径转换成真实路径,并以真实路径作为索引,将编译后的结果缓存起来,第二次加载的时候会更快。至于怎么缓存的,我们稍后会讲到。
自定义模块处理
自定义模块,一般指的是非核心的模块,它可能是一个文件或者一个包,它的查找会遵循以下原则:
- 在当前目录下的 node_modules 目录查找。
- 如果没有,在父级目录的 node_modules 查找,如果没有在父级目录的父级目录的 node_modules 中查找。
- 沿着路径向上递归,直到根目录下的 node_modules 目录。
- 在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有 package.json,在 node 环境下会以此查找 index.js,index.json,index.node。
require 加载原理
首先要明白两个概念,那就是 module 和 Module:
- module: 在 Node 中每一个 js 文件都是一个 module,module 上保存了 exports 等信息。
- Module: Module 是 module 的构造函数,同时在 Module 上挂载了很多属性,比如,会用 Module 缓存每一个模块加载的信息。
require 的源码在 Node 的 lib/module.js 文件,为了便于理解,本文引用的源码是简化过。
Module 构造函数
function Module(id, parent) {
this.id = id
this.exports = {}
this.parent = parent
this.filename = null
this.loaded = false
this.children = []
}
上面代码中,Node 定义了一个构造函数 Module,所有的模块都是 Module 的实例。
require 方法
- 在 Module 的原型有一个 require 方法
Module.prototype.require = function(path) {
return Module._load(path, this);
};
- require 方法调用了
Module._load
方法
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
}
- require 流程参考下图
- require 接收一个参数(文件标识符),根据其计算出文件的绝对路劲,首先在 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容。
- 如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,最后返回 module.exports 对象。
exports 和 module.exports
首先看一下两个的用法:
exports 使用
// a.js
exports.author = 'xiaoming'
exports.say = function () {
console.log(666)
}
引用
const a = require('./a')
console.log(a)
打印结果:
{
author: 'xiaoming',
say: function () {
console.log(666)
}
}
可以看到 exports 最后导出了一个对象。
module.exports 使用 module.exports 本质上就是 exports ,我们用 module.exports 来实现如上的导出。
module.exports = {
author:'xiaoming',
say(){
console.log(666)
}
}
module.exports 也可以单独导出一个函数
module.exports = function () {
// ...
}
从上述 require 原理实现中,我们知道了 exports 和 module.exports 持有相同引用,因为最后导出的是 module.exports 。所以,我们使用的时候选择exports 和 module.exports 两者之一,如果两者同时存在,很可能会造成覆盖的情况发生。比如如下情况:
exports.author = 'alien' // 此时 exports.name 是无效的
module.exports = {
author: 'xiaoming',
say() {
console.log(666)
}
}
既然有了 exports,为何又出了 module.exports ?
如果我们不想导出对象,而是只导出一个函数,那么使用 module.exports 就更方便了。因为 exports 始终会导出一个对象,不能导出除对象之外其他类型。
Browserify 中如何实现 Commonjs
上面的章节,我们详细介绍了 nodejs 中 Commonjs 的实现,Commonjs 除了在node中实现以外,它还应用的场景有:
Browserify
(一个编译工具) 是 Commonjs 在浏览器中的一种实现;- webpack 打包工具对 CommonJS 的支持和转换,也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。
我们知道 Commonjs 代码只能在 nodejs 环境中运行,是不能在浏览器中运行的,如果要想在浏览器环境下使用 Commonjs 模块的代码,那就需要通过 Browserify
编译后才能在浏览器中使用。下面我们详细来介绍 Commonjs 在 Browserify 中的实现。
首先安装 Browserify
npm install browserify -g
源码
// entry.js
const sum = require('./sum')
sum(1, 2)
// sum.js
function sum(a, b) {
return a + b
}
module.exports = sum
打包
browserify cjs/entry -o dist/entry.js
打包后的代码(对代码进行了精简)
;(function () {
function r(e, n, t) {
// 这个就是require函数
function o(i, f) {
if (!n[i]) {
var p = (n[i] = { exports: {} })
// e[i] 就是 entry.js 模块里面的代码,利用call函数执行,并把require, module, exports传入
e[i][0].call(
p.exports,
// 传入require函数
function (r) {
var n = e[i][1][r]
return o(n || r)
},
// 传入module
p,
// 传入exports
p.exports,
...
)
}
// 返回结果
return n[i].exports
}
for ( var i = 0; i < t.length; i++) {
// 调用require函数
o(t[i])
}
return o
}
return r
})()(
{
// entry.js 模块
1: [
// 在代码外面套了一层函数,然后把require, module, exports传入
function (require, module, exports) {
const sum = require('./sum')
sum(1, 2)
},
// entry.js 模块 依赖了那些模块
{ './sum': 2 }
],
// sum.js 模块
2: [
function (require, module, exports) {
function sum(a, b) {
return a + b
}
module.exports = sum
},
{}
]
},
{},
[1]
)
- 首先 Browserify 会把每个js文件代码外面套一层函数,通过参数的形式传入require, module, exports,类似于nodejs中的require的实现。
function (require, module, exports) {
function sum(a, b) {
return a + b
}
module.exports = sum
}
- 实现一个 require 函数
function o(i, f) {
if (!n[i]) {
var p = (n[i] = { exports: {} })
e[i][0].call(
p.exports,
function (r) {
var n = e[i][1][r]
return o(n || r)
},
p,
p.exports,
...
)
}
return n[i].exports
}
总结
本文分析了 nodejs 是如何实现 commonjs 规范要求的。首先,通过IIFE
在模块外层套一层函数,解决了变量私有化的问题;接着,分析了require函数的加载过程;最后比较了 exports 和 module.exports 的区别及使用场景。
利用上面所学的知识,进一步分析了 Browserify 是如何实现 commonjs 规范的,发现其实两者原理都很相似。通过本文的学习,我们对commonjs模块化有了大致了解,为后续的学习打下坚实的基础。