简写一个node中的require方法

91 阅读3分钟

直接new req,我们在调用的时候会传入一个路径,这个路径就是文件名,我们就叫它filename,之后就调用了一个方法叫Module._load,接着又调用了一个方法 Module._resolveFilename

function Module (id) {
  this.id = id
  this.exports = {}
}
Module._resolveFilename = function (id) {

}
function req (filename) {
  filename = Module._resolveFilename(filename) 
}

我们需要用到三个模块 fs、path、vm

const fs = require('fs')
const path = require('path')
const vm = require('vm')

我们要判断这个路径是否存在

Module._resolveFilename = function (id) {
  let filePath = path.resolve(__dirname, id)
  let isExists = fs.existsSync(filePath)
  if (isExists) return filePath
}

接着我们要尝试添加后缀,我们就用.js、.json来做例子 我们在filePath的基础上加keys[i]就等到了一个新的路径newPath

我们就判断newPath是否存在,如果存在就直接返回路径,不存在接着去添加,可是找了一圈没找着的话,就直接抛错

Module._extensions = {
  '.js' () {},
  '.json' () {},
}
Module._resolveFilename = function (id) {
  let filePath = path.resolve(__dirname, id)
  let isExists = fs.existsSync(filePath)
  if (isExists) return filePath
  // 尝试添加后缀
  let keys = Reflect.ownKeys(Module._extensions)

  for (let i = 0; i < keys.length; i++) {
    let newPath = filePath + keys[i]
    if (fs.existsSync(newPath)) return newPath
  }
  throw new Error('module not found')
}

接着new Modul 拿到绝对路径创造一个模块

1.创造一个绝对引用地址,方便后续读取

2.根据路径创造一个模块 最终需要导出module.export 默认是空对象

function Module (id) {
  this.id = id
  this.exports = {}
}
function req (filename) {
  filename = Module._resolveFilename(filename)
  const module = new Module(filename)

  return module.exports 
}

接着调用原型方法module.load 对模块进行加载 module.load()的核心 就是让用户给module.exports 赋值

原型load不需要传任何参数,因为里面有this,指的就是module

接着根据文件后缀 Module._extensions[".js"] 去做策略加载 那我们怎么拿文件的后缀名呢?

根据文件的path.extname(),传入当前this.id,就可以获取当前文件的后缀名,接着就调用对应的策略,再把当前模块传进去,这样的好处就是不管什么样的文件都可以采用不同的策略,后续的逻辑只需要在策略里面更改

Module.prototype.load = function () {
  let ext = path.extname(this.id) // 获取文件后缀名
  Module._extensions[ext](this)
}
function req (filename) {
  filename = Module._resolveFilename(filename) 
  const module = new Module(filename)

  module.load() 
  return module.exports 
}

接着就是读取文件了,js文件第一步需要先读取脚本,再包装一个模板函数(function(exports,module,require,__dirname,__filename){${script}}) ,这个目前只是一个字符串,我们需要用vm.runInThisContext()把他变成函数

接着我们需要让函数执行,那就得用fn.call(),函数的call 的作用 1.改变this指向 2.让函数指向

Module._extensions = {
  '.js' (module) {
    let script = fs.readFileSync(module.id, 'utf8')
    let templateFn = `(function(exports,module,require,__dirname,__filename){${script}})`
    let fn = vm.runInThisContext(templateFn)
    let exports = module.exports
    let thisValue = exports 
    let filename = module.id
    let dirname = path.dirname(filename)

    fn.call(thisValue, exports, module, req, dirname, filename) 
  },
  '.json' (module) {
    let script = fs.readFileSync(module.id, 'utf8')
    module.exports = JSON.parse(script)
  },
}

现在还有小问题,就是模块没有缓存,会导致多次引入,多次输出

解决方法:创建一个Module._catch,每当创造一个模块,我们就加上一个缓存,根据文件名,因为文件名是独一无二的,我们再req的时候先判断,有缓存的模块直接返回就行了

Module._catch = {}
function req (filename) {
  filename = Module._resolveFilename(filename) 
  let cacheModule = Module._catch[filename]
  if (cacheModule) return cacheModule.exports 

  const module = new Module(filename) 
  Module._catch[filename] = module 
  module.load() 
  return module.exports 
}

好了,一个简写的require方法就完成了,我把完整代码放在下面(代码有注释哦~)

const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module (id) {
  this.id = id
  this.exports = {}
}
Module._catch = {}
Module._extensions = {
  '.js' (module) {
    let script = fs.readFileSync(module.id, 'utf8')
    let templateFn = `(function(exports,module,require,__dirname,__filename){${script}})`
    let fn = vm.runInThisContext(templateFn)
    let exports = module.exports
    let thisValue = exports // this = module.exports = exports
    let filename = module.id
    let dirname = path.dirname(filename)

    // 函数的call 的作用 1.改变this指向 2.让函数指向
    fn.call(thisValue, exports, module, req, dirname, filename) // 调用了a模块
  },
  '.json' (module) {
    let script = fs.readFileSync(module.id, 'utf8')
    module.exports = JSON.parse(script)
  },
}
Module._resolveFilename = function (id) {
  let filePath = path.resolve(__dirname, id)
  let isExists = fs.existsSync(filePath)
  if (isExists) return filePath
  // 尝试添加后缀
  let keys = Reflect.ownKeys(Module._extensions) // 以后object新出的方法 都会放到Reflect上

  for (let i = 0; i < keys.length; i++) {
    let newPath = filePath + keys[i]
    if (fs.existsSync(newPath)) return newPath
  }
  throw new Error('module not found')
}
Module.prototype.load = function () {
  let ext = path.extname(this.id) // 获取文件后缀名
  Module._extensions[ext](this)
}
function req (filename) {
  filename = Module._resolveFilename(filename) // 1.创造一个绝对引用地址,方便后续读取
  let cacheModule = Module._catch[filename]
  if (cacheModule) return cacheModule.exports // 直接将上次缓存的模块丢给你就ok了

  const module = new Module(filename) // 2.根据路径创造一个模块
  Module._catch[filename] = module // 最终:缓存模块 根据的是文件名来缓存
  module.load() // 就是让用户给module.exports 赋值
  return module.exports // 默认是空对象
}