前驱知识准备
模块机制
CMJ 规范的可使用场景:
- 服务端JS应用
- 命令行工具
- 图形化用户界面(GUI)桌面应用
- 混合应用(如Titanium,Adobe AIR等)
模块上下文
module.exports | exports
模块寻径算法
- .js => .json => .node
- 递归父目录的
node_modules
模块缓存机制
通常一个包一旦被加载了,那么在第二次执行require()的时候就会在缓存中获取暴露的API,而不会重新加载一遍该模块里面的代码再次返回。
包机制
包描述文件
包目录
- package.json 在根目录下
- 二进制文件应当在bin目录下
- JS源码应当在lib目录下
- 文档应当在doc目录下
- 单元测试文件应当在test目录下
Node依赖简介(deps)
高效的V8
- JIT(Just in time):即时编译,编译出的结果直接是机器语言,而非字节码
- 垃圾回收:借鉴了 JAVA VM 的精确垃圾回收管理,别家是保守垃圾管理
- 内联缓存(InLine Cache):提高属性的访问效率,无须每次访问属性都对哈希表进行一次寻址,有了缓存,V8能立马知道这个属性的偏移量,不用再次计算寻址的偏移量了
-
驱动事件循环的 libuv
基于epoll/kqueue/IOCP/event ports实现的全能事件循环
- 异步TCP和UDP套接字
- 异步DNS解析
- 异步文件、文件系统操作
- 文件系统事件
- ANSI转义码控制的TTY
- 使用UNIX domain 套接字或者命名管道实现的套接字共享IPC
- 子进程
- 线程池
- 信号(Signal)处理
- 高精度时钟
-
http-parser
一个C实现的HTTP消息解析器
无依赖
- 持久化连接的流式处理(keep-alive)
- 分段信息(chunk)的解码
- 缓冲区溢出攻击的防御
- 解析HTTP消息
zlib
提供数据压缩功能的库。
facebook开发的另一套zstd据说压缩比zlib优秀。
环境
node-gyp
npm install -g node-gyp # 全局安装
node-gyp -h # 查看帮助
NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node node-gyp install # 把当前执行的node.js相应版本的源码头文件、lib文件等下载解压到当前用户目录的.node-gyp目录下
node-gyp build # 调用make以构建模块
node-gyp clean # 清理生成的构建文件以及out目录
node-gyp configure # 为当前模块生成Makefile项目配置文件
node-gyp rebuild # 一次性依次执行clean/configure和build
node-gyp install # 为指定版本的node.js安装开发环境的文件
node-gyp list # 输出当前安装的node.js开发环境文件
node-gyp remove # 移除指定版本的node.js开发环境文件
:::info 扩展:install命令有一个环境变量NODEJS_ORG_MIRROR,这个变量指向安装包的registry,因此将其设置为阿里巴巴的镜像地址,就能够顺利下载node文件 :::
什么是C++拓展
- 一个编译好的C++模块除了后缀名是.node之外,它其实就是一个系统的动态链接库。相当于Windows下的.dll、Linux下的.so、macOS 下的.dylib
- 在 node.js 中引入一个C++模块的过程实际上就是Node.js在运行时引入了一个动态链接库的过程。
模块加载原理
nodev6.9.4
node入口:node_main.cc
Start函数是 node.cc 中的,进一步深入到LoadEnvironment会发现如下代码: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.js执行// Execute the lib/internal/bootstrap_node.js file which was included as a
// static C string in node_natives.h by node_js2c.
// 'internal_bootstrap_node_native' is the string containing that source code.
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
// ...
Local<Function> f = Local<Function>::Cast(f_value);
// ...
// We start the process this way in order to be more modular. Developers
// who do not like how bootstrap_node.js sets up the module system but do
// like Node's I/O bindings may want to replace 'f' with their own function.
Local<Value> arg = env->process_object();
f->Call(Null(env->isolate()), 1, &arg);
lib/internal/bootstrap_node.js
文件以进行初始化启动。执行该文件后得到的f_value就是bootstrap_node.js
文件中所实现的那个函数对象。
SetupProcessObject就是用来实现全局对象process的,可以看到其设置API的部分代码如下:
env->SetMethod(process, "hrtime", Hrtime);
env->SetMethod(process, "cpuUsage", CPUUsage);
env->SetMethod(process, "dlopen", DLOpen);
env->SetMethod(process, "uptime", Uptime);
env->SetMethod(process, "memoryUsage", MemoryUsage);
env->SetMethod(process, "binding", Binding);
几种模块的加载过程
- C++核心模块:采用纯C++编写,存在于Node.js源码中且编译进Node.js的可执行二进制文件中。
- Node.js内置模块:即lib下以同名JS代码形式实现的模块,也被编译进Node.js的可执行二进制文件中了。
- 用户源码模块:非Node.js内置模块的JS源码模块。如入口模块
-
C++核心模块
static void Binding(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
// 从参数中获得文件标识的字符串(文件名)并赋值给 module_v
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;
}
// 将一个字符串附加在 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));
// 在得到标识字符串后,获取 C++ 核心模块
// get_builtin_module 内部就是在 modlist_builtin 的 C++ 核心模块链表上对比文件标识,从而返回相应的模块
node_module* mod = get_builtin_module(*module_v);
if (mod != nullptr) {
exports = Object::New(env->isolate());
// 内置的模块对象并没有 "module" 对象,只有 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_register中被逐一注册进链表中的。 ```cpp extern “C” void node_module_register(void m) { struct node_module mp = reinterpret_cast
(m); // 如果传入待注册模块的标识位是内置模块,就将其加入C++核心模块的链表中;否则将认为是其他模块 if (mp->nm_flags & NM_F_BUILTIN) { mp->nm_link = modlist_builtin; modlist_builtin = 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; } }
struct node_module get_builtin_module(const char name) { struct node_module* mp;
for (mp = modlist_builtin; mp != nullptr; mp = mp->nm_link) { if (strcmp(mp->nm_modname, name) == 0) break; }
CHECK(mp == nullptr || (mp->nm_flags & NM_F_BUILTIN) != 0); return (mp); }
node.h 中有一个[`NODE_MODULE_CONTEXT_AWARE_X`](https://github.com/nodejs/node/blob/v6.9.4/src/node.h#L458) 宏是用于注册C++核心模块的。即推测只要在C++核心模块中调用该宏就会被注册进Node.js核心模块链中。<br />我们看看file模块的源码[node_file.cc](https://github.com/nodejs/node/blob/v6.9.4/src/node_file.cc#L1511),其最后一行代码
```cpp
NODE_MODULE_CONTEXT_AWARE_BUILTIN(fs, node::InitFs)
至此,真相大白,基本上每个C++核心模块的源码末尾都有一个宏调用将该模块注册进C++核心模块的链表中,以供process.binding时进行获取。
Node.js内置模块
node的启动脚本lib/internal/bootstrap_node.js中有一个NativeModule的声明,其负责处理Node.js内置模块。
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}
// NativeModule._source 是 process.binding的返回结果,即DefineJavaScript的处理结果
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
// 当id值为'native_module'时返回它自身,否则就进入到nativeModule.compile进行编译
NativeModule.require = function(id) {
if (id === 'native_module') {
return NativeModule;
}
//...
process.moduleLoadList.push(`NativeModule ${id}`);
const nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
// ...
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() {
// 通过 NativeModule.getSource 获取源码,其返回的是 NativeModule._source 数组中的相应内容
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
});
// 进行包裹,形成一段闭包代码,之后将其放入到 vm 中运行,并传入事先准备好的 module 和 exports 对象供其导出
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
} finally {
this.loading = false;
}
};
在 src/node_javascript.cc 中有一个DefineJavaScript,它做的事情就是遍历一遍natives数组里面的内容,并将其一一加入要返回的对象中,其中对象名的键名为源码文件名标识,键值是源码本体的字符串。
那这个natives
又是怎么来的呢——是 node.gyp
编译而来。node.gyp
其中有一步是 node_js2c
,这一步就是用 Python 去调用 tools/js2c.py
的文件,其主要作用就是将lib下的JS文件转换成 src/node_natives.h
文件。这个src/node_natives.h
会在Node.js编译前完成,这样在编译到src/node_javascript.cc
时它所需要的 src/node_natives.h
头文件就存在了。
也就是说,在Node.js中调用 NativeModule.require
时,会根据传入的文件标识来返回相应的 JS 源文件内容。
用户源码模块
与内置模块类似,会被加上一个闭包的头尾,然后Node.js执行这个闭包产生结果。
在源码中执行的require()就是lib/module.js中的Module类实例对象的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
Module._load = function(request, parent, isMain) {
// ...
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;
};
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
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));
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};
默认有3种规则:
- Module._extensions[“.js”]
- Module._extensions[“.json”]
Module._extensions[“.node”]
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
Module._extensions['.js']
这种规则做了两件事情:同步读取源码的内容,使用
fs.readFileSync(filename, 'utf8');
调用
module._compile()
编译源码并执行 ```cpp // 生成闭包源码,然后传入相应的函数执行 Module.prototype._compile = function(content, filename) { // Remove shebang var contLen = content.length; if (contLen >= 2) { if (content.charCodeAt(0) === 35/#/ &&content.charCodeAt(1) === 33/*!*/) {
if (contLen === 2) {
// Exact match
content = '';
} else {
// Find end of shebang line and slice it off
var i = 2;
for (; i < contLen; ++i) {
var code = content.charCodeAt(i);
if (code === 10/*\n*/ || code === 13/*\r*/)
break;
}
if (i === contLen)
content = '';
else {
// Note that this actually includes the newline character(s) in the
// new output. This duplicates the behavior of the regular expression
// that was previously used to replace the shebang line
content = content.slice(i);
}
}
} }
// 生成闭包源码 var wrapper = Module.wrap(content); // 用 vm 编译 wrapper var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true });
if (process._debugWaitConnect && 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);
} else {
resolvedArgv = 'repl';
}
}
// Set breakpoint on module start if (filename === resolvedArgv) {
delete process._debugWaitConnect;
const Debug = vm.runInDebugContext('Debug');
Debug.setBreakPoint(compiledWrapper, 0, 0);
} } var dirname = path.dirname(filename); var require = internalModule.makeRequireFunction.call(this); var args = [this.exports, require, this, filename, dirname]; var depth = internalModule.requireDepth; if (depth === 0) stat.cache = new Map(); var result = compiledWrapper.apply(this.exports, args); if (depth === 0) stat.cache = null; return result; };
- 开发者调用`require(filename)`(等同于调用`module.load(filename);`)
- 源码加闭包马甲:一个模块的源码经过闭包化后,就形成了一个接收exports、require、module、__filename和__dirname的闭包函数。这就是我们平时编写代码的时候能直接使用exports、require、module等内容的原因了。在我们编写的源码模块被载入的时候,这些变量会随着闭包传进来而被使用。这个闭包会在第一次加载该模块的时候执行一次,之后就一直存在于模块缓存中(除非手动清除缓存)。也即一个模块逻辑代码只会被执行一次。
- 用 `vm` 编译 `wrapper`
- 传入参数执行`vm`
<a name="JcP0J"></a>
#### 入口模块
[src/node_main.cc](https://github.com/nodejs/node/blob/master/src/node_main.cc#L127)
```cpp
return node::Start(argc, argv);
C++拓展模块
用户源码模块和C++扩展模块加载的区别是通后后缀名来区分的。Module._extensions[".node"]
则是C++拓展模块。
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path._makeLong(filename));
};
实际调用的就是在src/node.cc中SetupProcessObject里挂载上去的DLOpen
env->SetMethod(process, "dlopen", DLOpen);
// DLOpen is process.dlopen(module, filename).
// Used to load 'module.node' dynamically shared objects.
// FIXME(bnoordhuis) Not multi-context ready. TBD how to resolve the conflict
// when two contexts try to load the same shared object. Maybe have a shadow
// cache that's a plain C list or hash table that's shared across contexts?
void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;
CHECK_EQ(modpending, nullptr);
if (args.Length() != 2) {
env->ThrowError("process.dlopen takes exactly 2 arguments.");
return;
}
Local<Object> module = args[0]->ToObject(env->isolate()); // Cast
node::Utf8Value filename(env->isolate(), args[1]); // Cast
// 使用uv_dlopen加载链接库
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
// Objects containing v14 or later modules will have registered themselves
// on the pending list. Activate all of them now. At present, only one
// module per object is supported.
node_module* const mp = modpending;
modpending = nullptr;
if (is_dlopen_error) {
Local<String> errmsg = OneByteString(env->isolate(), uv_dlerror(&lib));
uv_dlclose(&lib);
#ifdef _WIN32
// Windows needs to add the filename into the error message
errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate()));
#endif // _WIN32
env->isolate()->ThrowException(Exception::Error(errmsg));
return;
}
if (mp == nullptr) {
uv_dlclose(&lib);
env->ThrowError("Module did not self-register.");
return;
}
if (mp->nm_version != NODE_MODULE_VERSION) {
char errmsg[1024];
snprintf(errmsg,
sizeof(errmsg),
"Module version mismatch. Expected %d, got %d.",
NODE_MODULE_VERSION, mp->nm_version);
// NOTE: `mp` is allocated inside of the shared library's memory, calling
// `uv_dlclose` will deallocate it
uv_dlclose(&lib);
env->ThrowError(errmsg);
return;
}
if (mp->nm_flags & NM_F_BUILTIN) {
uv_dlclose(&lib);
env->ThrowError("Built-in module self-registered.");
return;
}
// 将加载的链接库句柄转移到 mp 上
mp->nm_dso_handle = lib.handle;
mp->nm_link = modlist_addon;
modlist_addon = mp;
Local<String> exports_string = env->exports_string();
// exports_string其实就是 exports
// 这句的意思是`exports = module.exports`
Local<Object> exports = module->Get(exports_string)->ToObject(env->isolate());
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
} else {
uv_dlclose(&lib);
env->ThrowError("Module has no declared entry point.");
return;
}
// Tell coverity that 'handle' should not be freed when we return.
// coverity[leaked_storage]
}
DLOpen先使用uv_dlopen(*filename, &lib)
打开了*.node
扩展(动态链接库),将其载入到内存uv_lib_t
中,然后通过
V8 基础
V8 宿主:
- chrome宿主:HTML DOM、window对象等内容
- node 宿主:沙盒机制(VM)、文件系统、网络操作等
Node:
- V8: JS 运行时,用于解释执行JS
- libuv: 实现了“事件循环”
内存机制
JS的数据类型都是由V8内部的内存机制进行管理的。即V8创建的数据存储单元只能被它的内存回收机制所回收,而不能被我们自己进行管理。
V8 堆内存:
- 新生代内存区:基本的数据对象都会被分配到这里。特点是小而频。
- 老生代指针区:这是一堆指向老生代内存区具体数据内容的指针。基本上从新生代蜕变过来的对象都会被移动至此。
- 老生代数据区:存放数据对象,而不是指向其他对象的指针。老生代指针区的指针就往这边指
- 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收并不会移动大对象。
- 代码区:代码对象(就是包含JIT之后指令的对象)会被分配到这里。这是唯一拥有执行权限的内存区
- Cell区、属性Cell区、Map区:存放cell、属性cell 和 Map,每个区域都是存放相同大小的元素,结构简单。
新生代内存区
Scavenge 算法:将内存一分为二,有较大的空间浪费
- From空间:处于使用状态
- To空间:处于闲置状态
在分配对象时,总使用 From空间进行分配;
在 GC 时,V8 检查 From 空间中的存活对象,将这些对象复制到 To 空间中,剩下的对象就会被释放,完成复制后From 和 To 角色对调。
经过多次新生代 GC 后如果某个对象依旧坚挺,则会被晋升到老生代内存。
- 至少经历过一次 GC 的过程
- 如果 To 的使用空间已经超过了25%
老生代内存
这里保存的对象都是些生命周期很长甚至是常驻内存的对象,且老生代占用的内存较多。
- Mark-Sweep(标记清除)
- 标记:遍历,并标记活着的对象
- 清除:只清除没有被标记的对象
- Mark-Compact(标记整理)
- 在清除的时候,让活着的剩余对象尽可能往内存区域前面靠
- 即内存区域的紧缩
- 惰性清理:因为标记了,所以直到哪些是活得,哪些是死的,因此不会实时清理,会根据自身需要延迟清理。
隔离实例(Isolate)
Isolate 就是 V8 引擎的实例,也可以理解为引擎本体。Isolate 内部拥有完全独立的各种状态、包括堆管理、垃圾回收等,但是不执行JS。可被创建多个。上下文(Context)
Context 用来定义 JS 的执行环境。可以理解为一个沙箱化的执行上下文环境,内部预置了一系列的对象和函数。脚本(Script)
Script是一个包含一段已经编译好的JS脚本的对象,数据类型就是Script。它在编译时就与一个处于活动状态的上下文进行绑定。
句柄(Handle)
句柄是用于获取JS对象实体的一种事物,有有效句柄连接的对象实体不会被垃圾回收器进行回收,而失去了所有句柄引用的对象实体会被认为是垃圾,从而在下次垃圾回收的时候被释放。
句柄类型:
- 本地句柄(v8::Local): 存在于栈内存中,并在对应的析构函数被调用时被删除。生命周期由句柄作用域决定。
- 创建:new()
- 清除:clear()
- 是否为空:isEmpty()
- 转换数据类型:As/Cast
- 持久句柄(v8::Persistent): 存在于堆内存中,
- 清除:clear()
- 是否为空:isEmpty
- 置为弱持久句柄:SetWeak()
- 取消弱持久句柄:ClearWeak()
- 标记独立句柄:MarkIndependent()
- 是否为弱的:isWeak()
- 是否为独立的:isIndependent()
- 永生句柄(v8::Eternal)
- 待实本地句柄
- 其他句柄
句柄的存在形式是C++的一个模板类,其需要根据不同的V8数据类型进行不同的声明。
句柄作用域
句柄作用域实际上是维护一堆句柄的类。以栈的形式一层一层套着,存在于Isolate实例中,栈顶的作用域是当前活动作用域,每次新建对象时得到的句柄都会与当前活动作用域绑定,当一个句柄作用域对象的析构函数被调用时,在这个作用域中创建的所有句柄都会被从栈中抹去。于是,通常情况下这些句柄所指的对象将会失去所有引用,然后会被垃圾回收器统一处理。这里有一种例外,就是可逃句柄作用域所设置的已逃脱句柄可以逃过一劫。
上下文(Context)
Context是独立且允许存在多个的,是JS代码的执行环境。一个Context为JS的执行提供了内置的对象和方法。
node 的内置模块 vm 的本质就是利用 Context 可以多个的特性。
lib/vm.js
模板(Template)
V8中的模板指的是在Context中JS对象及函数的一个模具。
Template的派生类
- 函数模板(Function Template)
-
常用数据类型
Value
- String
- Number
- Integer
- Int32
- Uint32
- Boolean
- Object
- Function
- Array
- FunctionCallbackInfo
-
异常
TryCatch对象:对应JS中的 try-catch 代码块
- Isolate::Throw-Exception:对应 JS 的 throw 操作
Exception::Error: 对应 new Error 操作,并且这个静态类能生成的不只是普通的 Error 对象,还能生成诸如SyntaxError 等错误对象。
资源工具
NodeJS使用淘宝npm镜像站的各种姿势:https://www.jianshu.com/p/253cb9003411
- node-scripts:https://docs.npmjs.com/misc/scripts
- V8: http://developers.google.com/v8/