nodejs之模块加载
本文介绍 nodejs
中的模块加载方式及用法。
前言
在 Node.js 模块系统中,每个文件都被视为一个独立的模块。 例如,假设有一个名为 foo.js 的文件:
1 | const circle = require('./circle.js'); |
在第一行中, foo.js 加载了与 foo.js 在同一目录中的 circle.js 模块。
以下是 circle.js 的内容:
1 | const { PI } = Math; |
circle.js 模块导出了 area() 和 circumference() 函数。 通过在特殊的 exports 对象上指定额外的属性,可以将函数和对象添加到模块的根部。
模块内的本地变量是私有的,因为模块由 Node.js 封装在一个函数中(详见模块封装器)。 在这个例子中,变量 PI 对 circle.js 是私有的。
可以为 module.exports 属性分配新的值(例如函数或对象)。
下面的例子中, bar.js 使用了导出 Square 类的 square 模块:
1 | const Square = require('./square.js'); |
square 模块定义在 square.js 中:
1 | // 赋值给 `exports` 不会修改模块,必须使用 `module.exports`。 |
模块系统在 require(‘module’) 模块中实现。
模块定义
在 nodejs
中,一个模块的定义或者说导出方式是使用 module.exports
或者 exports
。
1 | // foo.js |
如上,我们定义了 foo.js 模块,你可以是使用 require
函数来引用她。
使用 exports
向外提供接口。
1 | // baz.js |
如上,baz 模块向外提供了 say
方法。
模块导入
在 nodejs
中,使用 require
函数来引用一个模块。
1 | // bar.js |
如上代码,我们引用了 foo.js 模块,并使用了 foo.say
方法。
exports 和 module.exports
我们已经知道了在 nodejs 中 模块的定义及引用方式,那么 exports
和 module.exports
这两种导出方式有何不同呢? 当你使用 require
函数引用一个模块时,她返回的永远是 module.exports
对象。
类似于对 exports
重新赋值对外导出接口的方式是无效的:
1 | // foo.js |
1 | // bar.js |
如上代码的导出方式可以简单的这样理解:
1 | var a = {} |
所以,你可以为 exports
快捷方式添加属性,但是不可以对其直接重新赋值。
nodejs 中的模块加载规则
模块类型
在 nodejs
中,存在不同类型的模块:外部模块,核心模块,文件模块,文件夹模块。那么对于这些模块的加载规则都有所不同。
外部模块
所谓 外部模块
指的是使用 npm
管理的第三方模块,即安装在 node_modules
文件夹中的模块。比如:koa
等。
核心模块
核心模块
指的是 nodejs
自身提供的一些内部模块,无需安装,可直接 require
引用。比如:http, path, fs
等。
文件模块
在本地自己新建的 *.js
、*.json
、*.node
被称作 文件模块
。
目录模块
根据模块加载规则,nodejs
中,我们认为一个文件夹也是一个模块,也就是说,我们可以 require('./directory')
来引用一个模块。
1 | // foo 是一个文件夹,该文件夹下存在一个 index.js 文件 |
具体为什么可以这么引用,在接下来的模块加载规则中会详细说明。
加载规则
先看如下伪代码描述的高级算法:
1 |
|
翻译过来的具体行为如下:
假设你要引用一个模块 x (x 可以是 nodejs 的核心模块,外部第三方模块,文件模块或者一个目录)。
1 | const x = require(x) |
判断模块 x 是否为 nodejs 核心模块,如果是,直接返回该
核心模块
。不满足1时,判断 x 是否是以
/
、./
、../
开头,如果是,这 x 被认为是本地模块(文件模块或目录模块)
。如果根据路径找到的是文件类型
*.js、*.json、*.node
,则根据扩展名解析返回为不同的数据。如果是 x.js 文件,则返回 javascript 文本。
如果是 x.json 文件,则将其解析为 javascript 对象并返回。
如果是 x.node 文件,则将其作为 node 的二进制插件并返回。
如果根据路径找到的是文件夹类型,那么 node 会继续在该文件夹下查找以
index.*
的文件,包括index.js
、index.json
、index.node
等文件并将其返回。像在使用webpack
这样的构建系统的@vue/cli
项目中,index.vue
也会被匹配到。如果根据以上条件都没有匹配到模块文件,那么 node 会将 x 视为
第三方外部模块
。满足条件 8 时,node 会以当前引用 x 模块的文件为原点,递归向外查找
node_modules
文件夹,直到项目根目录。只要找到了第一个
node_modules
文件夹,那么 node 便会停止递归查找,并在该node_modules
文件夹下查找 x 文件夹。如果在
node_modules
中没有找到 x 文件夹,那么抛出not found
错误。如果在
node_modules
找到了 x 文件夹,那么 node 会查看解析 x 文件夹下的package.json
文件再查看该
package.json
文件中是否存在main
字段,该字段表示 x 模块的入口文件路径,node 会根据该文件路径解析出具体的文件并将其返回。如果
package.json
文件都不存在,那么 node 会尝试查找 x 文件夹下是否存在一个index.js
的文件,如果有,直接将其返回,如果没有抛出not found
错误。至此,如果根据以上规则都没有找到指定的模块,那么 node 就不会再查找了,直接抛出
not found
错误。
模块封装器
在执行模块代码之前,Node.js 会使用一个如下的函数封装器将其封装:
1 | (function(exports, require, module, __filename, __dirname) { |
通过这样做,Node.js 实现了以下几点:
它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象。
它有助于提供一些看似全局的但实际上是模块特定的变量,例如:
实现者可以用于从模块中导出值的 module
和 exports
对象。
包含模块绝对文件名和目录路径的快捷变量 __filename
和 __dirname
。