node模块机制&实现丐版require

模块加载&编译机制&require

路径分析

  • NodeJS 中的模块可以通过 文件路径 或者 名字 获取到模块,模块的引用会映射到一个 JS 的文件
  • 主要分为两种类型的模块:
    • Node 提供的模块,称为核心模块,内置模块公开了一些常用的 API 给开发者,并且它们在 Node 进程开始的时候就预加载了
    • 由用户编写的模块,称为 文件模块 ,通常是 npm 包或者是本地模块,通常会暴露一个公开的 API ,供其他开发者使用

核心模块是Node源码在编译过程中编译进了二进制执行文件。在Node启动时这些模块就被加载进内存中,所以核心模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断,因此核心模块的加载速度是最快的。文件模块则是在运行时动态加载,速度比核心模块慢

模块载入

  • 模块的载入是有先后顺序的,这里简单介绍一下
  1. 载入内置模块(A Core Module
  2. 载入文件模块(A File Module),可以忽略 .js, .json, .node后缀,如果不存在对应的文件,就将这个路径作为文件夹加载(步骤3)
  3. 载入文件目录模块(A Folder Module)此时的策略是:
    • 先找寻该目录下的 package.json文件,然后 parse 出来,读取 mainindex字段中指定的文件(main 的优先级高)
    • 如果都没有指定,那么会默认寻找 index.js 文件
    • 如果 index.js 文件也不存在,则报错
  4. 载入 node_modules 里的模块
    • 如果 传入的模块名称不是 路径,也不是核心模块,那么 会从 当前目录的 node_modules 文件夹搜索,如果当前目录下的 node_modules 找不到,则会从父目录的 node_modules 搜索,这样递归搜索,知道当前项目的根目录
  5. 自动缓存已载入模块,对于已加载的模块Node会缓存下来,而不必每次都重新搜索,因为寻找模块是一件很费时的事情,所以引入了缓存机制

模块编译

  • 每个模块文件模块都是一个对象,他的定义如下:
js 复制代码
function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  moduleParentCache.set(this, parent);
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}
  • 对于不同的拓展名,编译的方法也是不同的
    • .js 通过 fs 模块 同步 读取文件后编译执行
    • .json 通过 fs 模块 同步 读取文件后,用 JSON.parse() 解析返回结果
    • .node 这是由 C/C++ 编写的拓展文件,通过 dlopen() 方法加载编译生成的文件
    • 对于其他的文件,一律当作 .js 处理
  • 每一个编译成功的模块都会将其文件路径作为 key 缓存再 Module_cache 对象上
  • 主要是通过 Module._extensions[xxx] 的方法来实现 编译,后续我们会自己实现一个简易版

js 模块编译

  • 编译过程中,Node 会对 读取的 js 文件包装一层,方便注入一些 require, __dirname 等全局变量,以及执行
js 复制代码
(function (exports, require, module, __filename, __dirname) {
	......
})
  • 包装后的代码,会通过 vm 原生模块的 runInThisContext 方法编译,这个方法接受一个字符串,将其转化为函数,并且指定明确的上下文,不污染全局
  • 此方法用于创建一个独立的沙箱运行空间,cotent 内的代码可以访问外部的 global 对象,但是不能访问其他变量

实现一个简易版 require

  • 从我们之前了解到的 模块机制中 不难发现,其实 require 和 module.exports 干的事情并不复杂,假设有一个全局的对象 ,初始值为 {},当我们 require 的文件,就将这个文件拿出来执行,如果这个文件里存在 module.exports 的语句,则当运行到这行代码的时候,将module.exports的值加入这个对象,key为对应的 文件名,最终,这对象会变成这样:
js 复制代码
{
	'a.js': 'hello',
	'b.js': function hello() {},
	'c.js': 2,
	'd.js': { d: 2 }
}
  • 当再次require 这个文件时,如果这个对象里面有对应的值,就直接返回,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports加入这个全局对象,并返回给调用者。
  • 这个全局对象其实就是上面说的的缓存机制(Module._cache)。
  • 所以requiremodule.exports并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行。
  • 再看看这个对象,因为d.js是一个引用类型,所以在任何地方获取了这个引用都可以更改其中的值,如果不希望自己模块的值被更改,需要在写模块时进行处理,比如使用Object.freeze()Object.defineProperty()之类的方法冻结住

