commonjs 的 require 机制
#nodejs
目录
- 1. 基本加载过程:7 个步骤
- 2. 模块缓存机制:查看缓存和清除缓存 →
require.cache
- 3. 路径解析规则
- 4. 模块包装机制
- 5. 异常处理
- 6. 条件加载
- 7. 性能优化:比如 fs/path 等
- 8. 模块导出方式
- 9. 目录作为模块
- 10.
require.main
的使用 → 判断该模块是否是入口文件 - 11. 调试技巧
- 12. 最佳实践
- 13. 核心模块是二进制和缓存,所以很快
- 14. Node.js 中对不同扩展名文件的处理机制
1. 基本加载过程:7 个步骤
- 解析路径
- 检查==缓存==
- 创建==模块对象==
- 将模块放入==缓存==
- 加载模块
- 标记==模块对象== 为已加载
- 返回
exports 对象
// require 的基本实现原理
function require(modulePath) {
// 1. 解析路径
const absolutePath = resolveModulePath(modulePath);
// 2. 检查缓存
if (require.cache[absolutePath]) {
return require.cache[absolutePath].exports;
}
// 3. 创建模块对象
const module = {
exports: {},
loaded: false,
id: absolutePath
};
// 4. 将模块放入缓存
require.cache[absolutePath] = module;
// 5. 加载模块
loadModule(absolutePath, module, require);
// 6. 标记为已加载
module.loaded = true;
// 7. 返回 exports 对象
return module.exports;
}
2. 模块缓存机制:查看缓存和清除缓存 → require.cache
// example.js
module.exports = {
count: 0,
increment() {
this.count++;
}
};
// main.js
const module1 = require('./example');
const module2 = require('./example');
console.log(module1 === module2); // true
// 因为缓存的存在,两次 require 返回同一个对象
// 查看缓存
console.log(require.cache);
// 清除缓存
delete require.cache[require.resolve('./example')];
3. 路径解析规则
- 核心模块
- 文件模块
- npm 包
- 文件扩展名解析路径 ==顺序如下==
.js
.json
.node
- `mymodule/index.js
// 1. 核心模块
const fs = require('fs'); // 直接从 Node.js 核心模块加载
// 2. 文件模块
const myModule = require('./myModule'); // 相对路径
const config = require('/opt/config'); // 绝对路径
// 3. npm 包
const lodash = require('lodash'); // 从 node_modules 查找
// 文件扩展名解析顺序
require('./myModule');
// 依次查找:
// 1. myModule.js
// 2. myModule.json
// 3. myModule.node
// 4. myModule/index.js
4. 模块包装机制
// Node.js 实际上会将你的模块代码包装在一个函数中
(function(exports, require, module, __filename, __dirname) {
// 你的模块代码在这里
const something = require('./something');
module.exports = {
// ...
};
});
5. 异常处理
- 比如
- 处理模块不存在
- 处理模块加载错误
try {
// 处理模块不存在
const nonExistent = require('./non-existent');
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
console.log('模块未找到');
}
}
// 处理模块加载错误
try {
const badModule = require('./bad-module');
} catch (err) {
console.error('模块加载失败:', err);
}
6. 条件加载
// 根据环境加载不同的配置
const config = require(process.env.NODE_ENV === 'production'
? './config.prod'
: './config.dev');
// 动态加载模块
function loadPlugin(name) {
try {
return require(`./plugins/${name}`);
} catch (err) {
console.error(`插件 ${name} 加载失败`);
return null;
}
}
7. 性能优化:比如 fs/path 等
- ==缓存==路径
- ==预加载==模块
- ==延迟==模块
// 1. 使用路径缓存
const modulePath = require.resolve('./myModule');
const myModule = require(modulePath);
// 2. 预加载模块
const modules = {
fs: require('fs'),
path: require('path'),
util: require('util')
};
// 3. 延迟加载
let heavyModule;
function getHeavyModule() {
if (!heavyModule) {
heavyModule = require('./heavyModule');
}
return heavyModule;
}
8. 模块导出方式
module.exports
导出exports.xxx
快捷方式
// 1. module.exports 导出
module.exports = {
method1() {},
method2() {}
};
// 2. exports 快捷方式
exports.method1 = function() {};
exports.method2 = function() {};
// ❌ 3. 注意: 直接赋值 exports 无效
exports = { method: () => {} }; // 这样做是错误的
9. 目录作为模块
先找 pageage.json 中的 main 字段,然后再找对应的文件
// myModule/index.js
module.exports = {
// 模块内容
};
// myModule/package.json
{
"main": "lib/entry.js" // 指定入口文件
}
// 使用
const myModule = require('./myModule');
10. require.main
的使用 → 判断该模块是否是入口文件
// 检查模块是否为入口文件
if (require.main === module) {
// 直接运行此文件
console.log('这是主模块');
} else {
// 被其他模块引用
console.log('这是被导入的模块');
}
11. 调试技巧
// 1. 查看模块搜索路径
console.log(module.paths);
// 2. 查看已加载的模块
console.log(Object.keys(require.cache));
// 3. 模块解析路径
console.log(require.resolve('./myModule'));
// 4. 模块元信息
console.log(module);
12. 最佳实践
- 总是使用
const
声明require
- 将所有
require
语句放在文件顶部 - 使用明确的文件扩展名
- 适当使用模块缓存机制
- 注意循环依赖问题
- 正确处理异常情况
- 合理组织模块结构
- 使用
package.json
管理依赖 - 注意模块加载性能
- 遵循单一职责原则
13. 核心模块是二进制和缓存,所以很快
require('fs')
核心模块很快,因为是编译好的二进制可执行文件
- 缓存:所以很快
14. Node.js 中对不同扩展名文件的处理机制
js → json → .node → 目录 → inex.js ...
14.1. 扩展名解析优先级
// require 的扩展名解析优先级
const path = './myModule';
require(path); // 按以下顺序查找:
// 1. ./myModule.js
// 2. ./myModule.json
// 3. ./myModule.node
// 4. ./myModule/index.js
// 5. ./myModule/index.json
// 6. ./myModule/index.node
14.2. .js
文件处理
// 1. JS 文件会被包装在函数中执行
(function(exports, require, module, __filename, __dirname) {
// 你的 JS 代码
const foo = 'bar';
module.exports = { foo };
});
// 2. 支持所有 JavaScript 特性
// example.js
const fs = require('fs');
class MyClass {
constructor() {
this.data = 'test';
}
}
module.exports = {
MyClass,
async readFile(path) {
return fs.promises.readFile(path, 'utf8');
}
};
14.3. .json 文件处理
会有
json.parse
的操作
// 1. JSON 文件直接被解析为对象
// config.json
{
"host": "localhost",
"port": 3000,
"debug": true
}
// 使用 JSON 文件
const config = require('./config.json');
console.log(config.host); // localhost
// 2. JSON 文件的错误处理
try {
const badJson = require('./bad.json');
} catch (err) {
if (err instanceof SyntaxError) {
console.error('JSON 解析错误');
}
}
// 3. JSON 文件是只读的
const config = require('./config.json');
config.port = 4000; // 修改只影响内存中的副本
// 重新 require 时会获得原始文件的内容
14.4. .node
文件处理
// .node 文件是编译好的 C++ 插件
// 1. 加载 .node 文件
try {
const nativeModule = require('./addon.node');
} catch (err) {
console.error('加载 native 模块失败:', err);
}
// 2. 通常通过 node-gyp 编译
// binding.gyp
{
"targets": [{
"target_name": "addon",
"sources": [ "addon.cc" ]
}]
}
// 3. C++ 插件示例
// addon.cc
`#include` <node.h>
void Initialize(v8::Local<v8::Object> exports) {
// 导出函数和对象
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
14.5. 目录模块处理:查找顺序
总结:pageage.json 的 main 字段
→ .js
→ .json
→ .node
// 1. package.json 方式
// myModule/package.json
{
"name": "myModule",
"main": "./lib/index.js" // 指定入口文件
}
// 2. index 文件方式
// myModule/index.js
module.exports = {
// 模块内容
};
// 3. 目录模块加载顺序
const myModule = require('./myModule');
// 查找顺序:
// 1. ./myModule/package.json 中的 main 字段
// 2. ./myModule/index.js
// 3. ./myModule/index.json
// 4. ./myModule/index.node
14.6. 特殊文件处理
// 1. 处理二进制文件
const binary = require('fs').readFileSync('./file.bin');
// 2. 处理源码文件
const sourceMap = require('./file.js.map');
// 3. 处理配置文件
const tsConfig = require('./tsconfig.json');
const babelConfig = require('./.babelrc');
14.7. 自定义扩展名处理 → require.extensions
// 注册自定义扩展名处理器
require.extensions['.xyz'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
// 处理内容
module._compile(content, filename);
};
// 使用自定义扩展名
const xyzModule = require('./file.xyz');
14.8. 文件缓存机制 → require.cache
require.cache
// 1. 不同扩展名的缓存处理
const jsModule = require('./file.js');
const jsonModule = require('./file.json');
const nodeModule = require('./file.node');
// 2. 清除特定扩展名的缓存
function clearCache(extension) {
Object.keys(require.cache).forEach(key => {
if (key.endsWith(extension)) {
delete require.cache[key];
}
});
}
// 3. 监视文件变化:fs.watch
const fs = require('fs');
fs.watch('./config.json', (event, filename) => {
delete require.cache[require.resolve('./config.json')];
});
14.9. 错误处理最佳实践
function safeRequire(path) {
try {
return require(path);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
console.error(`模块 ${path} 不存在`);
} else if (err instanceof SyntaxError) {
console.error(`模块 ${path} 语法错误`);
} else {
console.error(`加载模块 ${path} 时发生错误:`, err);
}
return null;
}
}
// 使用示例
const config = safeRequire('./config.json') || defaultConfig;
const addon = safeRequire('./addon.node') || mockAddon;
14.10. 性能考虑
- 缓存:比如使用 map 缓存
- 大文件处理
- 流式读取
- 延迟处理
// 1. JSON 文件缓存
const configs = new Map();
function loadConfig(path) {
if (!configs.has(path)) {
configs.set(path, require(path));
}
return configs.get(path);
}
// 2. 大文件处理
const fs = require('fs');
const stream = fs.createReadStream('./large-file.json');
// 使用流处理大型 JSON 文件
// 3. 延迟加载
let heavyModule;
function getHeavyModule() {
if (!heavyModule) {
heavyModule = require('./heavy.node');
}
return heavyModule;
}