Skip to content

Node 模块化

TIP

Node 目前采用的是 commonjs 规范编写的模块化

1、commonjs 规范

  1. 每个 js 文件都是一个模块 (每个文件在运行的时候都会产生一个函数)
  2. 模块的导出 module.exports
  3. 每次使用一个模块可以采用 require 方式来进行导入

2、模块的分类

  1. 核心模块/内置模块,不需要安装,直接引入使用,引入不需要加入相对路径和绝对路径,比如:fs http path
  2. 自定义模块,需要通过绝对路径或者相对路径进行引入
  3. 第三方模块,需要先安装,再引入使用

3、require 模块实现代码

3.1 两个常量

  • 在 commonjs 规范中 文件模块会默认提供两个参数 **dirname **filename
    • __filename 文件的绝对路径 ,当前的文件的路径不能修改
    • __dirname 文件所在的目录,绝对路径 不能修改的

3.2 如何让一个字符串能执行?

  • eval 执行不会创造隔离,而且性能差
  • new Function 在 node 中可以用,浏览器会有一些小问题
  • vm.compileFunction("console.log(a)"); 可以创建一个独立的函数来运行代码

3.3 模块加载的流程

  • 1.加载模块 Module.__load 加载模块之后 最终返回的就是 module.exports
  • 2.Module._resolveFilename 解析文件名, 产生一个可读取的文件名 .js? .json?
  • 3.Module._cache 如果文件被缓存过 直接拿上一次的返回结果
  • 4.如果模块没有加载过,会根据路径创建一个模块 new Module()
  • 5.缓存模块为了后续使用
  • 6.module.load 加载模块(读文件)
  • 7.获取扩展名来调用不同的加载方式
  • 8.根据扩展名查找 对应的加载方式 Module._extension
  • 9.js 的模块主要是读取
  • 10.读取文件后包裹函数,并且传入五个参数 [ 'exports','require','module','__filename', '__dirname' ]
  • 11.执行函数 用户会给 module.exports 赋予值
    1. 因为最终返回的是 module.exports 所以可以拿到最终的返回结果

3.4 手写 commonjs 模块加载加载

js
// 1、建一个文件a.js

console.log(1);
module.exports = "hello";
js
// 2、编写模块化代码

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

// 定义模块函数
function Module(fileName) {
  this.id = fileName; // 文件名
  this.exports = {}; // 导出的结果
  this.path = path.dirname(fileName); // 父目录
}

// 给字符串包裹函数
Module.wrapper = (content) => {
  // 假如说我把变量挂载在了global newFunction 是获取不到的
  return `(function(exports,require,module,__filename,__dirname){${content}})`;
};

// 模块静态方法
Module._extensions = Object.create(null);
// 模块缓存
Module._cache = Object.create(null);

// 策略一:模块加载js文件
Module._extensions[".js"] = function (module) {
  // 1、读取文件
  let content = fs.readFileSync(module.id, "utf8");
  // 2、给字符串包裹函数
  let str = Module.wrapper(content);
  // 3、将字符串变为函数
  let fn = vm.runInThisContext(str);
  // 4、函数执行,赋值结果
  let exports = module.exports;
  fn.call(exports, exports, myRequire, module, module.id, module.path);
  // 模块中的this是module.exports 不是 module
  // 参数:exports require module __dirname __filename
  // 这句代码执行后 会做module.exports = 'hello'
};
// 策略二:模块加载json文件
Module._extensions[".json"] = function (module) {
  let content = fs.readFileSync(module.id, "utf8");
  // 手动将读取的json字符串转化为json对象
  module.exports = JSON.parse(content);
};

// 静态方法: 解析查找文件路径
Module._resolveFilename = function (fileName) {
  // 1、解析文件路径
  let filePath = path.resolve(__dirname, fileName);

  // 2、判断文件路径是否存在,存在就返回,不存在继续解析后缀加载
  let isExists = fs.existsSync(filePath);
  if (isExists) return filePath;

  // 3、解析后缀(.js 和 .json),继续加载
  let keys = Reflect.ownKeys(Module._extensions);
  for (let i = 0; i < keys.length; i++) {
    let newFile = filePath + keys[i];
    if (fs.existsSync(newFile)) return newFile;
  }
  throw new Error("module not found");
};

/**
 * 原型定义加载方法:
 * 加载时 需要获取当前文件的后缀名 ,根据后缀名采用不同的策略进行加载
 */
Module.prototype.load = function () {
  let extensions = path.extname(this.id);
  Module._extensions[extensions](this);
};

// 自定义require方法
function myRequire(fileName) {
  // 1、解析当前的文件名
  fileName = Module._resolveFilename(fileName);

  // 判断是否有缓存,如果有直接返回exports
  if (Module._cache[fileName]) {
    return Module._cache[fileName].exports;
  }

  // 2、创建模块
  let module = new Module(fileName);

  // 给模块添加缓存
  Module._cache[fileName] = module;

  // 3、加载模块
  module.load();

  // 4、返回结果
  return module.exports;
}

// 3、引入测试
let r = myRequire("./a");
myRequire("./a");
myRequire("./a");
myRequire("./a");
myRequire("./a");
console.log(r);

4、模块加载顺序

  • 查找顺序--内置模块的优先级是最高的,永远是优先查找
    1. 先查找当前文件夹下的 js 文件
    2. 如果 JS 查找不到,就查找 json 文件
    3. 如果 json 查找不到,就查找 package.json 中 main 字段,找到对应结果
    4. 如果 main 查找不到,就查找 index.js
  • require 加载方式为同步加载,需要加载完成才能执行后续操作
  • 正确用法
    1. exports.a
    2. module.exports.a
    3. module.exports
    4. global

5、循环引用的问题

模块 A 加载模块 B,模块 B 又加载模块 A,解决方案就是: 先缓存,在单独加载

模块 a

js
let moduleB;

module.exports = {
  saveModule(b) {
    moduleB = b;
  },
  say() {
    console.log("a的say");
  },
  init() {
    moduleB.say();
  },
};

模块 b

js
let moduleA;
module.exports = {
  saveModule(a) {
    moduleA = a;
  },
  say() {
    console.log("b的say");
  },
  init() {
    moduleA.say();
  },
};

引用部分

js
const useA = require("./module-a");
const useB = require("./module-b");

// 先将模块互相保存
useA.saveModule(useB);
useB.saveModule(useA);
// 再去初始化
useA.init();
useB.init();

Released under the MIT License.