模块化
模块化简述
模块化很好的解决了如下问题:
- 变量名冲突
- 作用域冲突
- 引入多个 script
在社区中推广了许多模块化规范,如今最常用的两个就是 CommomJS 和 ES Module,其中 Node 的模块化使用的就是 CommonJS 规范。
CommonJS 与 Node
CommonJS
是一种模块化规范,简称 CJS
,最初是在浏览器以外的地方使用的,Node 是 CommonJS
在服务端一个具有代表性的体现。
- 在 Node 中 每一个 JS 文件 就是一个单独的模块
- 模块中包含
CommonJS
核心变量:exports
、module.exports
和require
模块化导出
exports
和 module.exports
负责对模块中的内容进行导出,下面演示一下使用 CommonJS
规范将模块中的内容导出的基本方式。
只需要将当前需要导出的内容作为属性赋值给 exports
全局对象。
const name = 'Yukee-798';
const age = 18;
let message = 'I am Yukee.';
function sayHello() {
console.log('hello' + name);
}
// CommonJS 导出规范
exports.name = name;
exports.age = age;
exports.message = message;
exports.sayHello = sayHello;
每个模块中都具有一个 exports
对象,默认情况下是一个不含有任何属性的对象 {}
,并且它们在堆内存中都是独一无二的,CommonJS
模块化规范导出模块的本质就是给模块中的 exports
对象添加相应的 key-value
,其中 value
就是我们需要导出的内容。
简单来说,CommonJS
模块化规范实现的模块化本质就是 对象的引用赋值。
module.exports 与 exports
下面我们来讨论一下 module.exports
,有了模块中有了 exports
对象就可以正常导出模块中的内容了,那 module.exports
有啥用呢?
实际上在 CommonJS
规范中没有 module.exports
这个概念,它是在 Node 中单独实现的,本质上 module.exports === exports
,而 module
对象就是当前模块对 Module
类的实例化,可以理解为在 Node 中每一个 js 文件对应独一无二的 module
对象。
在 Node 中的模块化导出,实际上导出的是 module.exports
这个变量,在初始状态下 module.exports = exports = {}
,因此才可以使用 exports
来操控导出的对象,不过一旦我们手动修改了 module.exports
的取值,那么 exports
就无法进行内容导出了,下面可以来看一下他们之间的区别。
// bad
exports = 123;
// good
module.exports = 123;
当我们执行 exports = 123
时,实际上并没有将 123 数值导出,此时 module.exports
还默认是一个空对象 {}
,因此外部引入该模块时依旧是一个空对象 {}
,因为本质上导出的是 module.exports
这个变量。
当执行 module.exports = 123
时,此时导出的数据就由空对象 {}
变为了 123
。这样就很容易能够解释:
使用 exports
导出内容时,必须如像下面这样做,并且它只能够导出对象。
// ... 省略
exports.name = name;
exports.age = age;
exports.message = message;
exports.sayHello = sayHello;
使用 exports
是不能够直接赋值给一个对象来导出内容的。
// ... 省略
// bad
exports = {
name,
age,
message,
sayHello,
};
因为本质上导出的是 module.exports
变量,因此使用 module.exports
就可以这样做。
// ... 省略
// good
module.exports = {
name,
age,
message,
sayHello,
};
并且还可以导出非对象的数据。
module.exports = 213;
模块化导入
require
负责导入其他模块(自定义模块、系统模块、第三方库模块)内容到当前模块中,下面演示一下使用 CommonJS
规范将上述导出的内容导入到当前模块的基本方式。
需要调用 require
函数,它会返回目标模块中的 module.exports
变量。
const { name, age, message, sayHello } = require('./bar');
// 接下来就可以在 main.js 这个模块内部使用导入的数据了
这里 require('./bar')
的底层中所做的一切就是为了获取 bar.js
模块内部的 module.exports
变量,它的返回值就是 bar.js
模块内部的 module.exports
变量,并且在调用 require('./bar')
的时候还会完全执行一遍 bar.js
中的代码。
require 查找规则
require(X)
情况一
X 是 node 内置的核心模块,比如 path
、fs
、http
等,那么直接返回该模块。
情况二
X 以 /
或 ./
或 ../
开头,说明我们引入的模块是一个本地模块,那么就会去对应的目录内查找,需要注意的是 /
表示从硬盘的根目录查找
- 如果带有后缀名,比如
require(./bar.js)
则会按照后缀名格式查找对应文件 - 如果没有后缀名,比如
require(./bar)
,会按如下顺序:- 查找
./bar
文件,找到则返回 - 查找
./bar.js
文件,找到则返回 - 查找
./bar.json
文件,找到则返回 - 查找
./bar.node
文件,找到则返回 - 如果上述类型的文件都没有查到,则会查找文件夹
./bar
,然后在里面依次按照index.js
、index.json
、index.node
的顺序查找
- 查找
情况三
X 不是核心模块,也不是本地模块,比如我们在 /Users/yukee-798/Downloads/daily/amain/Yukee-798.github.io/test
目录下的 main.js
文件中调用 require('abc')
,那么会按照如下顺序去查找 abc
模块:
'/Users/yukee-798/Downloads/daily/amain/Yukee-798.github.io/test/node_modules',
'/Users/yukee-798/Downloads/daily/amain/Yukee-798.github.io/node_modules',
'/Users/yukee-798/Downloads/daily/amain/node_modules',
'/Users/yukee-798/Downloads/daily/node_modules',
'/Users/yukee-798/Downloads/node_modules',
'/Users/yukee-798/node_modules',
'/Users/node_modules',
'/node_modules'
即先查找当前文件夹下是否有 node_modules
文件夹,没有则到上层文件夹查找 node_modules
... 一直返回上层文件夹,直到找到 node_modules
文件夹为止,然后再在 node_modules
里面去查找 abc
文件夹,返回里面的 index.js
模块中的 module.exports
变量。
important
上面的所有路径实际上是 module.paths
属性取值,类型为一个数组。
情况四
如果按照上面所有规则来查找后都没有找到目标模块,则会抛出 Not Found
错误。
模块加载过程
同步性
前面谈到只要使用 require()
引入一个模块时,目标模块中的所有代码都会执行一遍,并且这里补充说明一点,调用 require()
函数加载模块的过程是 同步的。
console.log('bar');
require('./bar');
console.log('main')
当我们执行命令行 node ./main.js
时:
- 调用
require
函数,发现是一个模块引入,会进行模块查找,然后到目标模块bar.js
中执行代码,打印 'bar' - 目标模块所有代码执行结束,
require
函数返回,接着执行main.js
剩余代码,打印 'main'
缓存性
先说结论,当模块第一次被加载时会被缓存起来,后续再到别的地方被导入时不会再被执行,而是直接使用缓存的内容,因此一个模块无论被导入多少次,里面的代码也只会执行一次,请看如下例子。
在 main.js
中分别导入了 bar
和 foo
,foo.js
中也导入了 bar
,当我们执行命令行 node main.js
时:
- 进入
main.js
执行到require('./bar')
时,进入bar.js
中执行代码 bar.js
里面到代码执行完毕后,接着返回main.js
里面执行require('./foo')
,然后会进入foo.js
中执行代码- 在
foo.js
中会执行require('.bar')
,但是bar
模块在之前已经被加载过了,因此只会返回缓存中的bar
模块的module.exports
变量,不会再执行一遍bar.js
中的代码
important
在每个模块中的 module.loaded
中记录了当前模块是否被加载过,是一个布尔类型值。
循环引入解决方案
如果在模块导入的过程中产生了闭环,比如下面这样。
本质上还是利用的 缓存性,这样并不会导致模块被重复循环执行。
对于一些复杂的模块引入结构,比如下面这样。
require('./baz');
require('./foo');
本质上导入顺序就是图的深度优先遍历,main -> baz -> qux -> jay -> bar -> foo
,简单了解下即可。
Node 环境中使用 ESModule
第一种方案就是将使用了 ESModule 的模块后缀名改为 .mjs
。
const sayHi = () => {
console.log("hi");
};
export default sayHi;
import sayHi from "./modules/foo.mjs";
sayHi();
第二种方案是在 package.json 的 type 字段取值为 module
。