node 模块

405

标签: 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 堆内存的占用,从而提升后续垃圾回收的效率。

参考

[1] https://github.com/nodejs/node/issues/5895