实现

  • 思路
    1. 通过传入的 id 找到对应的文件
    2. 执行找到的文件,同时要注入modulerequire这些方法和属性,以便模块文件使用
    3. 返回模块的module.exports
  • 实现细节如下:
js 复制代码
const path = require('path')
const fs = require('fs')
const vm = require('vm')

function MyModule(id = '') {
  this.id = id
  this.filename = path.resolve(id)
  this.exports = {}
  this.filename = null
  this.loaded = false
}

// require 实际上是 调用 _load 方法
MyModule.require = function (id) {
  return MyModule._load(id)
}

// 存放缓存模块
MyModule._cache = Object.create(null)

// 加载 模块
// 返回 module.exports
MyModule._load = function (id) {
  // 根据 id 解析出 filename
  const filename = MyModule._resolvePath(id)

  // 根据 filename 判断是否有 缓存,有的话,直接返回 缓存的 exports 即可
  const cachedModule = MyModule._cache[filename]
  if (cachedModule !== undefined) {
    return cachedModule.exports
  }

  // 没有缓存 则构造一个模块 并且调用 load 方法加载
  const module = new MyModule(filename)
  // load 之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的 exports 可能还没有或者不完整
  MyModule._cache[filename] = module
  module.load(filename)

  return module.exports
}

MyModule._resolvePath = function (id) {
  const filename = path.resolve(id)
  const extname = path.extname(id)

  // 如果 没有后缀,则尝试找寻,添加后缀,找到则返回
  if (!extname) {
    const exts = Object.keys(MyModule._extensions)
    for (const ext of exts) {
      const curPath = `${filename}${ext}`
      if (fs.existsSync(curPath)) {
        return curPath
      }
    }
  }
  return filename
}

MyModule.load = function (filename) {
  // 拿到 后缀,根据后缀,调用相应的 加载函数 来处理
  const extname = path.extname(filename)
  MyModule._extensions[extname](this, filename)
  this.loaded = true
}

// js 文件的 加载函数
MyModule._extensions['.js'] = function (module, filename) {
  // 思路很简单,读取文件内容,随后编译,执行该文件
  const content = fs.readFileSync(filename, 'utf-8')
  module._compile(content, filename)
}

// 编译 文件
// 首先,我们在执行 js 的时候,是可以拿到 一些全局变量,例如 require exports module __filename __dirname
// 所以,在执行前,我们要注入这些变量
// 实现也很简单,通过 IIFE 的形式注入即可
// 所以,这里我们需要将 js 代码包裹一层 IIFE,后续 执行的时候,可以注入这些变量
MyModule._compile = function (content, filename) {
  // 将 读取的代码 包裹起来
  const wrappedFn = MyModule._wrap(content)

  // vm 是 nodejs 的虚拟机沙盒模块,runInThisContext 方法可以接受一个字符串并将它转化为一个函数
  // 返回值就是转化后的函数,所以 compiledFn 是一个函数
  const compiledFn = vm.runInThisContext(wrappedFn, {
    filename,
    lineOffset: 0,
    displayErrors: true,
  })
  const dirname = path.resolve(filename)

  // 执行 这段代码,注入全局变量
  // 代码中 会有类似 module.exports.xxx = xxx,这就为 this.exports 挂载上了 导出的属性
  compiledFn.call(
    // call 的第一个参数就是 this,这里传入了 this.exports, 也就是 module.exports
    // 这也是为什么在 js 文件里面 this 是对 module.exports 的一个引用
    this.exports,
    this.exports,
    this.require,
    this, // module 就是 this,指向当前模块
    filename,
    dirname
  )
}

// 需要包裹的部分
// 注意拼接的开头和结尾多了一个()包裹,这样我们后面可以拿到这个匿名函数
// 拿到后,在后面再加一个 () 就可以传参数执行了。
MyModule.wrapper = [
  '(function(exports, require, module, __filename, __dirname){',
  '\n})',
]
// 包裹函数
// 将需要执行的 js 代码拼接到这个方法中间
MyModule._wrap = function (content) {
  return MyModule.wrapper[0] + content + MyModule.wrapper[1]
}

相关文章

nodejs文档

深入Node.js的模块加载机制,手写require函数

深入了解Nodejs模块机制

相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu7 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss