模块的查找
绝对路径
直接去 绝对路径 找模块加载,找不到则报错;
相对路径
相对于当前模块,将 相对路径 (./a、../a)转化成 绝对路径 加载模块;
模块名(mono-repo)
先检查是否是内置模块(如:fs、path 等),如果是直接使用;
检查当前目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块,找不到继续查找;
检查上级目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块,找不到继续查找;
…
找到顶级后,找到则加载,找不到则报错;
没有后缀的文件
若模块没有后缀名,则会按照 模块.js、模块.json、模块.node(用不到)、模块.mjs 的顺序加载;
目录
如果仅提供目录,不提供文件名,则自动寻找该目录下的 index.js
包名
如果仅提供 包名 ,若当前目录中的 node_modules 包含 包名 这个模块,会找 包名 下的 package.json 入口文件中的 main 字段去加载这个文件,找到后转成 绝对路径 加载模块,找不到继续查找;
检查上级目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块;
…
找到顶级后,找到则加载,找不到则报错;
module 对象
-
a.js
module.exports = 1111 console.log(module)
-
node a.js
的输出结果// 记录了当前模块的信息 Module { // id:返回模块标识符,一般是一个绝对路径,如果是入口文件则是一个 . id: '.', // 当前模块的目录 path: '/Users/wushuai/Desktop/node基础/05Module', // 返回当前模块需要暴露的内容 exports: 'lg', // 存放调用当前模块的父模块 parent: {}, // 当前模块的文件名称,也是绝对路径; filename: '/Users/wushuai/Desktop/node基础/05Module/m.js', // loaded:返回布尔值,表示模块是否完成加载; loaded: false, // 当前模块调用的子模块 children: [], // 返回当前模块所在目录和上级所有目录拼接 node_modules 的绝对路径,可以用于分析 Node 中模块加载的具体位置 paths: [ '/Users/wushuai/Desktop/node基础/05Module/node_modules', '/Users/wushuai/Desktop/node基础/node_modules', '/Users/wushuai/Desktop/node_modules', '/Users/wushuai/node_modules', '/Users/node_modules', '/node_modules' ] }
require 函数
-
a.js
console.log(require);
-
node a.js
的输出结果[Function: require] { // resolve 方法会把路径转换成 绝对路径 resolve: [Function: resolve] { paths: [Function: paths] }, // 主模块,入口模块 main: Module { id: '.', path: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src', exports: {}, parent: null, filename: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js', loaded: false, children: [], paths: [ '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/node_modules', '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/node_modules', '/Users/wushuai/Desktop/frontend-node-master/node_modules', '/Users/wushuai/Desktop/node_modules', '/Users/wushuai/node_modules', '/Users/node_modules', '/node_modules' ] }, // 扩展名 extensions: [Object: null prototype] { '.js': [Function], '.json': [Function], '.node': [Function], '.mjs': [Function] }, // 目前已经缓存的模块 cache: [Object: null prototype] { '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js': Module { id: '.', path: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src', exports: {}, parent: null, filename: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js', loaded: false, children: [], paths: [Array] }, } }
模块加载流程
// 伪代码演示
function require(modulePath) {
// 1. 将 modulePath 转换为绝对路径:D:\repository\NodeJS\源码\myModule.js
// 2. 判断 require.cache 中该模块是否已有缓存
if (require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"]) {
return require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"].result;
}
// 3. 读取文件内容
console.log("当前模块路径:", __dirname);
console.log("当前模块文件:", __filename);
exports.c = 3;
module.exports = {
a: 1,
b: 2
};
this.m = 5;
// 4. 包裹到一个函数中
function __temp(exports, require, module, __dirname, __filename) {
console.log("当前模块路径:", __dirname);
console.log("当前模块文件:", __filename);
exports.c = 3;
module.exports = {
a: 1,
b: 2
};
this.m = 5;
}
// 5. 创建 module 对象
module.exports = {};
const exports = module.exports; // exports 是 module.exports 的引用地址
// 6. 缓存加载过的模块
require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"] = module.exports;
// 7. 编译执行
__temp.call(module.exports /* this 指向 */, exports, require, module, module.path, module.filename);
// 8. 返回模块
return module.exports;
}
手写模块加载
本例以文件模块为例,模拟实现 Nodejs 的模块加载流程,以了解 Nodejs 的加载规则
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module(id) {
this.id = id
this.exports = {}
}
Module._resolveFilename = function (filename) {
// 利用 Path 将 filename 转为绝对路径
let absPath = path.resolve(__dirname, filename)
// 只完成主要逻辑
// 判断当前路径对应的内容是否存在
if (fs.existsSync(absPath)) {
// 如果条件成立则说明 absPath 对应的内容是存在的
return absPath
} else {
// 如果不存在
// 文件定位(尝试补足不同的后缀重新判断)
// 先 .js .json .node补足,继续找
// 若没有找到,则当成一个目录,找 package.json 中的 main 对应的路径继续补足后缀,还是不存在,则继续找 index,没有继续找
// 把每一个 node module 都按照以上步骤找一遍
// 这里只进行简单判断
let suffix = Object.keys(Module._extensions)
for (var i = 0; i < suffix.length; i++) {
let newPath = absPath + suffix[i]
if (fs.existsSync(newPath)) {
return newPath
}
}
}
// 找不到抛出错误
throw new Error(`${filename} is not exists`)
}
Module._extensions = {
'.js'(module) {
// 读取
let content = fs.readFileSync(module.id, 'utf-8')
// 包装
content = Module.wrapper[0] + content + Module.wrapper[1]
// VM
let compileFn = vm.runInThisContext(content)
// 准备参数的值
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用,expoets 默认就是 {},这也是为什么在 模块中打印 this 输出的是一个 {}
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module) {
let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
module.exports = content
},
// ......
}
Module.wrapper = [ "(function (exports, require, module, __filename, __dirname) {", "\n})" ]
Module._cache = {}
Module.prototype.load = function () {
// 后缀名字
let extname = path.extname(this.id)
Module._extensions[extname](this)
}
function myRequire(filename) {
// 1 绝对路径
let mPath = Module._resolveFilename(filename)
// 2 缓存优先
let cacheModule = Module._cache[mPath]
if (cacheModule) return cacheModule.exports
// 3 创建空对象加载目标模块
let module = new Module(mPath)
// 4 缓存已加载过的模块
Module._cache[mPath] = module
// 5 执行加载(编译执行)
module.load()
// 6 返回数据
return module.exports
}
let obj = myRequire('./js文件') // 引入 js文件.js
let obj2 = myRequire('./json文件') // 引入 json文件.json
面试题
输出结果
-
执行下面的代码,示例一
console.log("当前模块路径:", __dirname); console.log("当前模块文件:", __filename); // exports 是 module.exports 的引用 exports.c = 3; // module.exports 重新赋值,切断了 exports 的联系,module.exports 是新的引用(内存地址) module.exports = { a: 1, b: 2 }; // this 的指向是 exports this.m = 5; console.log(this === module.exports); // false,module.exports 重新赋值,是新的引用(内存地址) console.log(this === exports); // true console.log(module.exports); // { a: 1, b: 2 },重新赋值后的值
-
执行下面的代码,示例二
console.log("当前模块路径:", __dirname); console.log("当前模块文件:", __filename); // exports 是 module.exports 的引用 exports.a = 1; exports.b = 2; exports.c = 3; // this 的指向是 exports this.m = 5; console.log(this === module.exports); // true console.log(this === exports); // true console.log(module.exports); // { a: 1, b: 2, c: 3, m: 5 }
CommonJS 的循环引用
-
分析下列这段代码,并解释原理
JSJSJS//main.js var a = require('./a') console.log(a)
// a.js module.exports.a = 1 var b = require('./b') console.log(b) module.exports.a = 2
// b.js module.exports.b = 11 var a = require('./a') console.log(a) module.exports.b = 22
-
require 后的模块是会被缓存的,还需要注意的是先加入缓存,然后再执行,这样在按照代码同步的执行顺序去分析代码就会很清晰,具体分析如下:
- 执行
require('./a')
会将 a 模块加入缓存,然后执行 a 模块中的内容,执行权交到了 a 模块中,执行 a; - 执行第一行将缓存的 a 值赋值为 1 ,然后执行第二行
require('./b')
把 b 模块加入缓存,并把执行权交到 b 模块中; - b 模块中把 b 的值赋值为 11 ,在
require('./a')
时,是从缓存中取的值,这里就会在控制台打印 {a: 1},最后把缓存中的 b 值修改为 22 ,执行权交给上一级; - 代码执行权回到 a 模块中,这时 b 从缓存中取的值是 22,控制台中打印 { b: 22 },最后把缓存中的 a 值修改为 2 ,执行权交给上一级;
- 代码执行回到 main 模块中,这时缓存中的 a 是 2 ,控制台中打印 { a: 2 },然后代码执行完毕;
- 执行
案例🌰 搭建项目
上一篇