为什么不推荐你使用模块导出使用 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/b
utils/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
转译实现