require查找模块机制
# 简单例子
老规矩,讲原理前我们先来一个简单的例子,从这个例子入手一步一步深入原理。Node.js里面如果要导出某个内容,需要使用module.exports
,使用module.exports
几乎可以导出任意类型的JS对象,包括字符串,函数,对象,数组等等。我们先来建一个a.js
导出一个最简单的hello world
:
// a.js
module.exports = "hello world";
2
然后再来一个b.js
导出一个函数:
// b.js
function add(a, b) {
return a + b;
}
module.exports = add;
2
3
4
5
6
然后在index.js
里面使用他们,即require
他们,require
函数返回的结果就是对应文件module.exports
的值:
// index.js
const a = require('./a.js');
const add = require('./b.js');
console.log(a); // "hello world"
console.log(add(1, 2)); // b导出的是一个加法函数,可以直接使用,这行结果是3
2
3
4
5
6
# require会先运行目标文件
当我们require
某个模块时,并不是只拿他的module.exports
,而是会从头开始运行这个文件,module.exports = XXX
其实也只是其中一行代码,我们后面会讲到,这行代码的效果其实就是修改模块里面的exports
属性。比如我们再来一个c.js
:
// c.js
let c = 1;
c = c + 1;
module.exports = c;
c = 6;
2
3
4
5
6
7
8
在c.js
里面我们导出了一个c
,这个c
经过了几步计算,当运行到module.exports = c;
这行时c
的值为2
,所以我们require
的c.js
的值就是2
,后面将c
的值改为了6
并不影响前面的这行代码:
const c = require('./c.js');
console.log(c); // c的值是2
2
3
前面c.js
的变量c
是一个基本数据类型,所以后面的c = 6;
不影响前面的module.exports
,那他如果是一个引用类型呢?我们直接来试试吧:
// d.js
let d = {
num: 1
};
d.num++;
module.exports = d;
d.num = 6;
2
3
4
5
6
7
8
9
10
然后在index.js
里面require
他:
const d = require('./d.js');
console.log(d); // { num: 6 }
2
3
我们发现在module.exports
后面给d.num
赋值仍然生效了,因为d
是一个对象,是一个引用类型,我们可以通过这个引用来修改他的值。其实对于引用类型来说,不仅仅在module.exports
后面可以修改他的值,在模块外面也可以修改,比如index.js
里面就可以直接改:
const d = require('./d.js');
d.num = 7;
console.log(d); // { num: 7 }
2
3
4
# require
和module.exports
不是黑魔法
我们通过前面的例子可以看出来,require
和module.exports
干的事情并不复杂,我们先假设有一个全局对象{}
,初始情况下是空的,当你require
某个文件时,就将这个文件拿出来执行,如果这个文件里面存在module.exports
,当运行到这行代码时将module.exports
的值加入这个对象,键为对应的文件名,最终这个对象就长这样:
{
"a.js": "hello world",
"b.js": function add(){},
"c.js": 2,
"d.js": { num: 2 }
}
2
3
4
5
6
当你再次require
某个文件时,如果这个对象里面有对应的值,就直接返回给你,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports
加入这个全局对象,并返回给调用者。这个全局对象其实就是我们经常听说的缓存。**所以require
和module.exports
并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行。**再看看这个对象,因为d.js
是一个引用类型,所以你在任何地方获取了这个引用都可以更改他的值,如果不希望自己模块的值被更改,需要自己写模块时进行处理,比如使用Object.freeze()
,Object.defineProperty()
之类的方法。
# 模块类型和加载顺序
这一节的内容都是一些概念,比较枯燥,但是也是我们需要了解的。
# 模块类型
Node.js的模块有好几种类型,前面我们使用的其实都是文件模块
,总结下来,主要有这两种类型:
- 内置模块:就是Node.js原生提供的功能,比如
fs
,http
等等,这些模块在Node.js进程起来时就加载了。- 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
# 加载顺序
加载顺序是指当我们require(X)
时,应该按照什么顺序去哪里找X
,在官方文档上有详细伪代码 (opens new window),总结下来大概是这么个顺序:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules
下面找。- 还找不到就报错了。
# 加载文件夹
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json
,如果有就找里面的main
字段,main
字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json
里面的main
字段吧,比如jquery
的main
字段就是这样:"main": "dist/jquery.js"
。- 如果没有
package.json
或者package.json
里面没有main
就找index
文件。- 如果这两步都找不到就报错了。
# 支持的文件类型
require
主要支持三种文件类型:
- .js:
.js
文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports
作为require
的返回值。- .json:
.json
文件是一个普通的文本文件,直接用JSON.parse
将其转化为对象返回就行。- .node:
.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。