CommonJS 规范是把 Node.js 代码拆分成一个个文件,每个文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
那这是如何做到的呢?官方文档 里面能找到一些线索:
在执行模块代码之前,Node.js 会使用一个如下的函数封装器将其封装:
(function(exports, require, module, __filename, __dirname) { // 模块的代码实际上在这里 })通过这样做,Node.js 实现了以下几点:
- 保持了顶层的变量(用 var/const/let 定义)作用在模块范围内,而不是全局对象。
- 提供了一些看似全局的但实际上是模块特定的变量,例如 exports、__dirname 等
也就是说 CommonJS 规范是把每个单独的文件读取出来之后,包裹成一个 function 来执行的,从而实现代码的隔离。例如,我们把一个非常简单的打印两数之和的代码强拆成三个文件:
-
entry.js入口文件const {add} = require('./math') add(1,2) -
math.js用于两数求和const print = require('./print') function add(a, b) { print(a + b) } module.exports = { add } -
print.js用于打印function print(value) { console.log(value) } module.exports = print
当运行 node entry.js 的时候控制台会输出 3,因为 CommonJS 帮我们实现了模块化。接下来我们就抛弃 CommonJS,自己纯手工打造一个能够实现 js 文件模块化的代码。
首先我们新建一个 commonjs.js 的文件,在这个文件中实现自己的 Module 类:
const fs = require('fs')
const path = require('path')
function Module(id) {
this.id = id // 传入的路径
this.exports = {} // 保存导出的对象
this.load(Module._resolveFilename(id)) // 查找文件并进行加载
}
然后实现 _resolveFilename 静态方法,可以把传入的 id 解析成绝对路径,并检查文件是否存在:
Module._resolveFilename = function (id) {
const filePath = path.resolve(__dirname, id) // 解析成绝对路径
if (fs.existsSync(filePath)) return filePath // 存在就直接返回
const keys = Object.keys(Module._extensions) // 不存在就添加 .js 和 .json 后缀再次查找即可
for (let i = 0; i < keys.length; i++) {
const newFilePath = filePath + keys[i]
if (fs.existsSync(newFilePath)) return newFilePath
}
throw new Error(`Cannot find module '${id}'`) // 都找不到则报错
}
还要实现两个静态属性,一个是 _cache 用于缓存已经加载过的模块,一个是 _extensions 用于对 json 和 js 后缀的文件分类解析:
Module._cache = {} // 缓存
Module._extensions = { // 按照后缀分类处理
'.js'(module, filename) { // js文件包裹成函数来执行
const scripts = fs.readFileSync(filename, 'utf8')
const f = new Function('exports', 'require', 'module', '__filename', '__dirname',scripts)
f.call(module.exports, module.exports, module.require, module, filename, path.dirname(filename))
},
'.json'(module, filename) { // json文件直接parse成对象返回即可
module.exports = JSON.parse(fs.readFileSync(filename, 'utf8'))
},
}
到这里,核心代码基本都写完了,再封装一个 load 实例方法来调用:
Module.prototype.load = function (filename) { // 加载文件
const extname = path.extname(filename)
Module._extensions[extname](this, filename)
}
最后还缺少一个我们自己的 require 函数,不妨叫它 _require,可以从代码或缓存中取到模块加载的结果:
Module.prototype.require = _require
function _require(id) {
const filename = Module._resolveFilename(id)
if (Module._cache[filename]) return Module._cache[filename].exports
const module = new Module(filename)
Module._cache[filename] = module
return module.exports
}
上面的代码加起来整整 40 行,不多不少,这 40 行代码基本上实现了简易版的 CommonJS 规范了,我们不妨用自己的 _require 加载入口文件试试:
// 在 commonjs.js 最后一行加载入口文件,然后运行:node commonjs 即可
_require('./entry.js')
可以看到同样会输出 3。