第二章 模块机制

2017-02-22  本文已影响49人  Air_cc

之前 ECMAScript 的问题:

没有模块系统,标准库较少(如文件系统等缺失API),没有标准接口,无包管理系统

CommonJS

CommonJS 规范涵盖:模块、二进制、Buffer、字符编码、I/O流、进程环境、文件系统、套接字、单元测试、Web 服务器网关接口、包管理。

Node借鉴CommonJS的Modules规范实现了一套模块系统。

Node 与浏览器以及W3C组织、CommonJS组织、ECMAScript之间的关系

CommonJS 的模块规范

包括:模块引用、模块定义、模块标识3部分。

示例:

var math = require('math');

被引用模块的上下文提供 exports 对象用于导出当前模块的方法与变量,这是该模块唯一的导出出口。在模块中还包括一个标识模块本身的 module 对象。exports 正是 module 对象的一个属性的引用。

示例:

// math.js
let count = 0;
exports.incr = function () {
  count += 1;
  return count;
};

// program.js
var incr = require('match');
console.log(incr()); // 1
console.log(incr()); // 2

其实就是传递给 require() 方法的参数,它必须是符合小驼峰命名的字符串,或者一个相对路径,或者一个绝对路径。

模块加载的具体实现

Node 引入一个模块包括如下步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

Node 中的模块分为:核心模块(Node本身提供的模块)、文件模块(用户编写的模块)。

模块加载步骤:

优先从缓存加载

Node对引入过的模块都会进行缓存(缓存的是编译和执行后的对象),以避免二次引入时的开销。所以对于二次加载的模块,Node会优先从缓存中引入。另外核心模块的缓存检测优先于文件模块。

路径分析

即对模块标识符的分析。

Node 自定义模块的查找策略(类似于JavaScript的原型链或者作用域的查找方式):

文件定位

模块编译(文件模块)

定位到模块后便会编译执行该模块。每个文件模块都是一个对象,其定义如下:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  if (parent && parent.chidren) {
     parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Node 对于不同类型的文件模块执行不同的加载方法。

wrap 内容:

(function (exports, require, module, __filename, __dirname) {
  /* 实际文件内容 */
})
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;
  }
};

注:文件模块加载的具体代码实现可以参考这里

核心模块包括:c/c++ 编写的模块、js 编写的模块

JavaScript 核心模块的编译过程

  1. 通过 v8 附带的 js2c.py 工具将 js 代码以字符串的形式存储到 node 的命名空间中;
  2. 通过 process.binding('natives') 取出代码,存放到 NativeModule._cache 对象中;
  3. 当 require() 方法调用时,从 NativeModule._cache 中取出对应 id(模块标识符) 的代码,通过 NativeModule.compile() 方法 wrap、执行相应的代码。

C/C++ 核心模块的编译过程

这里分为:纯 C/C++ 编写的模块、核心部分由 C/C++ 编写,对外封装由 JS 完成的模块。其中纯 C/C++ 编写的部分称为内建模块

内建模块

Node 内建模块的结构体定义:

struct node_module {
  int nm_version;
  unsigned int nm_flags;
  void* nm_dso_handle;
  const char* nm_filename;
  node::addon_register_func nm_register_func;
  node::addon_context_register_func nm_context_register_func;
  const char* nm_modname;
  void* nm_priv;
  struct node_module* nm_link;
};

可通过 get_builtin_module() 方法取出该模块。内建模块在编译 Node 源代码时会被编译成二进制文件,在 Node 进程启动时,直接加载进内存中,可直接被外部(核心模块、C/C++拓展模块-但不建议直接调用)调用。这里同 JS 核心文件加载一样通过 process.binding() 方法加载,但它将 exports 对象缓存到 binding_cache_object 中。

os 原生模块引入流程

C/C++ 拓展模块

C/C++ 拓展模块的编写基本同内建模块一致,可借助 node-gyp 进行编译,只是不需要注册到 node builtin 模块中,而是通过 process.dlopen() 动态加载进来。由于 .node 文件已是编译后的二进制文件,所以被加载进来后不需编译直接执行,相较于 JavaScript 模块会略快一点。

.node 文件引入流程

包与 NPM

包实际被打包成一个存档文件(zip 或 tar.gz 格式)。CommonJS 规范的包结构:

NPM

依赖安装:

一些钩子: package.json 文件的scripts 中定义。

scripts: {
  "preinstall":  "install 该包之前执行的脚本",
  install: "install 该包时执行的脚本",
  uninstall: "卸载该包时执行的脚本",
  test: "单元测试脚本",
}
上一篇下一篇

猜你喜欢

热点阅读