模块
模块
node常见的模块
- ECMAScript Modules(ESM)
- CommonJS Modules (CJS)
浏览器和Node都支持ESM,Node从v12版本开始正式支持ESM,但需要package.json配置type或要添加命令行参数--input-type
CJS为Node从始至终都支持的模块格式,写到.cjs(自动当CJS模块处理)或.js结尾的文件中
浏览器
各个node环境下的模块加载
package.json
exports 和 imports 配置
这里提到的配置为node环境独有,vite、webpack不能识别这个配置
{ |
exports
子路径导出,可以配置当前模块的多个入口,也可以重写import和require导入方法
imports
子路径导入,可以配置私有包等
通用配置
下面提到的通用配置就可以在所有环境使用
workspaces
{ |
当执行npm i时,会在项目的node_modules文件夹下创建一个符合链接,链接到这个a包,当然a包中得有package.json
还是挺巧妙的,node_modules文件夹不论那种找包策略都会去找,这样就不存在兼容性文件
CJS
使用require()方法来加载模块,且加载的模块必须是同步的
function require(/* ... */) { |
复制了node官方文档关于require方法的代码,基本就是这个导入方法的原理
题外话,require()要支持ESM模块加载了 (目前还没正式上线2025.8.18)
缓存
require加载模块会node会创建加载这个模块的缓存,是根据模块的名字来控制缓存的,并且大小写敏感。但是,当一个文件和另一个名字同名,内容不同,导入可能会出问题,不会建立新文件的缓存
循环导入
a.js导入b.js,b.js在导入a.js,之后在main.js中导入他们俩
比如main.js中,导入a语句在最顶部
- 就先进入a.js执行,解析到导入b.js就会又去执行b.js
- 之后解析b.js又配到导入a了,就先创建一个未完成的
exports对象,是a.js的exports的拷贝对象,返回给b,让b先正常解析 - 等a解析完了,再回过头来替换b中的a导入
找包策略
如果导入的包路径不已相对路径开头'/', '../', or './'
那么就会按下面方式找
- 找当前路径下的
node_modules - 找上一级目录的
node_modules - 再找上一级的
node_modules,直到系统根目录(比如mac上的/目录)
CJS支持文件夹查找,文件夹根目录得包含package.json,node会把这个文件夹当成一个模块
PS. 注意
CJS不支持node的私有包导入
require.resolve() |
可以看到常用的变量,exports、__dirname等,这也就是可以直接在cjs模块中访问的原因
但同时,也要看作用域,被限制到了一个函数内,cjs全局的一些this指向就要注意了,并不是globalThis
require函数
模块使用 require引入,伪代码如下
// require函数的伪代码 |
ESM
Node ESM环境
支持
- 协议
file:url,data:url,node:url - 裸模块(Bare specifier package)node_modules里的
- 私有模块
找包策略
找包策略大体和cjs相同,不过需要注意要考虑上面支持的这些url
简单来说会调用ESM_RESOLVE方法,对有拓展名的导入url还会调用ESM_FILE_FORMAT方法格式化文件(仅支持json,js,cjs,mjs),如果不是这些拓展名就会报错了
循环导入
foo.js导入bar.js,bar.js在导入foo.js,之后在main.js中导入他们俩
// foo.js |
// bar.js |
// main.js |
执行结果
ReferenceError: Cannot access 'foo' before initialization |
比如main.js中,导入a语句在最顶部
- 就先进入foo.js执行,解析到导入bar.js就会又去执行bar.js
- bar.js中要使用foo.js(console输出的那句话)但是foo.js还没执行完毕,没有导出,所以直接报
ReferenceError
前端浏览器
浏览器环境需要<script type="module" src="path_to_js"></script>,src中引入的文件格式可以是.js或者是.mjs(准确说,设置了MIME类型text/javascript的文件)
加载
浏览器模块加载是异步的,并且每一个模块对应一个模块请求,如果遇到循环导入,和上文ESM处理方式一致
浏览器找文件策略
只能导入.js或.mjs文件,或者Content-Type设置text/javascript的文件
ESM的符号绑定
在ESM中的导出,导出的符号 export const foo = 'bar',在 import中导入这个符号,import {foo} from 'foo.js',导入的是这个符号的引用,意味着导入的 foo会随着导出模块中的 foo值的变化而变化
这是很奇怪的现象,按理说,模块的出现就是为了隔离变量,但是现在这种情况看似是打破了规则
同时还有两点要遵守:
- 导入绑定是只读的
- 实时绑定(Live Binding)
实时绑定:就是上面的描述,修改了导出模块中导出符号的值,所有导入模块的符号对应的值都会修改
export导出
在模块中的顶层变量会处在每一个模块的词法作用域里,不会污染到全局的环境
