为什么不推荐你使用模块导出使用 index 一把梭
预设
假设你有一个这个目录
.
├── components
│ └── a.js
├── utils
│ ├── a.js
│ ├── b.js
│ └── index.js
├── main.js
└── package.json
你的 main.js 导出 a.js 的 App 组件并渲染
// main.js
import { App } from "./components/a.js";
循环引用
现在你的 App 组件里定义了一个 compute 函数,需要用到 utils/b 里面的一个常量
// utils/b.js
export const B = 2;
// components/a.js
import { B } from "../utils/b";
export function compute() {
return B * 2;
};
export const App = () => {
return 'App';
};
你调用了 node --experimental-specifier-resolution=node main.js,一切都很正常
但是有一天你觉得 ../utils/b 依赖导入太长了,你想要改成 ../utils
index 一把梭
这个很容易实现,而且很常用,因为 ES Module 的模块解析符算法里面 ../utils 的模块解析符中,如果 utils 是一个目录,默认会解析成 ../utils/index.js
所以你把代码改成了下面这样
// utils/index.js
export * from "./a";
export * from "./b";
// components/a.js
import { B } from "../utils";
export function compute() {
return B * 2;
};
export const App = () => {
return 'App';
};
因为 utils/a 也需要导出,所以在 index 这里统一导出了,下面是 a.js 的内容
// utils/a.js
import { compute } from "../components/a";
console.log(compute());
结果再次调用的时候就报错了
node --experimental-specifier-resolution=node main.js
cycle-ref/components/a.js:4
return B * 2;
^
ReferenceError: Cannot access 'B' before initialization
at compute (cycle-ref/components/a.js:4:5)
at cycle-ref/utils/a.js:3:13
at ModuleJob.run (node:internal/modules/esm/module_job:183:25)
at async Loader.import (node:internal/modules/esm/loader:178:24)
at async Object.loadESM (node:internal/process/esm_loader:68:5)
at async handleMainPromise (node:internal/modules/run_main:63:12)
原因分析
依赖关系分析
components/a -> utils/butils/a -> components/a
本来是不存在循环引用的,但是中间加入 index 之后就会有了
components/a -> utils -> utils/a -> components/a
-> utils/b
循环引用出现了,components/a -> utils -> utils/a -> components/a,因为直接从 utils 导入会直接导入一遍 utils 里所有定义过导出的模块,即使你只是使用其中的一个模块
解决方案
第一种方案就是不要使用 utils 这种集中导出的方式(推荐),比如
import { B } from "../utils/b";
export function compute() {
return B * 2;
};
export const App = () => {
return 'App';
};
第二种方案就是,把 utils/b 的导出放到 utils/a 的前面,比如
export * from "./b";
export * from "./a";
这种方案的解决原理如下
components/a -> utils -> utils/b
# 此时 cache 会存入 components/a 的相关模块
-> utils/a -> cache -> components/a
# 因此能够正确拿到对应模块
但是不太推荐这种方式,因为 a 模块可能是别人写的,你不一定知道存在循环引用的问题
package.json 示例
为了支持 ES Module 的导入导出,你需要将 package.json 的 type 字段设置为 module,并且在运行时需要使用实验特性选项,比如
node --experimental-specifier-resolution=node main.js
绝大部分 node 稳定版本的 ES Module 还是实验特性,大部分通过 Babel 转译实现