为什么不推荐你使用模块导出使用 index 一把梭

2,375 阅读2分钟

为什么不推荐你使用模块导出使用 index 一把梭

预设

假设你有一个这个目录

.
├── components
│   └── a.js
├── utils
│   ├── a.js
│   ├── b.js
│   └── index.js
├── main.js
└── package.json

你的 main.js 导出 a.jsApp 组件并渲染

// 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)

原因分析

依赖关系分析

  1. components/a -> utils/b
  2. 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.jsontype 字段设置为 module,并且在运行时需要使用实验特性选项,比如

node --experimental-specifier-resolution=node main.js

绝大部分 node 稳定版本的 ES Module 还是实验特性,大部分通过 Babel 转译实现

参考资料

  1. 常见的 JS 循环导入错误 - 知乎 - 松若章