4.node 模块化
Opened this issue · 0 comments
Zijue commented
为什么需要模块化
模块本质上是封装,把模块内可访问的和不可以访问的区分得清清楚楚。同时为了解决冲突,实现高内聚低耦合
node 的模块化机制
node 遵循了 CommonJS 的模块规范来隔离每个模块的作用域。与 ES6 模块规范对比
- CommonJS 依赖于 node 的特性,可以按需依赖,无法实现 tree-shaking
- ES6 模块只能静态依赖,可以实现 tree-shaking
CommonJS 规范
- 每一个文件都是一个模块
- 需要通过
module.exports
导出需要给别人使用的值 - 通过
require
拿到需要的结果
CommonJS 规范中分三种模块
- 内置模块和核心模块:node 中自带的不需要安装,引用的时候不需要添加路径
- 第三方模块
- 自定义模块、文件模块
下面介绍三个内置模块便于后面学习 CommonJS 规范实现原理
const fs = require('fs');
let r1 = fs.readFileSync('./1.js', 'utf8'); // 同步读取文件内容
let r2 = fs.existsSync('./1.js'); // 同步判断是否存在的方法
const path = require('path');
console.log(path.resolve(__dirname, '1.js')); // 解析出一个绝对路径,默认以 process.cwd() 解析
console.log(path.join(__dirname, 'a', 'b')); // join 和 resolve 可以互换使用,但是‘/’不能使用 resolve,会跑到根路径下
console.log(path.resolve(__dirname, 'a', '/', 'b')); // 输出结果为‘/b’
console.log(path.join(__dirname, 'a', '/', 'b')); // 输出结果为‘xx/xx/xx/a/b’
console.log(path.extname('a.min.js')); // .js 获取文件扩展名
console.log(path.relative('a/', 'a/b/1.js')); // b/1.js 相减取差异的部分
console.log(path.dirname('a/b.js')); // a 获取目录名
const vm = require('vm');
let a = 1;
vm.runInThisContext('console.log(a)'); // ReferenceError: a is not defined
a = 1; // 等同于 global.a = 1;
vm.runInThisContext('console.log(a)'); // 1
// vm 运行方式同 new Function,但是不需要把字符串包装成函数。Function 与 eval 不同的是,Function 创建的函数只能在全局作用域中运行
使用 vscode 调试 node 代码
点击 debugger 按钮先建 node 调试文件 launch.json
// launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**" // 该项表示跳过 node 源代码,需要查看源代码时应删除
],
"program": "${workspaceFolder}/temp.js"
}
]
}
CommonJS 规范实现原理
通过调试源码梳理 require 模块引入代码执行顺序如下
- require 是一个 Module 原型上的方法 Module.prototype.require
- Module._load 加载方法(模块加载)
- Module._resolveFilename(解析文件名变成绝对路径并且带有后缀)
- new Module 创建一个模块 (id, exports) require 方法获取到的是 module.exports 属性
- Module.prototype.load 进行模块的加载
- js 模块 json 模块 根据不同的文件扩展名使用不同的策略去进行模块的加载
- fs.readFileSync 读取文件的内容
- module._compile 将内容包裹进一个函数
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
- 让文件执行,用户会给 exports 赋值
- 最终获取到的是 module.exports 的结果
核心原理流程
- 读取文件
- 创建 exports 空对象,并传给用户
- 用户赋值后返回
手写 require 模块原理实现
const path = require('path');
const fs = require('fs');
const vm = require('vm')
function Module(id){
this.id = id;
this.exports = {}
}
// 策略模式:根据不同的后缀,定义解析规则
Module._extensions = {
'.js'(module){
let script = fs.readFileSync(module.id, 'utf-8'); // 读取文件内容
let code = `(function(exports, require, module, __filename, __dirname){
${script}
})`;
let func = vm.runInThisContext(code);
let exports = module.exports;
let thisVal = exports;
let dirname = path.dirname(module.id);
func.call(thisVal, exports, req, module, module.id, dirname);
},
'.json'(){}
}
Module._resolveFilename = function(id){
let filepath = path.resolve(__dirname, id);
// 查看该文件是否存在,如果不存在尝试添加后缀
let isExists = fs.existsSync(filepath);
if (isExists) return filepath; // 文件存在则直接返回
let keys = Object.keys(Module._extensions)
for (let i = 0; i < keys.length; i++) {
let newFilepath = filepath + keys[i];
if(fs.existsSync(newFilepath)) return newFilepath
}
// 循环结束仍未返回文件路径,说明文件不存在
throw new Error('模块文件不存在')
}
Module.prototype.load = function(){
// 核心的加载方法,根据文件不同的后缀名进行加载
let extname = path.extname(this.id);
Module._extensions[extname](this);
}
Module._load = function(id){
let filename = Module._resolveFilename(id); // 就是将用户的路径变成绝对路径
let module = new Module(filename);
module.load(); // 内部会读取文件,用户会给 exports 对象赋值
return module.exports
}
// 自定义 require 方法
function req(id){ // 根据用户名加载模块
return Module._load(id)
}
const r = req('./1');
console.log(r); // > zijue
<--------------------------------->
// 1.js
module.exports = 'zijue'
给模块导入加缓存
Module._cache = {}
Module._load = function(id){
let filename = Module._resolveFilename(id); // 就是将用户的路径变成绝对路径
if(Module._cache[filename]){
return Module._cache[filename].exports // 如果有缓存直接将上次缓存的结果返回
}
let module = new Module(filename);
Module._cache[filename] = module;
module.load(); // 内部会读取文件,用户会给 exports 对象赋值
console.log('no cache')
return module.exports
}
添加 .json
导入方法
Module._extensions = {
'.js'(module){
...
},
'.json'(module){
let script = fs.readFileSync(module.id, 'utf-8'); // 读取文件内容
module.exports = JSON.parse(script); // 文件中没有 module.exports,直接手动赋值
}
}
module.exports 与 exports 的关系
在源码 let exports = module.exports
中 module.exports 和 exports 指向同一个引用类型 {}
,故 exports = [newVal]
并不会改变 module.exports
的空间
// 1.js
exports = 'zijue'
// test.js
...
const r = req('./1');
console.log(r); // > {}
// <------------------------>
exports.a = 'zijue'
// test.js
...
const r = req('./1');
console.log(r); // > { a: 'zijue' }
// <------------------------>
// module.exports 和 exports 同时写
module.exports = 'zijue 1'
exports = 'zijue 2'
// test.js
...
const r = req('./1');
console.log(r); // > zijue 1
// module.exports = exports = {} ==> module.exports = 'zijue 1' ==> exports = 'zijue 2'
由此引出另一个问题:所有相关模块中的 global
都是同一个,尽量不要使用 global
,可能会污染全局变量;但是有些例外,比如:数据库连接属性 conn
ES6 中既可以使用 export default 也可以使用 exports;ES6中使用exports {a}
方式导出的是一个变量a
,当使用import {a}
时取值。