使用 webpack 进行构建,我们在混用 commonjs 和 esModule 情况下(随便引几个 node_modules 三方库,这两种规范就凑齐了),模块解析很容易出现问题,特别是再搭配各种 babel 、 polyfil 插件时,情况就更复杂了。这边拿来分析一下。

先说结论:import、export 关键字与 module.exports、exports 关键字同时出现在一个文件中时,webpack 会产生构建冲突

如以下代码是错误的

1
2
import demo from 'demo'
module.exports = demo;

伴随产生的对比:

require 关键字永远都是安全的,可以任性的使用在 esModule 和 commonjs 文件中的任何位置

原因分析

先看 webpack 是怎么解析 commonjs 和 esModule 代码的。

举个栗子:

common.js

1
2
const b = 2;
module.exports = b;

common.output.js

1
2
3
4
function(module, exports) {
var b = 2;
module.exports = b;
}

es.js

1
2
const a = 2;
export default a;

es.output.js

1
2
3
4
5
6
7
8
9
10
11

function(module, __webpack_exports__, __webpack_require__) {

// 定义当前模块为 esModule
__webpack_require__.r(__webpack_exports__);

// 将变量 a 注册在 __webpack_exports__ 上作为 exports 的替代导出
var a = 2;
__webpack_exports__["default"] = (a);

}

可以看到,webpack 只是对 commonjs 模块包一层语法容器,而对 esModule 的操作会复杂一些。首先使用 r 函数作用标记当前 exports 的类型为 esModule, 然后会把自己定义的变量 __webpack_exports__ 作为 module.exports 的替代导出。(标记模块类型是否为 esModule 的目的是方便后面进行模块引用,判断是调用 __webpack_exports__ 还是调用 __webpack_exports__.default

r 函数代码如下:

1
2
3
4
5
6
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

重点来了,当我们在 commonjs 中使用 import 关键字时,会发生如下现象:

common.js:

1
2
import a from './a';
module.exports = a;

common.output.js

1
2
3
4
5
6
7
8
9
10
11
12
function(module, __webpack_exports__, __webpack_require__) {

// 定义当前模块为 esModule
__webpack_require__.r(__webpack_exports__);

// 这里的 module 中 exports 是 read only 属性,赋值时会发生错误
function(module) {
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./a.js");
module.exports = _a__WEBPACK_IMPORTED_MODULE_0__["default"];
}.call(this, __webpack_require__("./webpack/buildin/harmony-module.js")(module));

}

因为 webpack 在 esModule 中传入的 module 的 exports 属性是只读的,上述代码企图去给 exports 赋值,所以执行时会报错。

实际上我们期待的输出应该是这样子的

common.output.expected.js

1
2
3
4
5
6
7
8
9
10
function(module, __webpack_exports__, __webpack_require__) {

// 定义当前模块为 esModule
__webpack_require__.r(__webpack_exports__);

var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ "./a.js");
// 我们希望 a 变量挂在 __webpack_exports__上
__webpack_exports__ = _a__WEBPACK_IMPORTED_MODULE_0__["default"];

}

很遗憾,webpack 只能把 esModule 中 export 语法转换为如上的 __webpack_exports__ 属性挂载, 无法把 commonjs 中语法 module.exports 赋值转换为 __webpack_exports__ 属性挂载。

采坑场景

单纯如上原理非常清晰,我们只要在写代码时稍微注意下就可以完美的避过。

但是!项目中总会存在我们很少触碰的某些角落,这些地方总是暗藏杀机:

还是先上结论:webpack 在开启 tree-shaking 时,如果同时引入了 babel-transform-runtime-plugin 并开启 corejs2 polyfill 填充,在引入某些 commonjs 三方库时会报错

我的业务场景很简单:

  • 需要用到 webpack 的 tree shaking, 就要求把 babel 的模块分析关闭(module:false)。
  • 针对低端机想要自动引入不造成全局污染的 polyfill,那么非 babel-transform-runtime + corejs2 莫属

原因分析

  • 引入的 commonjs 库含有浏览器高级特性,需要 babel-transform-runtime + corejs2 来做 polyfill 替换。
  • polyfill 替换时,会给当前 commonjs 注入 import 关键字。
  • webpack 看到了 commonjs 中出现了 import 关键字样,很开心的把我的 commonjs 模块识别为了 esModule,触犯了上文分析的原则,页面立马挂了。
  • 于是看到了 Cannot assign to read only property ‘exports’ of object 这个报错

解决方案

  1. 可以把 babel 中的模块分析打开,把所有 esModule 强行转换为 commonjs 再塞给 webpack 就没问题了,但是 webpack 的 tree-shaking 会失效(因为只能对 esModule 使用这项技术)。
  2. 紧跟上一条,针对部分有需要的 commonjs 三方库,单独开启 babel 的模块分析。完美,但是成吨的 webpack 配置项等着我。
  3. 可以放弃 tranform-runtime-plugin , 使用 @babel/polyfill。 如此造成被动技术选型,感兴趣可以了解一下两者的区别。
  4. 项目全部使用 esModule 语法,性能最好,问题最少。可是太超前了吧!

一些思考:

有关 webpack 设置 module 的 exports 属性是只读的必要性

个人猜测,如果不这样设置,那么上述 common.output.js 的代码在执行就不会再有错误提示,但是这段代码却是错误的,因为代码实际的功能逻辑没有暴露给模块出口 __webpack_exports__ (而是被不明作用的 module 吞掉了),由此产生很难追查的程序 bug。
具体代码可以参考,
webpack 是如何设置 module.exports 的只读属性

有关 webpack 如何判断当前模块是 esModule 还是 commonjs
直接看下面摘自 webapck 的源代码:

1
2
3
4
5
6
// 是否是 esModule 模块
const isHarmony =
isStrictHarmony ||
ast.body.some(statement => {
return /^(Import|Export).*Declaration$/.test(statement.type);
});

是的,文件中出现 import 或 export 关键字,就会被认为是 esModule