Node模块机制

139 阅读6分钟

web2.0时代,B/S表现出比C/S更大优势,JS的作用经历了 工具类库->功能组件->框架->应用 的迁移。但却始终缺少一个模块机制:

CJS规范

1. CJS出发点

ECMAScript规范只涉及JS语言的基本要素: 词法 作用域 上下文 类型等。浏览器端有W3C主推的HTML5规范给JS带来了多种标准API。社区总结了后端JS的规范: CJS。它给JS带来了 模块系统 包管理系统 Buffer 文件系统 IO流 字符集编码 进程环境 套接字 Web服务器网关等要素。
CJS规范是一种理论,一个凝结社区努力的优秀的设计。Node正是对这一个规范的优秀实现。

Node与CJS、浏览器、W3C的关系

2. CJS模块规范

CJS规范的模块系统包括: 模块引入 模块导出 模块标识

  • 模块引入:require(模块标识)
  • 模块导出:一个文件一个模块(模块本身module),挂载到exports上实现导出
  • 模块标识:相对路径/绝对路径

CJS模块意义在于将API限定在私有作用域,通过导出 引入实现上下游依赖。

Node的模块实现

Node实现 模块加载分为:

  • 路径分析
  • 文件定位
  • 编译执行

Node模块分为两种:核心模块 文件模块。针对这两种类型的模块,核心模块在Node源码编译过程中,加载过程不需要文件定位编译执行文件模块则需要在运行时完整经历这三步;所以核心模块文件模块加载速度快。

1. 优先从缓存加载

类似于浏览器缓存文件,Node中对二次引入的模块也会缓存编译和执行之后对象,以减少二次引入的开销

2. 路径分析和文件定位

路径分析即通过require()+模块标识符获取模块路径的过程。

  • 模块标识符分析
    根据标识符可将模块分为:
    • 核心模块:Node源码编译阶段已经包含了核心模块
    • 路径形式文件模块:包括相对路径 绝对路径两种类型的模块,都是在路径分析时解析为真实路径
    • 非路径形式文件模块:包含自定义模块,各种等。其模块路径是一个数组。从当前文件所在目录开始一层层向上查找node_modules文件夹,直到找到未知。
    console.log(module.paths);
    // [ '/Users/hb/tzy/daily-fe/daily-fe-node/src/node_modules',
    //   '/Users/hb/tzy/daily-fe/daily-fe-node/node_modules',
    //   '/Users/hb/tzy/daily-fe/node_modules',
    //   '/Users/hb/tzy/node_modules',
    //   '/Users/hb/node_modules',
    //   '/Users/node_modules',
    //   '/node_modules' ]
    
  • 文件定位 注意文件扩展名目录和包的处理。
    • 文件没有后缀,按照.js .json .node进行补足。
    • 若定位到一个目录,当做来处理:
      • 查找包描述文件:package.json
      • 查找入口文件:package.jsonmain字段;若不存在main字段,则查找index文件。

3. 模块编译

模块载入

根据扩展名不同,载入方式也有所不同: - .jsfs 同步读取后编译模块 - .jsonfs 同步读取后JSON.parse() - .node:xxx 载入函数在Module._extensions上,可通过require._extensions查看。

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};

模块编译

注意这里的模块编译只针对文件模块

  • JS模块编译

module._compile()方法里:

    // ...
    const wrapper = Module.wrap(content);
    compiledWrapper = vm.runInThisContext(wrapper, {
      filename,
      lineOffset: 0,
      displayErrors: true,
      importModuleDynamically: experimentalModules ? async (specifier) => {
        if (asyncESM === undefined) lazyLoadESM();
        const loader = await asyncESM.loaderPromise;
        return loader.import(specifier, normalizeReferrerURL(filename));
      } : undefined,
    });
    // ...

Module.wrap()函数里:

let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

将模块代码进行头尾包装,前面的compiledWrapper就是vm.runInThisContext()执行后生成的一个具体的function对象,即:

(function (exports, require, module, __filename, __dirname) {
  require('./m1');
  console.log(module);
});

这个function最后被执行一次,参数是exports模块导出 require模块引入函数 this即module本身 filename dirname并且将返回值传递给调用方:

result = inspectorWrapper(
  compiledWrapper,  // 该function
  this.exports, 
  this.exports, // 参数
  require, 
  this, 
  filename, 
  dirname
);

Node中,++每个模块都是一个对象++。编译成功的模块将以文件路径索引存在Module._cache对象上。

  • C/C++模块编译

.node模块C/C++模块编译生成,只有加载和执行的过程,不需编译

// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path.toNamespacedPath(filename));
};

执行方法是process.dlopen()

  • JSON模块编译
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

fs模块 同步读取后JSON.parse()

核心模块

核心模块包括JS核心(Nodelib目录内)和C/C++核心(Nodesrc目录内)。

JS核心模块编译

  • 转存为C/C++
  • JS核心模块编译

C/C++核心模块编译

  • 内建模块的组织形式
  • 内建模块的导出

核心模块引入流程

os原生模块引入流程

编写核心模块

...

C/C++扩展模块

...

模块调用栈

模块调用栈示意图

从上图可以看出:

  • C/C++扩展模块也是文件模块的一种,提供给JS文件模块调用
  • C/C++核心模块处于最底层,提供给JS核心模块JS文件模块调用
  • JS核心模块有两个作用:
    • C/C++核心模块JS文件模块中间的桥接
    • 普通功能模块

包与NPM

包与NPM是将模块联系起来的一种机制,是对CJS包规范(包含包结构包描述)的一种实现。

包组织模块

1. 包结构

包是一个存档文件tar.gz/.zip),安装后还原为目录,完全符合CJS包规范的包结构应该包括:

  • package.json文件
  • lib目录存放JS代码
  • bin目录存放二进制命令
  • test目录
  • doc目录

2. NPM与包描述文件

NPM的所有行为都与包描述文件字段息息相关,需要注意的有:

  • main字段:包的出口模块文件,也就是包中其他模块的入口。

3. NPM常用功能

查看帮助

  • npm -h

安装依赖

全局安装

-g意思是将包安装为全局可用可执行命令(以commitizen举例):

  • 包安装位置为/usr/local/lib/node_modules/commitizen
  • 根据包描述文件的bin字段/usr/local/lib下创建commitizen可执行文件的链接。

本地安装

NPM钩子命令

"scripts": {
  "preinstall": "preinstall.js",
  "install": "install.js",
  "uninstall": "uninstall.js",
  "test": "test.js"
}”

发布包

  • npm publish

NPM初始化

  • npm init

编写模块

注册账号

  • npm adduser

上传包

  • npm publish

安装包

包权限

  • npm owner

分析包

  • npm ls

前后端共用模块

Node使用JS实现后,前后端便可以通用JS模块。

1. 模块的侧重点

前后端JS模块的侧重点不同,因为其宿主环境不同导致:

  • 前后端代码分布在HTTP两端,前端JS模块需要服务端下发给不同的客户端(瓶颈在带宽),后端JS模块则是直接从磁盘读取,然后执行多次(瓶颈在性能)。来源不同,所以两者的加载速度不在一个量级。

2. AMD规范

3. CMD规范

4. 兼容多种模块规范

根据环境字段,使用不同的加载方式