Node模块加载原理

460 阅读7分钟

导读

文章不介绍commonjs细节。会分两部分去介绍node模块加载的原理,第一部分是抽象模型,这部分是希望用最简单的语言去概括node模块运行状态。第二部分是对第一部分的补充,需要结合源码去分析其中的细节。

术语约定

  1. Node编译时:指node安装时候编译的过程
  2. Node启动时:指通过node命令开启一个node进程的过程
  3. 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。 其过程可以如下抽象:

js核心模块

外部模块

Node能解析三种后缀的模块 .js、.node、.json。Node在解析外部模块过程中,先要解析模块的路径,尝试获取文件,如果没有或尝试在node_module中获取。

  1. .js类型 js模块的加载过程,首先通过fs解析路径,获取文件代码,然后包装,最后通过v8进行编译解释运行,对于解释过的外部模块,会添加的Module对象的_cache缓存。

  2. .json类型 json模块实际上只是加载json数据的过程,解析完文件,将json文件用JSON.parse运行转换成js数据 格式。

  3. .node类型 .node类型模块是c/c++代码通过node-gyp程序将代码编译成二进制的node文件,node中通过dlopen方法可以加载.node模块进行运行。在写c/c++模块时候,需要先编译成.node类型才能被node程序识别,node无法直接识别c++模块

  4. 其他 其他后缀会被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

参考资料:

  1. 深入浅出nodejs.pdf
  2. node github源码
  3. 结合源码分析 Node.js 模块加载与运行原理