当我们在 Node.js 中调用一个模块时,会发生什么呢?

首先,定义一个入口文件

1
2
// entry.js
require('./module.js')

然后,再定义一个模块文件

1
2
// module.js
module.export = 123;

这时在 WebStorm 中,在 require(‘./module.js’) 这一行添加一个断点,然后 debug 调试,进入到 Node.js 的 lib/module.js 文件中,这个文件是负责加载模块。(WebStorm 如何调试 Node.js,请查看官方指南

Node.js 会调用 Module.prototype.require 来加载我们的 module.js,而它又会调用 Module._load。

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
// 在给定的文件路径加载模块。返回该模块的 `exports` 属性。
Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};

// 检查所请求文件的缓存。
// 1. 如果模块已经存在于缓存中:返回 exports 对象。
// 2. 如果模块是 Node.js 模块:则调用 `NativeModule.require()` 并传入文件名,然后返回结果。
// 3. 否则,为该文件创建一个新模块并将其保存到缓存中。然后在返回 exports 对象之前加载文件内容。
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent, isMain); // 1. 根据 request 解析最终完整的文件名

var cachedModule = Module._cache[filename]; // 2. 查找 Module._cache 是否缓存过此文件名
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports; // 2.1 缓存过直接返回
}

// 不调用 updateChildren(),Module 构造函数内部会调用它。
var module = new Module(filename, parent); // 3. 未缓存则创建一个初始模块(注意,此时并未加载模块)

Module._cache[filename] = module; // 4. 将我们创建的初始模块,挂载到 Module._cache 对象上

tryModuleLoad(module, filename); // 5. 尝试加载初始模块

return module.exports;
};

我们接着看第 5 步的 tryModuleLoad 函数,其内部调用了 module.load(filename)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename]; // 加载失败后,删除 Module._cache 对象上面的模块缓存
}
}
}

// 给定一个文件名,将它传递给合适的扩展处理函数。
Module.prototype.load = function(filename) {
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));

var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true; // 注意,当 Module._extensions[extension] 函数执行完成后,模块的 loaded 属性就标记为 true
};

而 Module._extensions[extension](this, filename),又会调用 Module._extensions[‘.js’] 函数,函数内部以 UTF-8 编码读取文件

1
2
3
4
5
// Node.js 自带的 .js 扩展处理函数
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8'); // 以 UTF-8 编码读取文件
module._compile(internalModule.stripBOM(content), filename);
};

继续深入发现,这里会调用 module._compile

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
// 在正确的作用域或沙箱中运行文件内容。向模块文件暴露正确的辅助变量(require,module,exports)。如果有异常,返回异常。
Module.prototype._compile = function(content, filename) {

content = internalModule.stripShebang(content);

// 创建外层容器函数(wrapper function)
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
}); // 将模块文件编译为一个函数

var inspectorWrapper = null;
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction(this);
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports, require, this, filename, dirname);
} else {
result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); // 直到这里,才开始调用模块文件函数,将辅助变量(this.exports, this.exports, require, this, filename, dirname)传入。
}
if (depth === 0) stat.cache = null;
return result;
};

调用结束后,我们退回到 Module._load 函数,发现这里返回了 module.exports。

这里我们捎带提一句,默认 Node.js 的模块系统已经通过声明 exports 对象,开辟出一块内存。如果我们在模块文件中,只操作 exports 对象,则返回的 module.exports 其实指向的还是我们默认开辟出的那块内存;然而,当我们在模块文件中,既操作 exports 又操作 module.exports(其中操作 exports 是在操作 Node.js 那块原有默认内存,操作 module.exports 是等于将 module 对象的 exports 属性指向一块新的内存),则在 Module._load 函数中只会返回我们新开辟的内存中存着的对象。

这里是我保留了重要内容的最终文件

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// https://github.com/nodejs/node/blob/master/lib/module.js
'use strict';

module.exports = Module;

function updateChildren(parent, child, scan) {
var children = parent && parent.children;
if (children && !(scan && children.includes(child)))
children.push(child);
}

function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}

Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};

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

