理解 NodeJS 模块化:原理、加载顺序与实现

295 阅读5分钟

menu:

  1. 模块化是什么
  2. 模块化解决的问题
  3. NodeJS 模块化
  4. 模块加载顺序
  5. 调试 CommonJS
  6. 实现 CommonJS 规范?

模块化是什么?

模块化是一种将事务按照规则拆分为多个独立模块, 并在需要时灵活组合的设计思想 每个模块都是独立存在的, 并不依赖于其他模块

模块化解决什么问题?

模块化主要解决一下问题: 分享, 命名冲突, 复用, 按需加载

  1. 方便团队分享代码
  2. 解决命名冲突
  3. 方便代码 按需引入 并 复用 模块规范主要有:
  4. AMD(Asynchronous Module Definition 异步模块定义 适用于 浏览器)
  5. CMD(Common Module Definition 公共模块定义)
  6. CommonJS(服务端模块规范)
  7. UMD(Universal Module Definition 通用模块定义, 适用于 浏览器与 NodeJS)
  8. ESModule(ESM)

NodeJS 模块化

NodeJS 的模块化主要是用 CommonJS 规范, 可以发现 NodeJS 的模块化规范渐渐向前端靠拢因为 NodeJS 在 package.json 中对 type: "module" 即可以使用 ESM 模块规范 同时很多库也开始使用 ESM 来替代 CommonJS 模块规范.虽然已经有趋势向ESM模块靠拢, 但现在主要使用的还是 CommonJS

每个模块都是一个模块, NodeJS 使用 module.exports || exports 来进行导出, 使用 require 进行导入模块

const fs = require('fs');

module.exports = () => {
	console.log('module');
}

模块加载顺序

模块加载顺序如下: 内置模块优先 -> 第三方模块/项目依赖, 自定义模块(因为自定义模块一般使用相对路径进行加载) 自定义模块加载规则:

  1. 会添加.js|.json|.node后缀
  2. 高版本 Node 中 文件夹与文件重名 优先查找 文件
  3. 高版本 Node 中 先查找 package.json 中的 main 字段, 若是没有 main 字段则查找 index.js 我们总结一下: 内置模块 | 自定义模块 > 第三方模块

调试 原生 Module

准备工具:

  1. NodeJS 编译环境
  2. VSCode
  3. 两个 JS 文件

创建两个文件

// a.js
module.exports = 123;
// index.js
const a = require('./a');

console.log(a); // 123

添加调试文件

Pasted image 20241017174222.png

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [],
      // 假设文件目录: D:\\article\\node\\module\\index.js, ${workspaceFolder} 就是 D:\\article
      "program": "${workspaceFolder}\\node\\module\\index.js" // 需要改变为自己的文件夹
    }
  ]
}

点击紫色的位置, 即可添加断点

Pasted image 20241017174736.png 进行断点

Pasted image 20241017174838.png 进入函数 Pasted image 20241017175804.png 生成 require 函数 将 path 传入 Pasted image 20241114174037.png 验证路径 Pasted image 20241114174702.png 查看是否有缓存并加载模块, 若没有缓存则进行缓存 Pasted image 20241114174901.png Pasted image 20241114174944.png 初始化paths并获取扩展名称, 在通过 策略模式 处理不同类型文件 Pasted image 20241114175113.png 处理 .js 文件 并 执行 JS 文件 Pasted image 20241114175312.png 处理完JS 文件后将 module.exports 抛出 Pasted image 20241114175419.png

require 导入文件步骤总结

  1. 生成 require 函数
  2. 获取 引入文件的 绝对路径
  3. 查看是否有模块缓存
  4. 加载模块
  5. 初始化文件名称与 paths
  6. 获取扩展名称(.js/.json/.node)
  7. 使用策略模式加载文件
  8. 设置模块已经加载过 并 缓存
  9. 获取 exports
  10. 将 module.exports 抛出

实现 CommonJS 规范?

// 1. 生成 require 函数
// 2. 获取 引入文件的 绝对路径
// 3. 查看是否有模块缓存
// 4. 加载模块
// 5. 初始化文件名称与 paths
// 6. 获取扩展名称(.js/.json/.node)
// 7. 使用策略模式加载文件
// 8. 设置模块已经加载过 并 缓存
// 9. 获取 exports
// 10. 将 module.exports 抛出

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

