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++编译后的二进制文件,纯前端一般很少接触这个类型。