面试官:试着说一下commonjs模块化加载过程,并试着手写一个简易版的commonjs
站长
· 阅读数 17
我们知道commonjs适用于nodejs中,因为他是同步加载文件模块的,由于文件模块都在本地磁盘,加载速度比较快,所以可以在nodejs中盛行。
nodejs中的内置模块在nodejs运行的时候就已经被作为二进制加载到内存中,require时直接在内存中取出。
加载流程分析
-
路径分析:确定目标模块位置 (内置,第三方(module.paths),本地)
-
文件定位:确定目标中的具体文件。(js/json/node -> package.json中的main -> index.js/json/node)
-
编译执行:对模块内容进行编译,返回可用的exports对象。(js -> 函数调用,传递参数。json -> JSON.parse())
源码解析
- relResolveCacheIdntifier 根据路径查找该路径是否被缓存。
- 无,解析并补全路径为完整的路径
- 根据路径查找模块是否被缓存。
- 是否是内置模块
- 创建一个module对象,并缓存该模块。
- module.load() 初始化一些模块属性。
- 通过后缀取出对应的函数。
- 函数内通过readFileSync同步加载文件内容。
- module._compile() 内部将文件内容包裹在一个全局函数中。即每个模块的作用域函数。
- 通过获取require、exports、module、__dirname、__filename传入并调用全局函数,返回结果。其中this被绑定为空对象。
自己实现commonjs模块化
- 通过fs.readFileSync读取文件,
eval
执行字符串。
缺点:没有自己的作用域。
const fs = require("fs")
const content = fs.readFileSync("./demo.js", "utf-8") // var a = 10
eval(content)
console.log("a", a) // 10
- 通过Function执行字符串。
缺点:定义麻烦。
const fs = require("fs")
const content = fs.readFileSync("./demo.js", "utf-8") // var a = 10
const fn = new Function("", `${content} return a`);
console.log(fn()) // 10
- 通过nodejs内置的核心模块
VM
可以创建一个独立运行的沙箱环境。
const fs = require("fs")
const vm = require("vm")
const b = 100
a = 100 // 可以被使用
const content = fs.readFileSync("./demo.js", "utf-8") // var a = 10
// vm.runInThisContext(content) // 内部是一个沙箱,不能使用外部变量。但是外部可以使用沙箱内的内容
// console.log(a) // 10
vm.runInThisContext("a = a + 10") // 但是沙箱内可以使用外部为使用声明定义的变量
console.log(a) // 110
实现
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module (id) {
// 存储文件路径
this.id = id
// 存储导出内容
this.exports = {}
}
// 分析传入的路径
Module._resolveFilename = function (filename) {
// 利用 Path 将 filename 转为绝对路径
let absPath = path.resolve(__dirname, filename)
// 判断当前路径对应的内容是否存在()
if (fs.existsSync(absPath)) { // 这里查到文件夹也是可以通过的
// 如果条件成立则说明 absPath 对应的内容是存在的
// 如果是文件夹取index.js,否则抛出错误
if(fs.statSync(absPath).isDirectory()) {
if(fs.existsSync(path.join(absPath, "index.js"))) {
return path.join(absPath, "index.js")
}else {
throw new Error(`${filename} is not exists`)
}
}else {
return absPath
}
} else {
// 文件定位
let suffix = Object.keys(Module._extensions)
for(var i=0; i<suffix.length; i++) {
let newPath = absPath + suffix[i]
if (fs.existsSync(newPath)) {
return newPath
}
}
}
throw new Error(`${filename} is not exists`)
}
// 定义查询的扩展名和函数映射
Module._extensions = {
'.js'(module) {
// 读取
let content = fs.readFileSync(module.id, 'utf-8')
// 包装
content = Module.wrapper[0] + content + Module.wrapper[1]
// VM
let compileFn = vm.runInThisContext(content)
// 准备参数的值
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module) {
let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
module.exports = content
}
}
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
// 缓存对象 (绝对路径:module对象)
Module._cache = {}
// 加载模块,取出扩展名执行对应函数
Module.prototype.load = function () {
let extname = path.extname(this.id)
Module._extensions[extname](this)
}
function myRequire (filename) {
// 1 绝对路径
let mPath = Module._resolveFilename(filename)
// 2 缓存优先
let cacheModule = Module._cache[mPath]
if (cacheModule) return cacheModule.exports
// 3 创建空对象加载目标模块
let module = new Module(mPath)
// 4 缓存已加载过的模块
Module._cache[mPath] = module
// 5 执行加载(编译执行)
module.load()
// 6 返回数据
return module.exports
}