webpack 模块解析之 commonjs 和 esModule 混用问题
使用 webpack 进行构建,我们在混用 commonjs 和 esModule 情况下(随便引几个 node_modules 三方库,这两种规范就凑齐了),模块解析很容易出现问题,特别是再搭配各种 babel 、 polyfil 插件时,情况就更复杂了。这边拿来分析一下。
先说结论:import、export 关键字与 module.exports、exports 关键字同时出现在一个文件中时,webpack 会产生构建冲突
如以下代码是错误的
1 | import demo from 'demo' |
伴随产生的对比:
require 关键字永远都是安全的,可以任性的使用在 esModule 和 commonjs 文件中的任何位置
原因分析
先看 webpack 是怎么解析 commonjs 和 esModule 代码的。
举个栗子:
common.js
1 | const b = 2; |
common.output.js
1 | function(module, exports) { |
es.js
1 | const a = 2; |
es.output.js1
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 | __webpack_require__.r = function(exports) { |
重点来了,当我们在 commonjs 中使用 import 关键字时,会发生如下现象:
common.js:
1 | import a from './a'; |
common.output.js
1 | function(module, __webpack_exports__, __webpack_require__) { |
因为 webpack 在 esModule 中传入的 module 的 exports 属性是只读的,上述代码企图去给 exports 赋值,所以执行时会报错。
实际上我们期待的输出应该是这样子的
common.output.expected.js
1 | function(module, __webpack_exports__, __webpack_require__) { |
很遗憾,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 这个报错
解决方案
- 可以把 babel 中的模块分析打开,把所有 esModule 强行转换为 commonjs 再塞给 webpack 就没问题了,但是 webpack 的 tree-shaking 会失效(因为只能对 esModule 使用这项技术)。
- 紧跟上一条,针对部分有需要的 commonjs 三方库,单独开启 babel 的模块分析。完美,但是成吨的 webpack 配置项等着我。
- 可以放弃 tranform-runtime-plugin , 使用 @babel/polyfill。 如此造成被动技术选型,感兴趣可以了解一下两者的区别。
- 项目全部使用 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