likes
comments
collection
share

一. Node模块机制

作者站长头像
站长
· 阅读数 22

1.CommonJs规范

CommonJs对模块的定义十分简单,主要分为3部分

1.1模块引用

const adc = require("abc")

在CommonJs规范中,存在require()方法,这个方法接受模块标识,会引入一个模块标识到当前上线文

1.2模块定义

对应导出功能,上下文提供了一个exportS对象用于导出当前模块想要导出的内容,并且它是唯一的导出出口。 在模块中还有一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式

exports.add = function (){
  console.log("abc")
}

1.3.模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.或..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js

2.Node模块的实现

Node的模块机制并没有完全遵守规范,而是做了一些取舍。尽管exportS,require和module听起来十分简单,但是Node在实现他们的过程中经历了什么,我们需要知晓

在Node中引入模块,需要经历如下三个步骤:

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

Node的模块分为两类:一类是Node提供的模块,称为核心模块;另一种是用户编写的模块,称为文件模块

核心模块部分在Node编译的过程中,编译进了二进制执行文件。在Node进程启动的时候,部分核心模块文件会被加载进内存,所以在导入这部分模块时,文件定位和编译执行可以省略,并且在路径分析中优先判断,所以它的加载速度是最快的

文件模块是在运行时动态加载,需要完成的导入路径,速度比核心模块慢

2.1 优先从缓存加载

Node为了提高性能,会对引入过的模块进行缓存,且缓存的是编译执行后的对象。以减少二次引用后的开销。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的策略,这是Node模块加载的第一优先级。不同在与核心模块的缓存检查优先于文件模块的缓存检查。

2.2路径分析和文件定位

我们在引入文件时使用require()方法,并将路径标识传给该方法。

因为模块标识有好几种类型,对于不同的模块标识,模块的查找和定位还有不同程度的差异。

2.2.1.模块标识符分析(也可以叫路径分析)

Node中模块标识符主要分为以下几类

  • 核心模块
  • .或..开始的相对路径文件模块
  • 以/开始的绝对路径文件模块
  • 非路径形式的文件模块,比如自定义的connect模块

核心模块

核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载速度最快

路径形式的文件模块

在分析路径模块时,require()方法会将路径转换为真实路径,并以真是路径为索引,将编译执行后的结果放到缓存中,方便二次加载,提高加载速度

自定义模块

自定义模块可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方法中最慢的一种

在介绍自定义模块的查找方式之前,首先介绍一下模块路径的概念

模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。关于这个数组,如下图:

一. Node模块机制

可以看出,模块路径的生成规则如下

  • 当前文件目录下的node_modules目录
  • 父目录下的node_modules目录
  • 父目录的父目录下的node_modules目录
  • 沿路径向上逐级递归,知道根目录下的node_modules目录

2.2.2文件定位

如果是二次加载则不需要路径分析,文件定位和编译执行。这大大提高了再次加载模块时的效率

但是在文件的定位过程中,还有些细节要注意,包括文件扩展名的分析,目录和包的处理

文件扩展名分析

require()方法在分析路径标识的时候,允许标识符中不包括文件扩展名的情况。这也是CommonJS规范所允许的,这种情况下Node会按照js,json,node的顺序去补足扩展名,依次尝试。

目录分析和包

在分析标识符的过程种,require()通过分析文件扩展名之后,可能没有查到对应文件,但是却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常出现,此时Node会将目录当作一个包来处理。

在这个过程中,Node对CommonJs包规范进行了一定程度的支持。

首先,Node在当前目录下查找package.json(CommomJs包规范定义的包描述文件),通过JSON.Parse()解析出包描述对象,从中取出main属性指定的文件进行定位。如果文件缺少扩展名,将会进入扩展名分析的步骤。

如果main指定的文件名错误,或者这个包中没有package.json文件,Node会将index当作默认文件名,然后依次查找js,json,node

如果目录分析时没有定位到任何文件,则会进入下一个模块路径进行查找,直到模块路径的终点,如果依旧没有定位到,则会抛出查找失败的异常。

2.3模块编译

在Node中每个模块都是一个对象。

编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后又根据路径载入并编译。不同种类的文件,Node的编译方式不同,这里主要介绍js文件

在定位到文件模块后,Node会先给模块做一层包装。包装成一个函数,形参为,exports,require,module(模块本身),__filename,__dirname

然后包装后的代码会通过vm原生模块的用runInThisContext()方法执行,返回一个function对象。最后将当前模块的对象的exports属性,require()方法,module(模块对象本身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function执行。

执行之后exports属性被返回给了调用方。

这就是这些变量没有被定义,却可以在模块中使用的原因

转载自:https://juejin.cn/post/7362464700879781939
评论
请登录