来源:Node.js

Nodejs模块化

Nodejs遵循两套模块化规范:CommonJS规范和ESModule规范

CommonJS规范

CJS使用require引入模块,支持四种格式:

  • 支持引入Nodejs内置模块,如http、os、fs、child_process等
  • 支持引入第三方模块,如express、md5、koa等
  • 支持引入自己编写的模块
  • 支持引入C++扩展模块(.node文件)
  • 支持引入json文件
1
2
3
4
5
const fs = require('node:fs')  // 导入Nodejs内置核心模块
const express = require('express') // 导入node_modules目录下的第三方模块
const myModule = require('./myModule.js') // 导入相对路径下的模块
const nodeModule = require('./myModule.node') // 导入扩展模块
const data = require('./data.json') // 导入json文件

使用module.exports导出模块

1
2
3
4
5
6
7
8
9
// 导出对象
module.exports = {
hello: function() {
console.log('Hello, world!');
}
}

// 导出值
module.exports = 123

ESModule规范

使用ESM模块前,需要在package.json设置"type": "module"

ESM使用import引入模块,而且import语句必须写在文件头部

1
2
3
4
5
6
// 导入默认模块
import fs from 'node:fs'
// 导入文件中的aaa模块,类似解构
import { aaa } from './index.js'
// 可以给引入起别名(bbb是原名,b是别名),防止重名
import { bbb as b } from './index2.js'

如果要引入json文件,则需要特殊处理:增加断言并指定类型为json,Node低版本不支持这一写法

前端项目中通常可以引入json文件,这是因为Vite或Webpack等拥有处理引入json的loader;而原生ESM确实不支持

1
2
3
// 实验性特性,可以用但会出警告
import data from './data.json' assert { type: "json" }
console.log(data)

也可以加载模块的整体对象,内含一个文件的所有导出

1
import * as all from 'xxx.js'

若需动态导入模块,使用import函数模式

1
2
3
4
if(true){
// 这是个异步函数
import('./test.js').then()
}

使用default导出模块

1
2
3
4
5
6
7
// export default是默认导出,一个文件中只能出现一次
export default {
name: 'test'
}

// 命名导出
export const a = 1

CJS和ESM的区别

  • CJS是基于运行时的同步加载;ESM是基于编译时的异步加载
  • CJS是可以修改值的;ESM值不可修改(可读的)
  • CJS不可以tree shaking;ESM支持tree shaking(本质是因为第一条区别)
  • CJS中顶层的this指向这个模块本身;而ESM中顶层this指向undefined

源码

处理.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 当文件后缀为.json时,进行的处理
Module._extensions['.json'] = function(module, filename) {
// 使用fs读取json文件,读到字符串
const content = fs.readFileSync(filename, 'utf8');

if (policy?.manifest) {
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}

try {
// 将字符串转成对象
setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};

处理.node文件:

1
2
3
4
5
6
7
8
9
10
// 当文件后缀为.node时,进行的处理
Module._extensions['.node'] = function(module, filename) {
if (policy?.manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};

处理.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 当文件后缀为.js时,进行的处理
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
// 首先尝试从cjsParseCache中获取已经解析过的模块源代码,如果已经缓存,则直接使用缓存中的源代码
// cjsParseCache可以看作一个封装过的WeakMap
const cached = cjsParseCache.get(module);
let content;
if (cached?.source) {
// 有缓存就直接用
content = cached.source;
cached.source = undefined;
} else {
// 否则从文件系统读取源代码
content = fs.readFileSync(filename, 'utf8');
}
// 文件是否以.js结尾
if (StringPrototypeEndsWith(filename, '.js')) {
// 读取package.json文件
const pkg = readPackageScope(filename);
// Function require shouldn't be used in ES modules.
// 如果package.json文件中设置了"type":"module",但代码中使用require
// 则抛出一个错误,提示不能在ES模块中使用require函数
if (pkg?.data?.type === 'module') {
// 获取父级parent,用于确定报错行号
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const usesEsm = hasEsmSyntax(content);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
// 如果抛出了错误,它还会尝试重构父模块的require调用堆栈,以提供更详细的错误信息
// 它会读取父模块的源代码,并根据错误的行号和列号,
// 在源代码中找到相应位置的代码行,并将其作为错误信息的一部分展示出来
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = fs.readFileSync(parentPath, 'utf8');
} catch {
// Continue regardless of error.
}
if (parentSource) {
// 报错行号
const errLine = StringPrototypeSplit(
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
err.stack, ' at ')), '\n', 1)[0];
const { 1: line, 2: col } =
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
if (line && col) {
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
const frame = `${parentPath}:${line}\n${srcLine}\n${
StringPrototypeRepeat(' ', col - 1)}^\n`;
setArrowMessage(err, frame);
}
}
}
throw err;
}
}
// 没有问题,调用compile进行编译;content源代码、filename文件路径
module._compile(content, filename);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
// 检测是否存在安全策略对象policy.manifest
const manifest = policy?.manifest;
// 如果存在policy.manifest,就是有安全策略限制需要处理
if (manifest) {
moduleURL = pathToFileURL(filename);
// 函数将模块文件名转换为URL格式
redirects = manifest.getDependencyMapper(moduleURL);
// redirects是一个URL映射表,用于处理模块依赖关系
manifest.assertIntegrity(moduleURL, content);
// manifest则是一个安全策略对象,用于检测模块的完整性和安全性
}

// 调用wrapSafe,获得全局上下文函数;filename文件路径、content源代码
const compiledWrapper = wrapSafe(filename, content, this);

let inspectorWrapper = null;
if (getOptionValue('--inspect-brk') && process._eval == null) {
if (!resolvedArgv) {
// We enter the repl if we're not given a filename argument.
if (process.argv[1]) {
try {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} catch {
// We only expect this codepath to be reached in the case of a
// preloaded module (it will fail earlier with the main entry)
assert(ArrayIsArray(getOptionValue('--require')));
}
} else {
resolvedArgv = 'repl';
}
}

// Set breakpoint on module start
if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
hasPausedEntry = true;
inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
}
}

// 组装参数
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new SafeMap();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
// Reflect.apply(代理函数调用)调用warpSafe返回的函数并填入参数
result = ReflectApply(compiledWrapper, thisValue,
[exports, require, module, filename, dirname]);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
// 返回执行结果
return result;
};

wrapSave方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function wrapSafe(filename, content, cjsModuleInstance) {
if (patched) {
// 获取包装后的代码(字符串)
const wrapper = Module.wrap(content);
// 将被warp包装后的代码放入Nodejs虚拟机的Script
// 支持ESM的模块
// import { a } from './a.js'; 类似于eval
// import()函数模式动态加载模块
const script = new Script(wrapper, {
filename,
lineOffset: 0,
// 检测有无动态import,有就处理
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});

// Cache the source map for the module if present.
if (script.sourceMapURL) {
maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);
}
// 返回执行结果
return script.runInThisContext({
displayErrors: true,
});
}

wrapSafe调用的wrap方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 把代码包装到一个函数中
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
// 如果传入的代码是var yajue = 24,就会被包装成这样:
//(function (exports, require, module, __filename, __dirname) {
// var yajue = 24
//\n});
// 相当于制作了一个不会污染全局,作用域仅限该模块内部的JS沙箱

const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n})',
];