本文内容
本文主要记录 nodeJS 中的模块系统,让我们对 node 的运行有更清晰的认识
主要参考:
模块
每个文件都被视为一个独立的模块
假设我们有两个文件
hello.js 和 world.js,内容如下:// world.js function world() { return 'world'; } exports.world = world; console.log('rerere'); // hello.js var world = require('./world'); console.log(world.world()); // node hello.js 后,输出 // rerere // worldnode xxx.js最终调用了Module._load,可以认为实际上等同于在 JS 里执行的require(xxx.js),因此我们上面的输出有两行,其中一行是执行了world.js里的一条输出语句后输出的。
require
在执行模块代码之前,Node.js 会使用一个如下的模块封装器将其封装:
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
可以看到如上有 5 个变量,在我们的模块代码里都是可以用这 5 个变量的,这些都是 node 的模块加载时注入进来的对象,下面我们来分析一下 require 的过程。
require 定义:
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(util.isString(path), 'path must be a string');
return Module._load(path, this);
};
可以看到 require 是 Module 原型上的一个属性,最终调用了 Module._load(path, this);,
Module 定义:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
Module._load 定义:
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id);
}
var filename = Module._resolveFilename(request, parent);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.exists(filename)) {
// REPL is a special case, because it needs the real require.
if (filename == 'repl') {
var replModule = new Module('repl');
replModule._compile(NativeModule.getSource('repl'), 'repl.js');
NativeModule._cache.repl = replModule;
return replModule.exports;
}
debug('load native module ' + request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
var hadException = true;
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
return module.exports;
};
- 接收 3 个参数,分别是:模块路径(相对或者绝对路径等后续可以解析到文件具体位置的一个路径)、模块的父模块、是否主模块(bool值,我们 node xxx.js 时,xxx.js 就是主模块)
var module = new Module(filename, parent);:加载模块时,为我们生成一个模块对象,这个模块对象就是我们require('模块名称')的那个模块- 最后返回
module.exports也就是模块对象的exports属性 - 加载模块逻辑在
module.load(filename)
module.load 定义:
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));
const extension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
throw new ERR_REQUIRE_ESM(filename);
}
Module._extensions[extension](this, filename);
this.loaded = true;
const ESMLoader = asyncESM.ESMLoader;
const url = `${pathToFileURL(filename)}`;
const module = ESMLoader.moduleMap.get(url);
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
// Called from cjs translator
if (module !== undefined && module.module !== undefined) {
if (module.module.getStatus() >= kInstantiated)
module.module.setExport('default', exports);
} else {
// Preemptively cache
// We use a function to defer promise creation for async hooks.
ESMLoader.moduleMap.set(
url,
// Module job creation will start promises.
// We make it a function to lazily trigger those promises
// for async hooks compatibility.
() => new ModuleJob(ESMLoader, url, () =>
new ModuleWrap(url, undefined, ['default'], function() {
this.setExport('default', exports);
})
, false /* isMain */, false /* inspectBrk */)
);
}
};
- 加载的关键:
Module._extensions[extension](this, filename);,根据扩展名来决定加载的逻辑,这里我们只看 js 的逻辑
js 加载逻辑:
Module._extensions['.js'] = function(module, filename) {
if (filename.endsWith('.js')) {
const pkg = readPackageScope(filename);
if (pkg && pkg.data && pkg.data.type === 'module') {
if (warnRequireESM) {
const parentPath = module.parent && module.parent.filename;
const basename = parentPath &&
path.basename(filename) === path.basename(parentPath) ?
filename : path.basename(filename);
process.emitWarning(
'require() of ES modules is not supported.\nrequire() of ' +
`${filename} ${parentPath ? `from ${module.parent.filename} ` : ''}` +
'is an ES module file as it is a .js file whose nearest parent ' +
'package.json contains "type": "module" which defines all .js ' +
'files in that package scope as ES modules.\nInstead rename ' +
`${basename} to end in .cjs, change the requiring code to use ` +
'import(), or remove "type": "module" from ' +
`${path.resolve(pkg.path, 'package.json')}.`,
undefined,
undefined,
undefined,
true
);
warnRequireESM = false;
}
throw new ERR_REQUIRE_ESM(filename);
}
}
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
- 关键点:
module._compile(content, filename);
module._compile 逻辑:
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
if (manifest) {
moduleURL = pathToFileURL(filename);
redirects = manifest.getRedirector(moduleURL);
manifest.assertIntegrity(moduleURL, content);
}
maybeCacheSourceMap(filename, content, this);
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]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} else {
resolvedArgv = 'repl';
}
}
// Set breakpoint on module start
if (!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 Map();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
return result;
};
- 关键点:
result = inspectorWrapper(compiledWrapper, thisValue, exports, require, module, filename, dirname); - 上面那行就是模块封装器的过程。
模块具有的 5 个变量
exports:从 module._compile 逻辑中,有一行是exports = this.exports;,而我们是使用这里的 this 指向我们要加载的模块 module,因此:exports = module.exportsrequire:传入的require函数(更深入的没有研究,实际上并不只是 Module.prototype.require)module:模块对象本身__filename:文件名(绝对路径)__dirname:模块的父目录
以上属性在每个模块 js 文件中都可以访问
require 返回的就是该 module 的 module.exports 对象,每个模块的 exports 只是 module.exports 对象的一个引用。
加载 json
一个 json 就是一个对象,可以使用 require 直接加载为 node 的一个对象,例如:
# package.json
{
"name": "node_start",
"version": "1.0.0",
"description": "",
"main": "hello.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
var package = require('./package.json');
console.log(package);
// 输出如下
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
{
name: 'node_start',
version: '1.0.0',
description: '',
main: 'hello.js',
scripts: { test: 'echo "Error: no test specified" && exit 1' },
author: '',
license: 'ISC'
}
加载其它资源
实际上还可以加载 .node 等,我目前还不打算深入这一块儿,总之 require 是可以加载很多资源的,我们也可以扩展,就像上面分析到的 module.load 是可以根据扩展来决定不同的加载逻辑的,所以我们是可以补充扩展和修改扩展逻辑的,例如 vue 应该就是使用了这种方式(没有真正研究过,猜的)。
模块的加载位置和顺序
参考:npm 模块安装机制简介
如果我们加载文件模块的时候,没有找到对应名称的文件,node 会去尝试加载添加了后缀名的文件:.js .json .node。(也就是说,如果我们加载一个没有后缀名的文件,会首先当做 JS 来加载)
- js:使用前文说的 js 加载逻辑,最后返回的是模块的 exports 对象。(module.exports)
- json:将这个 json 文件当做一个 js 对象进行返回
- node:预编译的 node 模块
文件路径:
require('/home/marco/foo.js'):加载的是绝对路径require('./circle'):加载的是相对路径(相对于执行这个 require 的文件)- 没有
/ ./ ../这些前缀的,要么加载的是核心模块,要么模块处于node_modules目录下面,会首先按照核心模块去加载,然后去node_modules去加载
node_modules 优先级:
- 应用程序目录内的
node_modules - 父目录的
node_modules - 继续找父目录的
node_modules,直到根目录 - 最后在全局安装的模块下寻找(npm get prefix 获取到的路径的 lib 目录下)
可以看出,当前目录的优先级最高,全局安装的优先级最低
目录路径:
加载一个模块的时候,如果提供的是目录,那么:
- 找到该目录的
package.json,如果不存在,就去加载index.js,不存在就去加载index.node,如果都没有这些文件,那么就加载失败,抛出异常 - 如果有
package.json,那么就加载里面main属性定义的模块,如果没有定义main,那么默认是index.js
npm install
npm install:将 devdependencies 和 dependencies 的包安装到当前目录下的node_modules目录中。npm install -g:等价于npm install --global将当前包(也就是运行这个命令所在的包),安装到全局模块中,全局模块的路径是:(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ npm get prefix /usr/local 因此全局模块路径是 /usr/local/libnpm install --production:等价于NODE_ENV=production npm install,添加了--production或者 NODE_ENV 环境变量为 production,则不会下载 devdependencies 的依赖了。参数:
- -P, --save-prod:这个是默认行为,安装的内容会加入到 dependencies
- -D, --save-dev:安装内容会加到 devDependencies
- -O, --save-optional:安装内容会加到 optionalDependencies
- --no-save:只是安装到
node_modules但是不加入到package.json了
过程:
- 发出
npm install命令 - npm 向 registry 查询模块压缩包的网址
- 下载压缩包,存放在
~/.npm目录 - 解压压缩包到当前项目的
node_modules目录