模块加载
基本知识
Node中的模块分为以下几类:
- 核心模块, 如http fs path等
- 以 . 或者 .. 开始的相对路径文件
- 以 / 开始的绝对路径文件
- 非路径形式的文件模块, 如自定义的connect模块
在模块加载时,Node会按照 .js .json .node的次序补足扩展名,依次尝试。对于第四种的自定义模块,Node在加载时会从当前文件目录下的node_modules文件开始,依次遍历父文件夹进行查找。 项目的目录为test,通过module.paths
可以知道模块查找时可能遍历的路径:
require源码解析
在Node中,需通过 var module = require("module")
这种形式调用模块,其内部实现逻辑如下:
// 模块加载入口
Module._load = function(request, parent, isMain) {
// 返回文件名 会调用到__findpath
var filename = Module._resolveFilename(request, parent);
// 有缓存直接返回缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
...
// 加载文件
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
return module.exports;
};
// 根据参数 返回文件名, _findPath的逻辑是
// 1. 若模块的路径不以 / 结尾,则先检查该路径是否真实存在:
// 2. 若存在且为一个文件,则直接返回文件路径作为结果。
// 3. 若存在且为一个目录,则尝试读取该目录下的 package.json 中 main 属性所指向的文件路径。
// 4. 判断该文件路径是否存在,若存在,则直接作为结果返回。
// 5. 尝试在该路径后依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径。
// 6. 尝试在该路径后依次加上 index.js index.json 和 index.node,判断是否存在,若存在则返回拼接后的路径。
// 7. 若仍未返回,则为指定的模块路径依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径
Module._resolveFilename = function(request, parent) {
...
var filename = Module._findPath(request, paths);
...
return filename;
};
// 加载一个文件
Module.prototype.load = function(filename) {
...
Module._extensions[extension](this, filename);
...
};
// 以.js结尾的文件为例 load函数 会执行到_compile方法中去
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
Module.prototype._compile = function(content, filename) {
// 包裹脚本
var wrapper = Module.wrap(content);
var compiledWrapper = runInThisContext(wrapper,{ filename: filename, lineOffset: 0 });
...
// 执行逻辑
const args = [this.exports, require, this, filename, dirname];
const result = compiledWrapper.apply(this.exports, args);
return result;
};
// Module.wrap的逻辑 把脚本前后包括起来,形成一个函数
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
通过对模块加载流程的梳理,可知module是对象,而require是函数;且它俩实际上是一个函数的参数,并不是全局属性:
console.log(require) // Function
console.log(module) // Object
console.log(global.require) // undefined
console.log(global.module) // undefined
模块加载的问题
我们假设的场景是:test和test1是一个大工程的两个子工程,为了维护整个工程中模块的统一(版本一致、同步更新等),我们希望一个模块在工程中只有一份代码存在。两个子工程的文件夹名称和工程一样。如果在开发时,test工程想引用test1工程下的模块,那么我们可以采用如下方法:
var moduleA = require("../test1/node_modules/moduleA")
这种方式有两个问题: 1. 写法比较丑 2. 难以维护。尤其是第二条,在真正的开发时,这是个比较令人头疼的地方,因此需要一个比较好的解决方法。
上述采用的是相对路径,我们可以通过全局的绝对路径进行实现:
global._rootTest1 = '/Users/ahu/test1/node_modules';
var path = require('path');
var moduleA = require(path.join(_rootTest1,'moduleA'));
这个和相对路径类似,有点换汤不换药的感觉,但也不失为一种方法。
模块加载优化
普通第三方模块加载只需要require进来就好,没有路径的问题;因此我们以此为目标考虑我们模块的加载优化。
var moduleA = require('moduleA');
修改module.paths
我们知道module.paths
是模块查找时要遍历的文件夹路径,如果往其中添加模块所在的路径,那么就可以直接通过模块名加载到模块了:
module.paths.push('/Users/ahu/test1/node_modules');
console.log(module.paths);
var moduleA = require('moduleA');
在考虑效率的情况下,需要依据路径下模块数决定其在
module.paths
的位置,数组位置越靠前,模块加载的优先级越高。
虽然基本目的达到了,但是对其原理不是很了解。我们从模块加载的源码进行探索:
// 初始化全局的依赖加载路径
Module._initPaths = function() {
...
var paths = [path.resolve(process.execPath, '..', '..', 'lib', 'node')];
...
// 我们需要着重关注此处,获取环境变量“NODE_PATH”
var nodePath = process.env['NODE_PATH'];
if (nodePath) {
paths = nodePath.split(path.delimiter).concat(paths);
}
// modulePaths记录了全局加载依赖的根目录,在Module._resolveLookupPaths中有使用
modulePaths = paths;
};
// @params: request为加载的模块名
// @params: parent为当前模块(即加载依赖的模块)
Module._resolveLookupPaths = function(request, parent) {
...
var start = request.substring(0, 2);
// 若为引用模块名的方式,即require('moduleA')
if (start !== './' && start !== '..') {
// 此处的modulePaths即为Module._initPaths函数中赋值的变量
var paths = modulePaths;
if (parent) {
if (!parent.paths) parent.paths = [];
paths = parent.paths.concat(paths);
}
return [request, paths];
}
...
};
通过Node module加载的源码可知,影响模块加载的有以下几点:
- NODE_PATH这个环境变量
- Module的_initPaths方法,只执行一次
基于这两点我们进行尝试。
NODE_PATH
可以修改系统环境变量中的NODE_PATH,需要保证开发、测试、发布环境同步进行修改,比较麻烦;而且由于影响范围较大,可能影响程序的正常运行:
export NODE_PATH=/Users/ahu/test1/node_modules
也可以在服务启动时修改NODE_PATH,如下方式:
NODE_PATH=/Users/ahu/test1/node_modules node test.js
这个影响访问小,发布环境中借组启动脚本可以比较优雅的实现,但是开发时有可能比较麻烦。
process.env
除了上面两种,可以在程序中修改process.env中的NODE_PATH进行实现,但是由于_initPaths只执行一次而且已经执行完毕,因此需要重新执行一边:
process.env.NODE_PATH='/Users/ahu/test1/node_modules';
require('module').Module._initPaths();
var moduleA = require('moduleA');
总结
本文提出的优化方法都是对 NODE_PATH 进行修改,包括对系统环境变量和程序运行环境变量修改两方面。app-module-path这个模块也通过类似的方法进行实现。
参考文章
module源码
nativeModule源码
通过源码解析 Node.js 中一个文件被 require 后所发生的故事
node模块加载层级优化