导读
文章不介绍commonjs细节。会分两部分去介绍node模块加载的原理,第一部分是抽象模型,这部分是希望用最简单的语言去概括node模块运行状态。第二部分是对第一部分的补充,需要结合源码去分析其中的细节。
术语约定
- Node编译时:指node安装时候编译的过程
- Node启动时:指通过node命令开启一个node进程的过程
- Node运行时:指node启动后,node脚本运行的过程
1. 抽象模型
模块边界划分
Node内部的模块加载过程一般会分为两种:核心模块和外部模块。 核心模块:指的是系统内部的,由c/c++或者js编写。 外部模块:指的是运行时非node原生定义的模块,一般支持.node, .js , .json三种后缀。其中.node后缀是由自建c++编译得到的模块文件。c/c++内建核心模块
如前面所说,这部分是由c/c++编写的原生的代码,放源码的src文件夹下。在Node编译时会将内建的c/c++代码提取编译成二进制代码,通过一个数组关联。在启动Node代码时,这部分二进制代码会随着Node启动一起运行。在Node脚本运行时,如果引入相关的模块,会直接提取使用和缓存。 其过程可以如下抽象:
js核心模块
这部分是由js编写的原生模块代码,放在源码的lib文件夹下。在Node编译时,这部分代码会被v8提供的js2c.py脚本提取到c++的一个头文件node_natives.h中。提取内容为模块和js源码字符串的对应关系。源码字符串指的是,不像c++,js代码并不会提前解释,所以存储在内存的是js的字符串。启动时,和c++相关部分模块一起关联在核心模块中,平时我们在使用node模块时候,并不会去注意其本身是c++还是js编写。运行时如果使用相关模块,会获取js源码字符串,这个和前面的c++相关模块对比,获取时候会判断是c++内置模块还是js核心模块,如果是c++的直接通过binding函数获取运行,如果是js则由compile方法通过v8解释运行。同时使用过核心模块会添加到NativeModule对象的_cache缓存一个模块id。 其过程可以如下抽象:
外部模块
Node能解析三种后缀的模块 .js、.node、.json。Node在解析外部模块过程中,先要解析模块的路径,尝试获取文件,如果没有或尝试在node_module中获取。
-
.js类型 js模块的加载过程,首先通过fs解析路径,获取文件代码,然后包装,最后通过v8进行编译解释运行,对于解释过的外部模块,会添加的Module对象的_cache缓存。
-
.json类型 json模块实际上只是加载json数据的过程,解析完文件,将json文件用JSON.parse运行转换成js数据 格式。
-
.node类型 .node类型模块是c/c++代码通过node-gyp程序将代码编译成二进制的node文件,node中通过dlopen方法可以加载.node模块进行运行。在写c/c++模块时候,需要先编译成.node类型才能被node程序识别,node无法直接识别c++模块
-
其他 其他后缀会被node当成js程序执行。
2. 具体过程细节
上面有个大体的流程,node新版和旧版本的代码必然是不一样的。
v6版本node
启动过程
node启动代码在src的node_main.cc的c++文件下。node_main.cc
int main(int argc, char *argv[]) {
// Disable stdio buffering, it interacts poorly with printf()
// calls elsewhere in the program (e.g., any logging from V8.)
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
return node::Start(argc, argv);
}
调用了node.cc文件的Start方法, Start方法中调用了StartNodeInstance,然后调用了LoadEnvironment, 在LoadEnvironment中,拿到bootstrap_node.js运行。
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
使用require加载
平时使用node,都是像下面这样用require加载模块,可能是http这样的模块,也可能是相对路径的文件模块。
require('http')
require('./utils.js')
require的方法定义在Module.js对象上。module.js文件在lib对于文件夹下。关于require的流程细节就像代码展示的一样:
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
return Module._load(path, this, /* isMain */ false);
};
...
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
};
可以看到,所有核心的模块是用NativeModule.require(filename)获取,外部模块通过tryModuleLoad(module, filename)方法获取。
外部模块的获取细节
在上面的代码中,可以看到,通过tryModuleLoad(module, filename)获取了外部模块,而在tryModuleLoad中,其实还是调用了Module._extensions对象来按文件后缀引入外部模块。平时在我们在写模块文件中,可以拿到module,exports, require等对象,可以通过console.log(require.extensions)打印extensions的结构。下面看看tryModuleLoad的流程代码是怎么走:
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
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;
};
_extensions定义了三个方法分别是.js,.json,.node:
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._makeLong(filename));
};
1. js文件
正如前面的抽象模型所表示的,js文件会调用_compile方法编译,_compile会调用vm对象通过v8解释代码,关于vm接口可以在node文档中查阅。
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
2. json
这个没什么好说的。 module.exports = JSON.parse(internalModule.stripBOM(content));
3. node
也没什么好说的: process.dlopen(module, path._makeLong(filename));
核心模块
核心模块的流程走的是NativeModule的require方法。
js核心模块
前面说到,在编译时候,js核心模块会被js2c.py脚本转换成一个c++头文件。生成位置是在out/release/obj/gen目录下。结构如:
namespace node {
const char node_native[] = { 47, 47, ..};
const char dgram_native[] = { 47, 47, ..};
const char console_native[] = { 47, 47, ..};
const char buffer_native[] = { 47, 47, ..};
const char querystring_native[] = { 47, 47, ..};
const char punycode_native[] = { 47, 42, ..};
...
struct _native {
const char* name;
const char* source;
size_t source_len;
};
static const struct _native natives[] = {
{ "node", node_native, sizeof(node_native)-1 },
{ "dgram", dgram_native, sizeof(dgram_native)-1 },
...
};
}
其中,source对应的是js源码字符串。编译完成后,这个头文件会被编译进node二进制执行文件中(node命令)。 Node启动时候,所有核心模块(js和c++的)都是通过NativeModule对象维护。文件对象定义在bootstrap_node,顾名思义,这个脚本会在启动时执行。 bootstrap_node源码文件 其中跟着脚本一起执行的有:
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
...
startup();
binding方法是定义在c++中的方法。实际上就是将上面c++头文件的关系挂载到_source对象下。
c/c++核心模块
c++的模块在安装node时候以及编译成了可执行代码。所以平时使用如果涉及到c++的模块,通过上面NativeModule调用vm对象运行c++的模块,是不需要定位和编译过程,相对js的代码来说效率会更高。
c++的模块,通过NODE_MODULE宏挂载在Node的命名空间里。
#define NODE_MODULE(modname, regfunc)
extern "C" {
NODE_MODULE_EXPORT node::node_module_struct modname ## _module =
{
NODE_STANDARD_MODULE_STUFF,
regfunc,
NODE_STRINGIFY(modname)
};
}
c++模块的结构体是:
struct node_module_struct {
int version;
void *dso_handle;
const char *filename;
void (*register_func) (v8::Handle<v8::Object> target); const char *modname;
};
在node底层,可以通过Binding方法直接获取到c++的核心模块(正常都是通过调用js模块,间接调用),Binding方法定义在node.cc中:
static void Binding(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<String> module = args[0]->ToString(env->isolate());
node::Utf8Value module_v(env->isolate(), module);
Local<Object> cache = env->binding_cache_object();
Local<Object> exports;
if (cache->Has(env->context(), module).FromJust()) {
exports = cache->Get(module)->ToObject(env->isolate());
args.GetReturnValue().Set(exports);
return;
}
// Append a string to process.moduleLoadList
char buf[1024];
snprintf(buf, sizeof(buf), "Binding %s", *module_v);
Local<Array> modules = env->module_load_list_array();
uint32_t l = modules->Length();
modules->Set(l, OneByteString(env->isolate(), buf));
node_module* mod = get_builtin_module(*module_v);
if (mod != nullptr) {
exports = Object::New(env->isolate());
// Internal bindings don't have a "module" object, only exports.
CHECK_EQ(mod->nm_register_func, nullptr);
CHECK_NE(mod->nm_context_register_func, nullptr);
Local<Value> unused = Undefined(env->isolate());
mod->nm_context_register_func(exports, unused,
env->context(), mod->nm_priv);
cache->Set(module, exports);
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env->isolate());
DefineConstants(env->isolate(), exports);
cache->Set(module, exports);
} else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
cache->Set(module, exports);
} else {
char errmsg[1024];
snprintf(errmsg,
sizeof(errmsg),
"No such module: %s",
*module_v);
return env->ThrowError(errmsg);
}
args.GetReturnValue().Set(exports);
}
如代码所示,c++内建的模块通过node_module* mod = get_builtin_module(*module_v);
调用,从node_module_list数组中拿出,然后通过register_func注册给exports对象,而前面的js模块则是由'native'统一挂载在NativeModule._source下。
新版本node
参考资料: