nodejs实现一个自定义的require方法的详细流程

 更新时间:2025年03月26日 09:09:36   作者:rookiefishs  
大家对nodejs中的require方法应该不会陌生,这个方法可以用来导入nodejs的内置模块,自定义模块,第三方模块等,使用频率非常高,那么这个方法内部是如何实现的呢?本篇文章就是从头到尾拆分实现流程,最终实现一个自定义的require方法,需要的朋友可以参考下

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方法的资料请关注脚本之家其它相关文章!

相关文章

  • 快速掌握Node.js模块封装及使用

    快速掌握Node.js模块封装及使用

    这篇文章主要为大家详细介绍了Node.js模块封装及使用,帮助大家快速掌握Node.js模块封装及使用,感兴趣的小伙伴们可以参考一下
    2016-03-03
  • 在nodejs中创建child process的方法

    在nodejs中创建child process的方法

    这篇文章主要介绍了在nodejs中创建child process的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • Thinkjs3新手入门之添加一个新的页面

    Thinkjs3新手入门之添加一个新的页面

    Thinkjs 是一个快速、简单的基于MVC和面向对象的轻量级Node.js开发框架,下面这篇文章主要给大家介绍了关于Thinkjs3新手入门之添加一个新的页面的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下。
    2017-12-12
  • sublime text配置node.js调试(图文教程)

    sublime text配置node.js调试(图文教程)

    下面小编就为大家分享一篇sublime text配置node.js调试(图文教程),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-11-11
  • 使用Node操作文件夹的常用API

    使用Node操作文件夹的常用API

    这篇文章我们将学习Node对文件夹的操作,当我们学习完文件夹的操作后结合文件的操作我们就可以真正的通过Node在日常的工作生活中解决许多和文件相关的问题,这篇文章我们将首先讲解文件夹操作的几个API,然后完成一下最常见的文件夹递归的操作,需要的朋友可以参考下
    2024-08-08
  • 深入理解Node.js中CORS的三个重要响应头

    深入理解Node.js中CORS的三个重要响应头

    CORS是一种安全机制,通过配置适当的响应头,服务器可以允许或限制外部域对资源的访问,本文主要介绍了Node.js中CORS的三个重要响应头,感兴趣的可以了解一下
    2024-12-12
  • 在Mac OS下使用Node.js的简单教程

    在Mac OS下使用Node.js的简单教程

    这篇文章主要介绍了在Mac OS下使用Node.js的简单教程,Node.js是让JavaScript应用运行于服务器端的框架,需要的朋友可以参考下
    2015-06-06
  • 使用Node.js给图片加水印的方法

    使用Node.js给图片加水印的方法

    使用Node.js给图片加水印,首先要确保本地安装了node环境。然后,我们进行图像编辑操作需要用到一个Node.js的库:images。具体详情大家可以通过本文了解下
    2016-11-11
  • Node.js复制文件的方法示例

    Node.js复制文件的方法示例

    这篇文章主要介绍了Node.js复制文件的方法,涉及nodejs针对文件流的创建、读取、写入等操作技巧,需要的朋友可以参考下
    2016-12-12
  • node.js安装超详细步骤教程(推荐!)

    node.js安装超详细步骤教程(推荐!)

    其实Node.js就是运行在服务端的JavaScript,Node.js是一个基于Chrome JavaScript运行时建立的一个平台,下面这篇文章主要给大家介绍了关于node.js安装超详细步骤教程的相关资料,需要的朋友可以参考下
    2023-06-06

最新评论