1.前言
大家对nodejs中的require方法应该不会陌生,这个方法可以用来导入nodejs的内置模块,自定义模块,第三方模块等,使用频率非常高,那么这个方法内部是如何实现的呢?本篇文章就是从头到尾拆分实现流程,最终实现一个自定义的require方法的
2.前置操作
导入所需的nodejs内置库,分别为fs库用来加载文件内容,path库用于操作路径,vm模块执行js代码
const fs = require('fs')
const path = require('path')
const vm = require('vm')
3.操作步骤拆分
- 路径分析,解析出绝对路径
- 缓存优先原则,加载文件的时候优先从缓存中加载
- 文件定位,确定当前模块的文件类型(
js,json等等文件,方便后续调用对应的编译函数) - 编译执行,将加载模块的内容变为可以在当前模块中直接使用的数据
4.开始创建自定义的require方法
1.创建customrequire方法
function customrequire(modulename){}
2.创建一个module类,用于存储模块信息,以及挂载一些辅助方法
class module {
constructor(id) {
this.id = id
this.exports = {}
}
}
3.执行第一步路径分析,解析出绝对路径操作
// 给module类添加一个静态属性,用于表示模块的加载函数以及模块后缀的加载顺序
module._extensions = {
'.js'(module) {},
'.json'(module) {},
// ...
}
// 创建辅助方法,用于解析导入模块的绝对路径
module._resolvefilename = (modulename) => {
// 获取当前模块的绝对路径
let filepath = path.resolve(__dirname, modulename)
// 获取当前模块的后缀名
const extname = path.extname(modulename)
// 判断文件是否存在
if (!fs.existssync(filepath)) {
// 如果文件不存在,则按照顺序查找后缀名
const keys = object.keys(module._extensions)
// 文件是否存在标识
let flag = false
for (let i = 0; i < keys.length; i++) {
// 获取当前循环的后缀名
const currentextname = keys[i]
// 拼接后缀名
const currentfilepath = filepath + currentextname
// 判断拼接后缀名之后的文件路径是否存在
if (fs.existssync(currentfilepath)) {
// 如果存在,则将当前文件路径赋值给filepath
filepath = currentfilepath
// 将文件是否存在标识设置为true
flag = true
// 终止循环
break
}
}
// 如果不存在,则进行报错
if (!flag) {
throw new error(`${modulename} is not exists`)
}
}
// 返回处理后得到的文件的绝对路径
return filepath
}
// 创建自定义的require方法
function customrequire(modulename) {
// 路径分析,解析出模块的绝对路径
const abspath = module._resolvefilename(modulename)
}
4.执行缓存优先原则,加载文件的时候优先从缓存中加载
// 创建自定义的require方法
function customrequire(modulename) {
// 路径分析,解析出模块的绝对路径
const abspath = module._resolvefilename(modulename)
// 缓存优先原则,加载文件的时候优先从缓存中加载
const cachemodel = module._cache[abspath]
if (cachemodel) return cachemodel.exports
}
5.执行创建空对象目标加载模块操作(使用绝对路径作为模块的id)
// 定义空对象,用于存储缓存的模块
module._cache = {}
// 创建自定义的require方法
function customrequire(modulename) {
// 路径分析,解析出模块的绝对路径
const abspath = module._resolvefilename(modulename)
// 缓存优先原则,加载文件的时候优先从缓存中加载
const cachemodel = module._cache[abspath]
if (cachemodel) return cachemodel.exports
// 创建空对象目标加载模块(使用绝对路径作为模块的id)
let module = new module(abspath)
// 将已加载的模块缓存起来
module._cache[abspath] = module
}
6.执行模块的执行加载(编译执行)
// 在module类的原型链上新增load方法,用于加载模块
module.prototype.load = function () {
// 获取当前模块的后缀名
const extname = path.extname(this.id)
// 根据后缀名调用对应的加载函数
module._extensions[extname](this)
}
// 这里定义不同文件的不同加载与处理方法
module._extensions = {
'.js'(module) {
// 读取module.id对应的文件内容(这里的id对应的是需要读取的文件的绝对路径)
let content = fs.readfilesync(module.id, 'utf8')
// 包装内容,使其成为一个函数,主要作用就是为了让加载的模块中可以使用一些全局变量
content = module.wrapper[0] + content + module.wrapper[1]
// 使用vm模块,将字符串内容变为一个函数
const compilefn = vm.runinthiscontext(content)
// 定义上方创建的函数的形参对应的实参
let exports = module.exports // 这里的module.exports就是接收到的形参,module的实例化对象对exports静态属性
let filename = module.id
let dirname = path.dirname(module.id)
// 调用函数,这里使用call方法,
// 1.将this指向module.exports,call方法的第一个值就是重新指向的this
// 2.此时被加载的模块中就可以使用module.exports,所以被加载模块中的变量就可以存储到module.exports中
// 3.然后将其他的参数传递给函数
// 4.基于call的特性,会立即执行此函数
compilefn.call(exports, exports, customrequire, module, filename, dirname)
// 返回被改动后的module.exports
return module.exports
},
'.json'(module) {
// 读取module.id对应的文件内容(这里的id对应的是需要读取的文件的绝对路径)
let content = fs.readfilesync(module.id, 'utf8')
// 将读取到的内容变为一个对象
content = json.parse(content)
// 将读取到的内容赋值给module.exports
module.exports = content
},
// ...
}
// 定义空对象,用于存储缓存的模块
module._cache = {}
// 创建自定义的require方法
function customrequire(modulename) {
// 路径分析,解析出模块的绝对路径
const abspath = module._resolvefilename(modulename)
// 缓存优先原则,加载文件的时候优先从缓存中加载
const cachemodel = module._cache[abspath]
if (cachemodel) return cachemodel.exports
// 创建空对象目标加载模块(使用绝对路径作为模块的id)
let module = new module(abspath)
// 将已加载的模块缓存起来
module._cache[abspath] = module
// 执行加载(编译执行)
module.load()
// 返回模块加载后的结果(注意,不能将id也返回出去)
return module.exports
}
7.测试自定义require方法的效果如何
// ...上方代码
// 使用自定义模块加载js文件
const obj1 = customrequire('./测试使用文件/测试使用js')
console.log('接收到使用customrequire模块导入的js文件中导出的内容', obj1)
// 使用自定义模块加载json文件
const obj2 = customrequire('./测试使用文件/测试使用json')
console.log('接收到使用customrequire模块导入的json文件中导出的内容', obj2)
// 重复加载相同文件内容,观察是否从缓存中读取
const obj3 = customrequire('./测试使用文件/测试使用js')
console.log('接收到使用customrequire模块导入的js文件中导出的内容', obj3)
5.整体代码
// 导入所需的模块
const fs = require('fs')
const path = require('path')
const vm = require('vm')
// 创建一个module类,用于存储模块信息,以及挂载一些辅助方法
class module {
constructor(id) {
this.id = id
this.exports = {}
}
}
// 在module类的原型链上新增load方法,用于加载模块
module.prototype.load = function () {
// 获取当前模块的后缀名
const extname = path.extname(this.id)
// 根据后缀名调用对应的加载函数
module._extensions[extname](this)
}
// 给module类添加一个静态属性,用于表示模块的加载函数以及模块后缀的加载顺序
module._extensions = {
'.js'(module) {
// 读取module.id对应的文件内容(这里的id对应的是需要读取的文件的绝对路径)
let content = fs.readfilesync(module.id, 'utf8')
// 包装内容,使其成为一个函数,主要作用就是为了让加载的模块中可以使用一些全局变量
content = module.wrapper[0] + content + module.wrapper[1]
// 使用vm模块,将字符串内容变为一个函数
const compilefn = vm.runinthiscontext(content)
// 定义上方创建的函数的形参对应的实参
let exports = module.exports // 这里的module.exports就是接收到的形参,module的实例化对象对exports静态属性
let filename = module.id
let dirname = path.dirname(module.id)
// 调用函数,这里使用call方法,
// 1.将this指向module.exports,call方法的第一个值就是重新指向的this
// 2.此时被加载的模块中就可以使用module.exports,所以被加载模块中的变量就可以存储到module.exports中
// 3.然后将其他的参数传递给函数
// 4.基于call的特性,会立即执行此函数
compilefn.call(exports, exports, customrequire, module, filename, dirname)
// 返回被改动后的module.exports
return module.exports
},
'.json'(module) {
// 读取module.id对应的文件内容(这里的id对应的是需要读取的文件的绝对路径)
let content = fs.readfilesync(module.id, 'utf8')
// 将读取到的内容变为一个对象
content = json.parse(content)
// 将读取到的内容赋值给module.exports
module.exports = content
},
// ...
}
// 定义一个数组,用于存储包装内容
module.wrapper = [
"(function(exports, require, module, __filename, __dirname) {\n",
"\n});"
]
// 创建辅助方法,用于解析导入模块的绝对路径
module._resolvefilename = (modulename) => {
// 获取当前模块的绝对路径
let filepath = path.resolve(__dirname, modulename)
// 获取当前模块的后缀名
const extname = path.extname(modulename)
// 判断文件是否存在
if (!fs.existssync(filepath)) {
// 如果文件不存在,则按照顺序查找后缀名
const keys = object.keys(module._extensions)
// 文件是否存在标识
let flag = false
for (let i = 0; i < keys.length; i++) {
// 获取当前循环的后缀名
const currentextname = keys[i]
// 拼接后缀名
const currentfilepath = filepath + currentextname
// 判断拼接后缀名之后的文件路径是否存在
if (fs.existssync(currentfilepath)) {
// 如果存在,则将当前文件路径赋值给filepath
filepath = currentfilepath
// 将文件是否存在标识设置为true
flag = true
// 终止循环
break
}
}
// 如果不存在,则进行报错
if (!flag) {
throw new error(`${modulename} is not exists`)
}
}
// 返回处理后得到的文件的绝对路径
return filepath
}
// 定义空对象,用于存储缓存的模块
module._cache = {}
// 创建自定义的require方法
function customrequire(modulename) {
// 1.路径分析,解析出模块的绝对路径
const abspath = module._resolvefilename(modulename)
// 2.缓存优先原则,加载文件的时候优先从缓存中加载
const cachemodel = module._cache[abspath]
if (cachemodel) return cachemodel.exports
// 3.创建空对象目标加载模块(使用绝对路径作为模块的id)
let module = new module(abspath)
// 4.将已加载的模块缓存起来
module._cache[abspath] = module
// 5.执行加载(编译执行)
module.load()
// 6.返回模块加载后的结果(注意,不能将id也返回出去)
return module.exports
}
// 使用自定义模块加载js文件
const obj1 = customrequire('./测试使用文件/测试使用js')
console.log('接收到使用customrequire模块导入的js文件中导出的内容', obj1)
// 使用自定义模块加载json文件
const obj2 = customrequire('./测试使用文件/测试使用json')
console.log('接收到使用customrequire模块导入的json文件中导出的内容', obj2)
// 重复加载相同文件内容,观察是否从缓存中读取
const obj3 = customrequire('./测试使用文件/测试使用js')
console.log('接收到使用customrequire模块导入的js文件中导出的内容', obj3)
6.总结
本篇文章解析了require的大致流程(原require肯定不止这么多代码与功能的,复杂度要多的多,这里只是大致实现效果),主要用到了fs,path,vm模块。
以上就是nodejs实现一个自定义的require方法的详细流程的详细内容,更多关于nodejs自定义require方法的资料请关注代码网其它相关文章!
发表评论