标签: node模块
If V8 is the engine of Node.js, npm is its soul!
npm 世界最大的模块仓库,我们看几个数据:
- ~21 万模块数量
- 每天亿级模块下载量
- 每周 10 亿级的模块下周量
- 由此诞生了一家做 npm 包管理的公司 npmjs.com.
模块加载准备操作
严格来讲,Node 里面分以下几种模块:
-
builtin module: Node 中以 c++ 形式提供的模块,如 tcp_wrap、contextify 等
-
constants module: Node 中定义常量的模块,用来导出如 signal, openssl 库、文件访问权限等常量的定义。如文件访问权限中的 O_RDONLY,O_CREAT、signal 中的 SIGHUP,SIGINT 等。
-
native module: Node 中以 JavaScript 形式提供的模块,如 http,https,fs 等。有些 native module 需要借助于 builtin module 实现背后的功能。如对于 native 模块 buffer , 还是需要借助 builtin - node_buffer.cc 中提供的功能来实现大容量内存申请和管理,目的是能够脱离 V8 内存大小使用限制。
-
3rd-party module: 以上模块可以统称 Node 内建模块,除此之外为第三方模块,典型的如 express 模块。
模块加载
http
我们仍旧从 var http = require('http'); 说起。 require 是怎么来的,为什么平白无故就能用呢,实际上都干了些什么?
(看不看得懂我就不知道了,总之,先了解一下,读一读代码,也许可能大概或许会明白一些。。。)
- lib/module.js 的中有如下代码。
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(path) {
assert(path,'missing path');
assert(typeof path ==='string','path must be a string');
return Module._load(path, this);
};
首先 assert 模块进行简单的 path 变量的判断,需要传人的 path 是一个 string 类型。
// 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) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent);
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;
var hadException = true;
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
return module.exports;
};
- 如果模块在缓存中,返回它的 exports 对象。
- 如果是原生的模块,通过调用 NativeModule.require() 返回结果。
- 否则,创建一个新的模块,并保存到缓存中。
让我们再深度遍历的方式查看代码到 NativeModule.require
NativeModule.require = function(id) {
if (id =='native_module') {
return NativeModule;
}
var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}
if (!NativeModule.exists(id)) {
throw new Error('No such native module '+ id);
}
process.moduleLoadList.push('NativeModule' + id);
var nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
我们看到,缓存的策略这个贯穿在 node 的实现中。
同样的,如果在 cache 中存在,则直接返回 exports 对象。 如果不在,则加入到 moduleLoadList 数组中,创建新的 NativeModule 对象。 下面是最关键的一句
nativeModule.compile();
具体实现在 node.js 中:
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, __filename, __dirname) {','\n});' ];
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
var fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0
});
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
};
wrap
函数将 http.js 包裹起来, 交由 runInThisContext
编译源码,返回 fn 函数, 依次将参数传人。
process
先看看 node.js 的底层 C++ 传递给 javascript 的一个变量 process,在一开始运行 node.js 时,程序会先配置好 process Handleprocess = SetupProcessObject(argc, argv);
- 然后把 process 作为参数去调用 js 主程序 src/node.js 返回的函数,这样 process 就传递到 javascript 里
//node.cc
// 通过 MainSource() 获取已转化的 src/node.js 源码,并执行它
Local f_value = ExecuteString(MainSource(), IMMUTABLE_STRING(“node.js”));
// 执行 src/node.js 后获得的是一个函数,从 node.js 源码可以看出:
//node.js
//(function(process) {
// global = this;
// …
//})
Local f = Local::Cast(f_value);
// 创建函数执行环境,调用函数,把 process 传入
Localglobal = v8::Context::GetCurrent()->Global();
Local args[1] = {
Local::New(process)
};
f->Call(global, 1, args);
vm
runInThisContext
又是怎么一回事呢?
var ContextifyScript = process.binding('contextify').ContextifyScript;
function runInThisContext(code, options) {
var script = new ContextifyScript(code, options);
return script.runInThisContext();
}
- node.cc 的 Binding 中有如下调用,对模块进行注册,
mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv)
;
runInThisContext 是将被包装后的源字符串转成可执行函数,(runInThisContext 来自 contextify 模块),runInThisContext 的作用,类似 eval,再执行这个被 eval 后的函数。
这样就成功加载了 native 模块, 标记 this.loaded = true;
总结
Node.js 通过 cache 解决无限循环引用的问题, 也是系统优化的重要手段,通过以空间换时间,使得每次加载模块变得非常高效。
在实际的业务开发中,我们从堆的角度观察 node 启动模块后,缓存了大量的模块,包括第三方的模块,有的可能只加载使用一次。笔者觉得有必要有一种模块的卸载机制[1] , 可以降低对 V8 堆内存的占用,从而提升后续垃圾回收的效率。