Appearance
Node 模块化
TIP
Node 目前采用的是 commonjs 规范编写的模块化
1、commonjs 规范
- 每个 js 文件都是一个模块 (每个文件在运行的时候都会产生一个函数)
- 模块的导出 module.exports
- 每次使用一个模块可以采用 require 方式来进行导入
2、模块的分类
- 核心模块/内置模块,不需要安装,直接引入使用,引入不需要加入相对路径和绝对路径,比如:fs http path
- 自定义模块,需要通过绝对路径或者相对路径进行引入
- 第三方模块,需要先安装,再引入使用
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赋予值 - 因为最终返回的是
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、模块加载顺序
- 查找顺序--内置模块的优先级是最高的,永远是优先查找
- 先查找当前文件夹下的 js 文件
- 如果 JS 查找不到,就查找 json 文件
- 如果 json 查找不到,就查找 package.json 中 main 字段,找到对应结果
- 如果 main 查找不到,就查找 index.js
- require 加载方式为同步加载,需要加载完成才能执行后续操作
- 正确用法
- exports.a
- module.exports.a
- module.exports
- 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();