总述
本文仅以文件模块加载过程为例,梳理nodejs中模块加载的核心流程。步骤如下:
- 路径分析:获取当前文件绝对路径
- 文件定位:判定当前文件类型(js、json、node),以便后续调用相应编译函数
- 缓存优先:提高模块加载效率,加载前先查询缓存是否已加载过
- 编译执行:将模块内容编译成当前模块中直接使用的数据
测试文件
创建一个test.js测试文件,内容如下:
const str = 'test'
module.exports = str
再创建一个testJ.json测试文件,内容如下:
{
"name": "lyk",
"age": 29
}
代码结构
创建一个require.js文件,确定需要引入的内置模块,自定义myRequire方法,并加载测试文件,实现基本结构如下:
const fs = require('fs')
const path = require('path')
const vm = require('vm') //将字符串处理成可执行代码,类似eval
function myRequire(filename){
//...code
}
let str = myRequire('./test')
console.log('str', str)
将所有代码都置于...code处,会过于臃肿,此处模仿nodejs源码,定义一个Module类,在myRequire外部处理每一步骤的详细过程。在myRequire文件中定义Module,如下:
class Module{
constructor(id){
this.id = id //id为模块标识
this.exports = {} //未来导出数据的容器
}
}
步骤一:获取绝对路径
function myRequire(filename){
// 1:获取绝对路径
let absPath = Module._resolveFilename(filename)
}
Module._resolveFilename = function (filename){
// 使用path将filename转为绝对路径
let absPath = path.resolve(__dirname, filename)
//判断当前路径对应内容是否存在
if(fs.existsSync(absPath)){
// 存在则直接返回
return absPath
}else{
// 不存在则进行文件定位,对文件进行后缀补足,再查询文件是否存在
let suffix = Object.keys(Module._extensions)
console.log(suffix) // ['.js', '.json']
for (let index = 0; index < suffix.length; index++) {
let path = absPath + suffix[index]
if(fs.existsSync(path)){
return path
}
}
}
throw new Error(`${filename} is not exists`)
}
对应后缀文件的处理函数(此处node后缀不作处理),后续步骤实现
Module._extensions = {
'.js'(){},
'.json'(){}
}
步骤二:查询缓存
function myRequire(filename){
// ...
// 2.缓存优先
let cache = Module._cache[absPath]
if(cache) return cache.exports
}
Module._cache = {}
步骤三:缓存未命中
function myRequire(filename){
// ...
// 3.创建空对象加载目标模块
let module = new Module(absPath)
// 写入缓存
Module._cache[absPath] = module
}
步骤四:编译执行
function myRequire(filename){
// ...
// 4.执行加载(编译执行)
module.load()
// 返回数据
return module.exports
}
Module.prototype.load = function(){
//通过步骤3创建的实例,this.id拿到absPath,通过Module._extensions对应键处理函数
let extname = path.extname(this.id)
//将this,即module传入处理函数
Module._extensions[extname](this)
}
进而去完善Module._extensions
Module._extensions = {
'.js'(module){
// 读取并包装函数
let content = Module.wrapper[0] + fs.readFileSync(module.id, 'utf8') + Module.wrapper[1]
// vm将字符串解析为可执行函数
let compileFn = vm.runInThisContext(content)
// 准备参数
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用(此处call绑定exports,也说明了为什么每个模块打印this时是{})
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module){
let content = fs.readFileSync(module.id, 'utf8')
module.exports = JSON.parse(content)
}
}
一个自执行函数的结构,并将模块内可调用的属性或对象写入参数
Module.wrapper = [
'(function(exports, require, module, __filename, __dirname){',
'})'
]
代码完整版
const fs = require('fs')
const path = require('path')
const vm = require('vm') //将字符串处理成可执行代码,类似eval
class Module{
constructor(id){
this.id = id //id为模块标识
this.exports = {} //未来导出数据的容器
}
}
Module._resolveFilename = function (filename){
// 使用path将filename转为绝对路径
let absPath = path.resolve(__dirname, filename)
//判断当前路径对应内容是否存在
if(fs.existsSync(absPath)){
// 存在则直接返回
return absPath
}else{
// 不存在则进行文件定位,对文件进行后缀补足,再查询文件是否存在
let suffix = Object.keys(Module._extensions)
console.log(suffix) // ['.js', '.json']
for (let index = 0; index < suffix.length; index++) {
let path = absPath + suffix[index]
if(fs.existsSync(path)){
return path
}
}
}
throw new Error(`${filename} is not exists`)
}
Module._cache = {}
Module.wrapper = [
'(function(exports, require, module, __filename, __dirname){',
'})'
]
Module.prototype.load = function(){
//通过步骤3创建的实例,this.id拿到absPath,通过Module._extensions对应键处理函数
let extname = path.extname(this.id)
//将this,即Module传入处理函数
Module._extensions[extname](this)
}
Module._extensions = {
'.js'(module){
// 读取并包装函数
let content = Module.wrapper[0] + fs.readFileSync(module.id, 'utf8') + Module.wrapper[1]
// vm将字符串解析为可执行函数
let compileFn = vm.runInThisContext(content)
// 准备参数
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用(此处call绑定exports,也说明了为什么每个模块打印this时是{})
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module){
let content = fs.readFileSync(module.id, 'utf8')
module.exports = JSON.parse(content)
}
}
function myRequire(filename){
// 1:获取绝对路径
let absPath = Module._resolveFilename(filename)
// 2.缓存优先
let cache = Module._cache[absPath]
if(cache) return cache.exports
// 3.创建空对象加载目标模块
let module = new Module(absPath)
// 写入缓存
Module._cache[absPath] = module
// 4.执行加载(编译执行)
module.load()
// 返回数据
return module.exports
}
let str = myRequire('./test')
console.log('str', str)
let json = myRequire('./testJ')
console.log('json', json.name)