// 判断是否以 某个 字符串开头
function validateStartString(start, str) {
  return str.startsWith(start);
}

// 裁剪开头
function stringSplice(id, len) {
  return id.slice(len);
}

// 验证字符串
function validateString(id, name) {
  if (typeof id !== 'string') {
    throw new TypeError(`${name}: ${id} type isn\'t string`);
  }
}


// 创建 require
const moduleRequire = markRequireFunction();

// 生成 requrie
function markRequireFunction() {
  return function require(id) {
    return Module.require(id);
  }
}

const modules = {};

function Module(id) {
  this.id = id;
  this.exports = {};
  this.load = false;
}


// 策略模式解决 对不同文件处理
Module._extensions = {};

Module._extensions['.js'] = function (module, filename) {
  const exports = module.exports,
    thisArgs = exports,
    _filename = module.id,
    _dirname = filename;
  const jsCode = fs.readFileSync(filename, 'utf-8');
  const args = [
    'exports', 'require', 'module', '__filename', '__dirname'
  ];
  const anonymous = vm.compileFunction(jsCode, args);

  Reflect.apply(anonymous, thisArgs, [
    exports, moduleRequire, module, _filename, _dirname
  ]);
  
  // 加载完毕
  this.load = true;
  
  return module.exports;
}

Module._extensions['.json'] = function (module, filename) {
  const jsonCode = fs.readFileSync(filename, 'utf-8');

  module.load = true;

  module.exports = JSON.parse(jsonCode);
}

// .node 为 C++ 编写的扩展模块, 所以我们不进行处理
Module._extensions['.node'] = function () {
  const nodeFile = fs.readFileSync(filename, 'utf-8');
  module.load = true;
  
  module.exports = nodeFile;
}

// 加载模块
Module.prototype.loaded = function (filename) {
  // 判断文件类型
  const _filename = Module._resolveFile(filename);

  if (!_filename) {
    throw new Error('Cannot find module ' + this.id);
  }

  const ext = path.extname(_filename);

  Module._extensions[ext](this, _filename);

  // 返回最终结果
  return this.exports;
}

Module._resolveFile = function (filename) {
  // 文件存在则直接返回
  if (fs.existsSync(filename)) {
    return filename;
  }

  // ['.js', '.json', '.node']
  const exts = Reflect.ownKeys(Module._extensions);

  for (let i = 0; i < exts.length; i++) {
    const ext = exts[i];
    const filenamePath = `${filename}${ext}`;
    if (fs.existsSync(filenamePath)) {
      return filenamePath;
    }
  }
}

// 引入模块
Module.require = function (id) {
  // 验证是否是字符串
  validateString(id, 'id');
  return Module._load(id, this);
}

// 加载模块
Module._load = function (id, module) {
  if (validateStartString('node:', id)) {
    id = stringSplice(id, 5);
  }

  // 获取绝对路径
  const filename = path.resolve(path.dirname(id), id);
  const cacheModules = modules[filename];
  
  // 查看是否有模块缓存
  if (cacheModules && cacheModules.load) {
    return cacheModules.exports;
  }
  
  // 创建模块
  const cacheRequire = new Module(id);
  // 缓存
  modules[filename] = cacheRequire;
  // 加载模块
  return cacheRequire.loaded(filename);
}

module.exports = moduleRequire;

总结

  1. 模块化是什么: 模块化就是对一个事务进行拆分, 拆分为 若干个模块, 待需要使用时在进行组装
  2. 模块化解决的问题: 代码重名, 代码共享, 按需引入
  3. NodeJS 模块化: 使用 CommonJS 规范 同时NodeJS也向ESModule靠拢
  4. 模块加载顺序: 查找 .js/.json/.node 若是没有则查找文件夹 在按照查找文件规则进行查找, 若还没有找到则按照 package.json 文件中配置的 main/exports 字段进行查找
  5. 模块加载步骤: 创建 reqiure -> 获取文件绝对路径 -> 查看是否有缓存 -> 初始化文件名称与 paths -> 策略模式加载文件 -> 将以加载过的模块缓存 -> 导出 exports