nodejs的模块分为几种,有内置的c++模块,内置的js模块,还有用户自定义的模块。下面我们先分析内置模块。然后再分析用户定义的模块。
1 内置模块
首先以注册tcp_wrap.cc模块为例子,一步步分析一下c++模块的注册。下面是tcp_wrap.cc模块的最后一句代码。
1 NODE_BUILTIN_MODULE_CONTEXT_AWARE(tcp_wrap, node::TCPWrap::Initialize)
宏展开后
#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
2 NODE_MODULE_CONTEXT_AWARE_CPP(tcp_wrap, node::TCPWrap::Initialize, nullptr, NM_F_BUILTIN)
#define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)
#define NODE_STRINGIFY_HELPER(n) #n
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \
static node::node_module _module = { \
NODE_MODULE_VERSION, \
flags, \
nullptr, \
__FILE__, \
nullptr, \
(node::addon_context_register_func) (regfunc), \
NODE_STRINGIFY(modname), \
priv, \
nullptr \
}; \
void _register_ ## modname() { \
node_module_register(&_module); \
}
3 static node::node_module _module = { \
NODE_MODULE_VERSION, \
NM_F_BUILTIN, \
nullptr, \
__FILE__, \
nullptr, \
// 转成一个函数指针类型
(node::addon_context_register_func) (regfunc), \
'tcp_wrap', \
priv, \
nullptr \
}; \
void _register_ tcp_wrap) { \
node_module_register(&_module); \
}
所以上面的代码中的第三步的代码就是tcp_wrap.cc最后的一句代码宏展开后的样子。即定义了一个结构体和函数。其他内置的模块也类似。然后在nodejs启动的时候,通过RegisterBuiltinModules函数进行模块的注册。
RegisterBuiltinModules()
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
NODE_BUILTIN_MODULES(V)
#undef V
}
#define NODE_BUILTIN_MODULES(V) \
NODE_BUILTIN_STANDARD_MODULES(V) \
NODE_BUILTIN_OPENSSL_MODULES(V) \
NODE_BUILTIN_ICU_MODULES(V)
#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
V(buffer) \
V(cares_wrap) \
V(config) \
V(contextify) \
V(domain) \
V(fs) \
V(fs_event_wrap) \
V(http2) \
V(http_parser) \
V(inspector) \
V(js_stream) \
V(module_wrap) \
V(os) \
V(performance) \
V(pipe_wrap) \
V(process_wrap) \
V(serdes) \
V(signal_wrap) \
V(spawn_sync) \
V(stream_wrap) \
V(tcp_wrap) \
V(timer_wrap) \
V(trace_events) \
V(tty_wrap) \
V(udp_wrap) \
V(url) \
V(util) \
V(uv) \
V(v8) \
V(zlib)
#
#if HAVE_OPENSSL
#define NODE_BUILTIN_OPENSSL_MODULES(V) V(crypto) V(tls_wrap)
#else
#define NODE_BUILTIN_OPENSSL_MODULES(V)
#endif
#if NODE_HAVE_I18N_SUPPORT
#define NODE_BUILTIN_ICU_MODULES(V) V(icu)
#else
#define NODE_BUILTIN_ICU_MODULES(V)
#endif
上面是nodejs内置的c++模块初始化过程。里面用了大量的宏,通过宏展开后,就是
_register_async_wrap()
_register_buffer()
就是执行各个内置模块里最后一句代码宏展开之后的函数。每个函数的内容都是
node_module_register(&_module)
不同的模块&_module对应的内容不一样。接下来我们看一下node_module_register函数。
static node_module* modlist_builtin;
static node_module* modlist_internal;
static node_module* modlist_linked
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_BUILTIN) {
mp->nm_link = modlist_builtin;
modlist_builtin = mp;
} else if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
modpending = mp;
}
}
把内置模块对应的module结构体逐个加到modlist_builtin链表里。即内置模块最后形成一条链表。然后我们看看nodejs是怎么使用这些内置的c++模块的。我们看到nodejs里的内置的js模块都是通过process.binding函数引用c++模块的。所以我们首先看看binding函数。该函数是在bootstrap_node.js里定义的。
const bindingObj = Object.create(null);
// 保存c++的binding函数
const getBinding = process.binding;
process.binding = function binding(module) {
module = String(module);
let mod = bindingObj[module];
if (typeof mod !== 'object') {
mod = bindingObj[module] = getBinding(module);
moduleLoadList.push(`Binding ${module}`);
}
return mod;
};
我们看到,nodejs在执行bootstrap_node.js之前就给process挂载了binding函数,然后在bootstrap_node.js里,首先保存了c++层的binding函数,然后重写了binding函数,这是因为c++层的binding函数没有做缓存处理,这里封装了一下,加了一个缓存。我们接着看c++的binding函数。该函数是在node.cc的SetupProcessObject函数定义的。SetupProcessObject在nodejs初始化的时候会调用。
env->SetMethod(process, "binding", Binding);
static void Binding(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsString());
Local<String> module = args[0].As<String>();
node::Utf8Value module_v(env->isolate(), module);
node_module* mod = get_builtin_module(*module_v);
Local<Object> exports;
if (mod != nullptr) {
exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env->isolate());
CHECK(exports->SetPrototype(env->context(),
Null(env->isolate())).FromJust());
DefineConstants(env->isolate(), exports);
} else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
} else {
return ThrowIfNoSuchModule(env, *module_v);
}
args.GetReturnValue().Set(exports);
}
node_module* get_builtin_module(const char* name) {
return FindModule(modlist_builtin, name, NM_F_BUILTIN);
}
inline struct node_module* FindModule(struct node_module* list,
const char* name,
int flag) {
struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp->nm_link) {
if (strcmp(mp->nm_modname, name) == 0)
break;
}
CHECK(mp == nullptr || (mp->nm_flags & flag) != 0);
return mp;
}
其实就是根据模块名在链表里查找这个模块,找到之后执行了initModule函数。
static Local<Object> InitModule(Environment* env,
node_module* mod,
Local<String> module) {
Local<Object> 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);
return exports;
}
执行了模块的nm_context_register_func执行的函数,对应的就是每个内置模块最后一句代码里的Initialize函数。他会导出一个对象给调用方。比如
const { TCP, constants: TCPConstants } = process.binding('tcp_wrap');
这就是内置的js模块调用c++模块的过程。
2 用户自定义模块
我们在写一个模块的时候,一般会用require函数,那这个函数是哪里来的呢?一个模块的代码,在nodejs看来其实只是一个字符串,nodejs在require这个模块的时候,会在整个模块代码包裹在一个函数里。
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
your code
'\n});'
];
所以我们要首先看看这个这里的require函数和其他参数是哪里来的。这时候我们就要找到第一个被执行的用户js是如何被处理的。我们看看bootstrap_node.js的代码 。里面有个NativeModule函数。这个函数也是加载模块的,但是他是用来加载内置的js模块的,这些模块在lib下。比如http模块。我们看一下他的核心代码。
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}
// lib下的js,是一个模块路径到源码的对象
NativeModule._source = process.binding('natives');
// 缓存管理
NativeModule._cache = {};
const config = process.binding('config');
NativeModule.require = function(id) {
if (id === 'native_module') {
return NativeModule;
}
// 先查缓存
const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
return cached.exports;
}
// 判断是否在native对象里
if (!NativeModule.exists(id)) {
...
}
moduleLoadList.push(`NativeModule ${id}`);
const nativeModule = new NativeModule(id);
// 加载然后执行,导出
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
NativeModule.requireForDeps = function(id) {
if (!NativeModule.exists(id) ||
// TODO(TimothyGu): remove when DEP0084 reaches end of life.
id.startsWith('node-inspect/') ||
id.startsWith('v8/')) {
id = `internal/deps/${id}`;
}
return NativeModule.require(id);
};
NativeModule.getCached = function(id) {
return NativeModule._cache[id];
};
NativeModule.exists = function(id) {
return NativeModule._source.hasOwnProperty(id);
};
NativeModule.getSource = function(id) {
return NativeModule._source[id];
};
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, internalBinding, process) {',
'\n});'
];
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
this.loading = true;
try {
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
const requireFn = this.id.startsWith('internal/deps/') ?
NativeModule.requireForDeps :
NativeModule.require;
// require函数只会在lib路径下的js里找模块
fn(this.exports, requireFn, this, internalBinding, process);
this.loaded = true;
} finally {
this.loading = false;
}
};
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};
从上面的代码我们知道,NativeModule模块根据传进去的id找到lib下的一个文件,然后执行,返回导出的对象。接下来我们看我们执行node app.js时是怎么被执行的。
bootstrap_node.js
const Module = NativeModule.require('module');
Module.runMain();
runMain就是执行app.js的入口。
// bootstrap main module.process.argv[1]即app.js
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
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;
}
/*
如果是原生的js模块(除了lib/internal文件夹下的)则优先取原生的,比如我们require('fs'),
即使我们也写了一个fs.js也不会被加载到。
*/
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
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.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
this.filename = filename;
/**
* /dsasa/dsa/b.js
* path.dirname(filename) => /dsasa/dsa/
*
* 目录路径:/dsasa/dsa
["/dsasa/dsa/node_modules", "/dsasa/node_modules", "/node_modules"]
*/
// 解析出文件可能存在的路径,如果dirname解析出的当前目录下不存在b.js,则会在paths里继续找
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 默认拓展名是js,如果传的文件名有拓展名但是不合法则取拓展名为js
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
// 找到对应的函数执行
Module._extensions[extension](this, filename);
this.loaded = true;
}
Module._nodeModulePaths = function(from) {
// guarantee that 'from' is absolute.
from = path.resolve(from);
// Return early not only to avoid unnecessary work, but to *avoid* returning
// an array of two items for a root: [ '//node_modules', '/node_modules' ]
if (from === '/')
return ['/node_modules'];
const paths = [];
var p = 0;
var last = from.length;
// 从路径的最后一个字符开始遍历,假设from是/a/b
for (var i = from.length - 1; i >= 0; --i) {
const code = from.charCodeAt(i);
// 找到一个/,并且/后面的字符串不是node_modules,追加一个path,如果本身node_modules就是则不需要追加了
if (code === 47/*/*/) {
if (p !== nmLen)
paths.push(from.slice(0, last) + '/node_modules');
// 更新还没有遍历的字符的末位置
last = i;
// 重置
p = 0;
} else if (p !== -1) {
// 逐个字符比较,判断连起来是否是node_modules
if (nmChars[p] === code) {
++p;
} else {
p = -1;
}
}
}
// Append /node_modules to handle root paths.
paths.push('/node_modules');
return paths;
};
解析完文件的路径后继续读取文件内容然后执行。
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
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 dirname = path.dirname(filename);
// 带有上下文的require
var require = internalModule.makeRequireFunction(this);
// 从这里可以知道我们在模块里拿到的require到底是什么,还有模块里的exports,module.exports
compiledWrapper.call(this.exports, this.exports, require, this,filename, dirname);
return result;
};
require函数返回了的module.exports,就这样我们拿到了模块里导出的内容。模块系统的模块查找和缓存还有很多细节,有时间继续分析。