模块
- es6Module 静态导入 在编译的时候就可以知道使用了哪些变量 可以实现tree-shaking
- commonjs 动态导入 不支持tree-shaking
commonjs规范
- 想使用哪个模块就require谁 (后缀可以省略,默认会查找.js文件,没有js找.json)
- 想被别人使用需要导出 module.exports
- 在node中每一个js/json文件就是一个模块
- 一个包中包含多个模块(每个包都必须配置一个package.json文件)
原理: 读取文件 => 包装自执行函数,设置参数 => 默认返回module.exports对象
运行流程分析
let a = require(“./del”);
- mod.require(path) -> Module.prototype.require
- Module._load 加载模块
- Module._resolveFilename 方法就是把路径变成绝对路径 添加后缀名
- 实现模块的缓存(根据绝对路径进行模块的缓存)
- module.exports 会在第一次缓存起来(导出的结果如果是一个对象内部属性变了会跟着变, 普通值不会变)后续再去使用的话会取上次的返回值
- es6模块使用export {} 导出的是一个变量,如果内部对应的值发生变化 导出的是会跟着变的
- 会尝试加载是不是一个原生模块,如果带相对路径或者绝对路径就不是核心模块
- 拿到绝对路径,创造一个模块 new Module。
几个重要的属性: this.id this.exports = {} this.path 父路径 - module.load 对模块进行加载
- 根据文件后缀 去做策略加载
- 用的是同步读取
- 增加一个函数的壳子,并让函数执行,让 module.exports 作为了 this
- 用户会默认拿到 module.exports 的返回值
- 最终返回的是 exports 对象
node 中 commonjs 规范的简单实现
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function Module(id) {
// 传入的id是文件的绝对路径
this.id = id;
this.exports = {};
}
Module._cache = {};
Module._extensions = {
'.js'(module) {
// 读取脚本
let script = fs.readFileSync(module.id, 'utf8');
// 包装函数,生成字符串
let template = `(function(exports, module, require, __dirname, __filename){${script}})`;
// 字符串变成函数
let fn = vm.runInThisContext(template); // 相当于 new Function
// 函数执行,将this指向module.exports
let exports = module.exports;
let thisValue = exports; // this = module.exports = exports
let filename = module.id;
let dirname = path.dirname(filename);
// 函数执行,调用了模块,也就完成了 module.exports的赋值
fn.call(thisValue, exports, module, req, dirname, filename);
},
'.json'(module) {
// 获取文件内容
let script = fs.readFileSync(module.id, 'utf8');
// 导出
module.exports = JSON.parse(script);
},
};
// 返回require文件的绝对路径
Module._resolveFilename = function (id) {
let filePath = path.resolve(__dirname, id);
console.log(filePath, 'filePath');
// 通过判断filePath这个路径存不存在来看传入的路径有没有加后缀
let isExists = fs.existsSync(filePath);
if (isExists) return filePath;
// 不存在 则尝试添加后缀
let keys = Reflect.ownKeys(Module._extensions); // 拿到所有的key
for (let i = 0; i < keys.forEach.length; i++) {
let newPath = filePath + keys[i];
if (fs.existsSync(newPath)) return newPath;
}
throw new Error('module not found');
};
// 加载模块,让用户给module.exports赋值
Module.prototype.load = function () {
// 不需要传任何参数,this就指向创建的module实例
// 先取文件后缀名
let ext = path.extname(this.id);
// 根据后缀名,采用不同的策略去加载
Module._extensions[ext](this);
};
/*
自实现的require方法
filename是传入的文件路径
*/
function req(filename) {
// 返回绝对路径,并且加上后缀
filename = Module._resolveFilename(filename);
if (Module._cache[filename]) {
return Module._cache[filename].exports;
}
// 创造一个模块
const module = new Module(filename);
// 增加缓存
Module._cache[filename] = module;
// 对模块进行加载
module.load(); // 就是让用户给module.exports赋值
// 最终导出module.exports
return module.exports; // 默认是空对象
}
// let a = require("./del");
let a = req('./del');
console.log(a, '输出');
复制代码
⚠️注意点
this = module.exports = exports
指向的是同一个引用地址 如果将exports更改了 module.exports不会变
- 导出的时候 module.exports = “aaa”
- 不能 exports = “aaa”;
- 但是可以这样导出 exports.a = “111” this.b = “222”;(因为没有改变引用地址,和 module.exports 指向同样的引用地址)
- 小结:最终用户使用的结果都是来自于
module.exports
; 不要同时使用exports和module.exports否则会以module.exports结果为基准
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END