CommonJS模块导入机制
在CommonJS中我们通过require方法导入模块,通过module.exports导出模块,module.exports默认的出口。我们也可以通过exports.属性(跟this.属性是同样效果)导出一个变量。之所以能这样使用,是因为模块中exports和this对象都与模块(module)的exports属性指向同一块内存地址。
如果把exports或者this赋值为一个变量,那么exports和this将不再指向module.exports引用的内存地址,那么通过那么这种方式不会起作用。
exports和module.exports也不要混合使用。
require导入模块文件的过程:
1. 将导入的模块解析为绝对路径,然后尝试判断这个模块是否存在,存在就继续执行,如果不存在就尝试为其添加扩展名(.js、.json),并再次判断模块是否存在,存在就继续执行,直到报错;
2. 从缓存中寻找模块,找到就返回;
3. 如果缓存中没有找到就通过new Module创建一个实例,并缓存这个模块;
4. 调用模块原型方法load加载这个模块;
5. 从模块id(也就是文件路径)中寻找extname,并通过策略模式加载相应的模块;
6. 如果这个文件是js文件,就读取文件的内容,并为其添加一个函数包裹,然后通过vm.runInThisContext方法将其转化为函数,并且传入参数然后调用这个函数;
7. 如果这个文件是json文件,就读取文件的内容并转化为JSON对象赋值给module.exports属性;
8. 返回module.exports属性。
// require.js
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module (id) {
this.id = id
this.exports = {}
}
Module._extensions = {
'.js' (module) {
const filename = module.id
// 同步读取script文件
let script = fs.readFileSync(filename, 'utf8')
// 为script添加包裹函数
script = `(function (exports, module, require, __dirname, __filename) { ${script }})`
// 将script字符串转化为真正的函数
const fn = vm.runInThisContext(script)
const dirname = path.dirname(filename)
const exports = module.exports
const thisValue = exports
// 执行函数,并传入参数
fn.call(thisValue, exports, module, req, dirname, filename)
},
'.json' (module) {
// 同步读取JSON文件并将结果赋值给module.exports
const content = fs.readFileSync(module.id, 'utf8')
module.exports = JSON.parse(content)
}
}
Module._resolveFilename = function (filename) {
const resolvePath = path.resolve(__dirname, filename) // 将文件名转化为绝对路径
const isExists = fs.existsSync(resolvePath)
if (isExists) return resolvePath
// 然后尝试添加后缀定位文件
for (const ext of Object.keys(Module._extensions)) {
const newResolvePath = resolvePath + ext
const isExists = fs.existsSync(newResolvePath)
if (isExists) return newResolvePath
}
throw new Error(`cannot resolve module from ${filename}`)
}
Module._cache = {}
Module.prototype.load = function () {
const filename = this.id
// 通过策略模式去加载不同的模块
const extname = path.extname(filename)
Module._extensions[extname](this)
}
function req (filename) {
// 将文件名转化为绝对路径,然后尝试添加后缀定位文件
filename = Module._resolveFilename(filename)
// 确定文件路径,先从缓存中去取文件
const cachedModule = Module._cache[filename]
if (cachedModule) return cachedModule.exports
// 创建模块,并缓存模块
const module = new Module(filename)
Module._cache[filename] = module
// 加载文件
module.load()
// 返回文件的导出内容
return module.exports
}
const a = req('./a')
console.log(a)
// a.js
const a = 'hello a'
module.exports = a