Node系列 — 模块 requrie 机制

722 阅读5分钟

CommonJS 规范

Node 的发展使得 npm 模块库的内容日益丰富,这些 npm 的生成也遵循着 CommonJs 的规范。如今对于前端开发者已经离不开 npm ,所以 CommonJs 的规范及 require()加载机制对于开发者来说也就极其重要。

CommonJs 对于模块的定义分为 模块引用、模块定义、模块标识 三个部分

模块引用

const sum = require('sum')

模块定义

CommonJs 规范中,使用 require方法来引入模块。在模块中的上下文提供了一个 exports 对象和 module.exports 对象用于导出当前模块的变量或方法。

module.exports 导出模块:

module.exports = {
   a: 2
}

exports 导出模块:

exports.a = 2

关于 exports、module.exports 的区别:require方法加载到的只有 module.exports 这个对象,而我们在编写模块时用到的 exports 对象实际上只是对module.exports 的引用

模块标识

模块标识就是 require() 的时候传入的参数。 标识符代表着包名,也可以是以 ...开头的相对路径或者绝对路径。

模块实现

Node 中模块分为两类:

  • 一类是 Node 内置模块,又称为核心模块(httpfs等)
  • 一类是用户自定义模块,又称为文件模块

当我们在当前上下文中引入模块时,需要经历以下四个步骤:

  1. 缓存加载
  2. 路径分析
  3. 文件定位
  4. 文件编译执行

模块缓存加载

不论是核心模块还是文件模块,Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。Node 缓存的是编译过的对象。缓存加载是第一优先级,核心模块的缓存加载会优先于文件模块的缓存。

路径分析

const sum = require('sum')
const sum = require('../sum')
const sum = require('/sum')

require方法接受一个模块标识符作为参数,sum../sum/sum 即为模块标识符。Node 正是基于这样的标识符进行模块查找。

标识符分类:

  • 核心模块, http , fs , path
  • ... 开头的相对路径模块
  • / 开头的绝对路径模块
  • 自定义模块,比如众多 npm 包
核心模块的加载:

核心模块的加载仅此于缓存加载,在 Node 进程启动时,核心模块就被编译为二进制代码加载进内存中。

PS: 如果想加载一个标识符与核心模块相同的模块,我们必须换一个标识符或者换用路径的方式加载。

这里测试一下, 新建 test.jshttp.js 在同一目录下: 新建一个 test.js 文件:

const a = require('http')
console.log(a)

新建一个 http.js 文件

module.exports = {
    a : 1
}

打印如下: image.png 由此可见,打印内容为 http 模块内容。如果我们试图加载一个与核心模块标识符相同的模块,读取的还是核心模块。那如何加载与核心模块相同标识符的模块,可以采用相对路径的方式或修改标识符。 修改一下以相对路径方式引入:

const a = require('./http')
console.log(a)

效果如下:

image.png

路径形式的模块

...开头的标识符,在引入时会将相对路径转换为绝对路径,并以绝对路径为索引将文件编译执行后的对象添加到缓存,在二级加载时更快。

文件模块的加载指明了文件的位置,所以在查找的过程中很快,加载速度慢于核心模块。

自定义模块

自定义模块通常是指开发者自行封装的模块,比如丰富的 npm 社区,Node 在加载自定义模块的时候会遵循一套路径规则,该规则表现为一套路径数组。 关于数组的详细规则,我们可以自行测试: 创建一个测试文件,find_path.js

console.log(module.paths)

Mac 系统下,输出路径数组如下

[
  '/Users/zjg/study/demo/node_modules',
  '/Users/zjg/study/node_modules',
  '/Users/zjg/node_modules',
  '/Users/node_modules',
  '/node_modules'
]

可以看出,模块路径查找的规则是这样:

  1. 查找当前文件目录下的 node_modules 目录
  2. 查找父目录下的 node_modules 目录
  3. 父目录的父目录下的 node_modules 目录
  4. ...
  5. 根目录下的 node_modules 目录

这种查询目录的方式和 JavaScript 的原型链或作用域的查询方式很类似。

文件定位

当我们使用 require() 的时候,如果标识符中不包含文件扩展名,比如

require('mytest')

这种情况下,Node 会按照 .js.json.node 的次序补充扩展名尝试加载模块。

如果 mytest 是一个 npm 包文件,Node 通过补充文件扩展名没有找到对应文件,找到的是一个目录,这时 Node 会把这个目录当作一个包来处理。

NodeCommpnJs 包规范做了一定支持,首先,Node 会在当前包目录下找到 packsge.json 文件,通过 JSON.parse() 解析出包描述对象,找到 main 属性指定的入口文件位置。如果没有文件扩展名,则先进行扩展名分析。如果 main 属性指定的文件不存在,或者当前包目录没有 package.josn 文件, Node 则会尝试查找 index 文件名,一次查找 index.jsindex.josninndex.node

如果当前目录分析没有找模块,Node 则按照模块路径数组下一个目录开始查找,依次查找完成,如果最终没有找到模块,则会抛出错误,以上便是 Node 加载一个模块时所做的全部处理。