NodeJS的模块加载机制

327 阅读3分钟

1. CommonJS模块规范

NodeJS的实现 = ECMAScript + CommonJS

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

CommonJS的模块规范

1. 模块定义

  1. 模块= module
  2. exports是module的属性,将方法挂在到exports上即可导出

2. 模块引入 require()方法

3. 模块标识(可以省略文件名后缀.js)

  1. 小驼峰字符串
  2. . 或者 .. 开头的相对路径
  3. 绝对路径

2. Node 模块实现

模块引入

Node中引入模块需要经历三个步骤

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

核心模块(Node提供的模块)在Node源码编译过程中,编译进了二进制执行文件,在启动Node时,部分核心模块就被直接加载进了内存中,所以这部分模块引入时会直接跳过文件定位和编译执行,并且在路径分析中优先判断。加载速度最快

文件模块(用户编写的模块)需要完成整的路径分析、文件定位、编译执行。

模块的加载

1. 优先从缓存加载 Node会缓存编译和执行之后的对象,无论是核心模块还是文件模块,第二次加载都会从缓存优先,这是第一优先级,且核心模块的缓存检查要先于文件模块。

2. 路径分析

  1. 核心模块 的优先级仅次于缓存加载,如果一个模块与核心模块重名,会永远也加载不到。
  2. 路径形式的文件模块 如果是相对路径,则require方法会将相对路径转成绝对路径,并以绝对路径作为索引,将编译结果放入缓存。
  3. 自定义模块 Node会逐个尝试 模块路径 中的路径,直到找到文件为止。

3 文件定位

  1. 扩展名会按照.js、.json、.node的顺序补足扩展名,依次尝试( json和node添加扩展名会加快一点速度 )
  2. 如果得到的是一个目录,会按照以下顺序处理:
    1. 查找package.json,并解析,取出main属性指定的文件名进行分析
    2. 若main指定的文件名错误或者没有package.json,Node会将index作为文件名,补足扩展名依次查找
    3. 若未找到任何文件,抛出查找失败的异常。
模块的编译

Node会根据各种不同的扩展名,进行不同策略的载入

  1. js文件 通过fs模块同步读取后编译执行
  2. node文件 通过dlopen()方法加载最后编译生成的文件
  3. json文件 通过fs模块同步读取,然后返回JSON.parse的结果
  4. 其他扩展名均按照js处理

1. JS模块的编译

Node会对JS文件进行一个包装,相当于套上一个函数的壳子

// js文件
var Math = require('math')
exports.area = radius => Math.PI * radius * radius
// 包装后的效果
(function (exports, require, module, __filename, __dirname) {
  var Math = require('math')
  exports.area = radius => Math.PI * radius * radius
})

// 函数执行后,会将 exports 返回给执行方

这也是我们为什么能够在Node里面能够直接使用 exports, require, module, __filename, __dirname这些变量的原因。 注意: exports是一个形参,指向 module.exports ,所以赋值操作的时候需要避免 exports = { xx:X }的操作

2. C/C++模块编译

Node调用process.dlopen()方法加载C/C++模块,这个主要是指.node扩展名的文件,.node扩展名的文件在window和 ~nix平台下其实对应了不同的文件,分别为.dll 和 .so,process.dlopen()对这两种文件进行了一个兼容载入处理

3. JSON文件

.json文件载入后对调用 JSON.parse方法解析,并赋值给模块的exports属性,所以.json文件读取时不需要用fs模块,直接require()即可,require还可以享受到模块缓存。