起因是开发时的一个报错信息:Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization

由于报错信息不明确,网上也搜不到明确的原因解释和解决方法,所以自行排查了很久才逐渐找到原因。要说怎么排查,就是最笨的也是最有效的“代码删除法”,即从入口文件开始一行行删代码,直到定位到具体出错位置,然后凭借自身知识和经验判断出问题原因。

原因就是import的循环引用导致webpack无法正确解析。

一、循环引用简介

最简单的 a 引用了 b,b 又引用了 a,这就产生了循环引用。
复杂点的无非就是链路长一些,例如 a -> b -> c -> d -> a。

循环引用可能会导致内存栈溢出。但也不是一定会有问题,比如 b 导出了两个方法,a 引用的是 b 导出的方法 fn1,而 b 是在方法 fn2 里引用的 a,这种情况其实是不会有问题的。

但由于可能的风险,且难以发现,所以编写代码时还是尽量规避使用循环引用。

二、在项目中排查

利用 webpack 的插件 circular-dependency-plugin 来排查项目中的循环引用问题非常方便。
(并非所有的循环引用都会有问题,所以这个插件建议在需要排查问题时使用,开发时是否开启看个人需要。)

  1. 安装插件:
npm i circular-dependency-plugin -D
  1. 在 webpack 配置文件里添加 plugins 配置。
const CircularDependencyPlugin = require('circular-dependency-plugin')

export default {
  ...,
  plugins: [
    ...,
    new CircularDependencyPlugin(
      {
        exclude: /node_modules/,
        include: /src/,
        failOnError: false,
        allowAsyncCycles: false,
        cwd: process.cwd(),
      }
    ),
  ]
}
  1. 重启项目,在启动命令行里就能看到循环引用的警告信息,插件会帮你定位出问题的文件路径。

三、esm的引用处理

ESM(es modules,即es6模块化)是编译时加载,和commonJs的运行时加载不同。esm是尽量的静态化,编译时就能确定模块的依赖关系,正因为此,esm能够通过静态分析进行tree shaking优化。

代码示例:

  • main.js
import a from './a'
console.log(a)
  • a.js
console.log('执行a')
import b from './b'
console.log(b)
console.log('导入b之后')
export default '我是a模块'
  • b.js
console.log('执行b')
import a from './a'
console.log(a)
console.log('导入a之后')
export default '我是b模块'
  • 执行main.js,输出结果:
// main.js里import了a,先进到a解析
// import语句提前解析,a里import了b,所以暂停a的解析,进到b解析
// b里import了a,但a此时还未执行完,拿到的a值是undefined,然后往下继续解析b
b.js:1 执行b
b.js:3 undefined
b.js:4 导入a之后

// b解析完并拿到了b的导出结果,开始回到a里继续往下解析
a.js:1 执行a
a.js:3 我是b模块
a.js:4 导入b之后

// a解析完后,回到main.js解析执行
main.js:2 我是a模块

esm的import命令是在编译阶段就执行,优先于自身内容执行。
esm并不关心是否存在循环引用,只是生成一个指向被加载模块的引用,代码未执行时,这个引用的值就是undefined。

四、webpack 的引用处理

即使 esm 里循环引用没有异常,在webpack编译时可能也会报错,毕竟webpack会把 esm 降级为 es5,降级处理方式上可能会有一定的差异。

同样的上述示例代码,webpack(v5.64.4)处理后在执行console.log(a)语句时就会出现异常报错:Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization

  • esm的处理方式是,先静态分析import,然后动态export导出(导出引用)。
  • webapck的处理方式是,先将所有export提到了模块的开始,然后import提升。

五、解决方法

首先说明下思路,循环引用肯定是有问题的,所以解决方法不是去兼容循环引用,而是避免循环引用。

1、引用抽离

就是把有循环引用的地方抽离到另一单独文件里,抽离的这个文件只供其他地方import引用,自身不import可能导致循环的模块。

2、导出函数

把之前默认的导出对象改成导出函数的形式,从函数返回值里取导出结果。
由于每一个函数都会形成一个单独的局部作用域,不同的作用域有着不同的数据引用地址。
这样每次引入的结果都是一个新的引用,不会冲突,这种情况webpack也能正常的处理。