// given a module name, and a list of paths to test, returns the first
// matching file in the following precedence.
//
// require("a.<ext>")
// -> a.<ext>
//
// require("a")
// -> a
// -> a.<ext>
// -> a/index.<ext>

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
// filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent, isMain);

var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}

// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent);

Module._cache[filename] = module;

tryModuleLoad(module, filename);

return module.exports;
};

function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}

Module._resolveFilename = function(request, parent, isMain, options) {
if (NativeModule.nonInternalExists(request)) {
return request;
}

var paths;

if (typeof options === 'object' && options !== null &&
Array.isArray(options.paths)) {
const fakeParent = new Module('', null);

paths = [];

for (var i = 0; i < options.paths.length; i++) {
const path = options.paths[i];
fakeParent.paths = Module._nodeModulePaths(path);
const lookupPaths = Module._resolveLookupPaths(request, fakeParent, true);

if (!paths.includes(path))
paths.push(path);

for (var j = 0; j < lookupPaths.length; j++) {
if (!paths.includes(lookupPaths[j]))
paths.push(lookupPaths[j]);
}
}
} else {
paths = Module._resolveLookupPaths(request, parent, true);
}

// look up the filename first, since that's the cache key.
var filename = Module._findPath(request, paths, isMain);
if (!filename) {
var err = new Error(`Cannot find module '${request}'`);
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};


// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);

assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));

var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;

if (experimentalModules) {
const ESMLoader = internalESModule.ESMLoader;
const url = getURLFromFilePath(filename);
const urlString = `${url}`;
const exports = this.exports;
if (ESMLoader.moduleMap.has(urlString) !== true) {
ESMLoader.moduleMap.set(
urlString,
new ModuleJob(ESMLoader, url, async () => {
const ctx = createDynamicModule(
['default'], url);
ctx.reflect.exports.default.set(exports);
return ctx;
})
);
} else {
const job = ESMLoader.moduleMap.get(urlString);
if (job.reflect)
job.reflect.exports.default.set(exports);
}
}
};


// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};


// Resolved path to process.argv[1] will be lazily placed here
// (needed for setting breakpoint when called with --inspect-brk)
var resolvedArgv;


// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {

content = internalModule.stripShebang(content);

// create wrapper function
var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});

var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
if (!resolvedArgv) {
// we enter the repl if we're not given a filename argument.
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} else {
resolvedArgv = 'repl';
}
}

// Set breakpoint on module start
if (filename === resolvedArgv) {
delete process._breakFirstLine;
inspectorWrapper = process.binding('inspector').callAndPauseOnStart;
}
}
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction(this);
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
require, this, filename, dirname);
} else {
result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
}
if (depth === 0) stat.cache = null;
return result;
};


// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(internalModule.stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};


//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path.toNamespacedPath(filename));
};

if (experimentalModules) {
Module._extensions['.mjs'] = function(module, filename) {
throw new errors.Error('ERR_REQUIRE_ESM', filename);
};
}

// bootstrap main module.

Module._initPaths = function() {
const isWindows = process.platform === 'win32';

var homeDir;
var nodePath;
if (isWindows) {
homeDir = process.env.USERPROFILE;
nodePath = process.env.NODE_PATH;
} else {
homeDir = safeGetenv('HOME');
nodePath = safeGetenv('NODE_PATH');
}

// $PREFIX/lib/node, where $PREFIX is the root of the Node.js installation.
var prefixDir;
// process.execPath is $PREFIX/bin/node except on Windows where it is
// $PREFIX\node.exe.
if (isWindows) {
prefixDir = path.resolve(process.execPath, '..');
} else {
prefixDir = path.resolve(process.execPath, '..', '..');
}
var paths = [path.resolve(prefixDir, 'lib', 'node')];

if (homeDir) {
paths.unshift(path.resolve(homeDir, '.node_libraries'));
paths.unshift(path.resolve(homeDir, '.node_modules'));
}

if (nodePath) {
paths = nodePath.split(path.delimiter).filter(function(path) {
return !!path;
}).concat(paths);
}

modulePaths = paths;

// clone as a shallow copy, for introspection.
Module.globalPaths = modulePaths.slice(0);
};

Module._initPaths();