接 Webpack 打包 commonjs 和 esmodule 模块的产物对比 继续,这篇文章来测试下 commonjs 模块和 esmodule 混用的情况,也就是 import 导入 commonjs 的模块,require 导入 esomodule 的模块,看一下它们在 Webpack 下的产物。
import 导入 commonjs 模块
commonjs 模块会为我们预设一个 module = {exports: {}} 的对象,导出模块的话我们可以直接给 module.exports.xxx = xxxx 或者 exports.xxx = xxx 加属性,也可以给 module.exports = xxx 赋值为一个新对象或者函数。
下边看下这两种情况的异同:
exports 添加属性
两个文件的代码:
| 1 | // src/commonjs/add.js | 
如果还记得  Webpack 打包 commonjs 和 esmodule 模块的产物对比  这里总结的,我们的 import 会导入整个对象,然后执行的时候再通过 xxx.add 的形式调用。
| 1 | var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( | 
而 _add__WEBPACK_IMPORTED_MODULE_0__ 就是我们导出的整个 module.exports 对象。
| 1 | "./src/commonjs/add.js": (module, exports) => { | 
因此这种情况两种模式是完全契合的,不会有问题。
全部代码如下:
| 1 | (() => { | 
exports 赋值为新对象
这次我们将 module.exports 整个赋值为一个新对象,这种情况我们一般是直接赋值为一个新的函数。
| 1 | // src/commonjs/add.js | 
index.js 我们可以直接导入函数,不需要这样子 import { xxx } from 'yyy'  再进行对象解构。
| 1 | // src/commonjs/index.js | 
我们知道,对于直接 import 导入的话, esmodule 相当于导入 default 属性,事实上 commonjs 并没有导出 default ,但 webpack 帮我们进行了兼容。
| 1 | var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( "./src/commonjs/add.js"); | 
看一下 __webpack_require__.n 方法
| 1 | __webpack_require__.n = (module) => { | 
如果不是 esmodule 模块的话,我们会将整个模块作为 default 属性返回,但为什么在模块内又加了个 a 属性,这里没太懂,谁知道的话可以和我交流一下哈。
再看下整个的代码:
| 1 | (() => { | 
require 导入 esmodule 模块
esmodule 模块除了正常的 export ,我们把 export default 也加一下:
| 1 | // src/esmodule/add.js | 
然后 index.js 通过 require 来导入:
| 1 | // src/esmodule/index.js | 
因为 require 是直接导入整个对象,没有专门导入 default 的形式,所以调用 default 方法的时候,我们需要通过 m.default 来调用。
运行起来是没有问题的:

让我们回忆下 Webpack 打包 commonjs 和 esmodule 模块的产物对比  这里介绍的 esmodule 模块的导出产物:
| 1 | var __webpack_modules__ = { | 
相当于导出了一个 module.exports 大的对象,包含 sub 和 default 属性。
然后是 index.js 
| 1 | (() => { | 
需要注意的是虽然导出的是整个对象,但对于 index.js 我们不可以通过对象解构来拿到 default 方法。
| 1 | // src/esmodule/index.js | 
default 会在这里被认为是一个关键字,直接抛错:

来看下整体代码:
| 1 | (() => { | 
同时导出 commonjs 和 esmodule
我们在一个文件同时使用 module.exports 和 export 。
| 1 | const add = (a, b) => { | 
浏览器会直接抛错:

如果直接重写 module.exports 呢?
| 1 | const add = (a, b) => { | 

同样会抛错,让我们看一下 webpack 的产物。
| 1 | "./src/commonjs/add.js": ( | 
调用了 __webpack_require__.hmd 方法拿到 module 。
| 1 | __webpack_require__.hmd = (module) => { | 
hmd 方法重新定义了 exports 属性,没有定义 get 属性,所以 module.exports 返回的是 undefined,module.exports.add 就直接抛错了。
重新定义了 set 函数,所以 module.exports = xxx ,重新赋值属性的时候走到 set 后直接抛错。
同时导入 commonjs 和 esmodule
定义一个 commonjs 模块:
| 1 | // src/commonjs/add.js | 
定义一个 esmodule 模块:
| 1 | // src/esmodule/sub.js | 
然后在 index.js 同时引入:
| 1 | const add = require("./add") ; | 
没什么问题,正常运行:

因为导入的话它们是互不影响的,各自导入自己的即可,可以看下完整代码:
| 1 | (() => { | 
总
不管是 esmodule 还是 commonjs 模块,最终都转换成了 module = {exports: {}} 形式的模块,所以它们之间的混用成为了可能。
import commonjs 模块的话,import 拿到的就是整个 module.exports 对象,正常使用即可。如果我们直接改写 module.exports 对象,webpack 会认为等同于 export default ,进行兼容处理。
require esmodule 模块的话,如果之前 esmodule 模块中有 export default ,那么使用的时候需要显示的调用 xxx.default ,对于其他的 export 正常使用即可。
虽然可以混用,但一般情况下能不混用就不混用,以免遇到未知问题,目前更推荐 esmodule 模块。
如果遇到奇怪问题的话,可以考虑直接去查看 webpack 的产物,能更快的排查出问题。
讨论
对产物其实有两个问题,然后去请教了下 Tecvan ,杰哥。
第一个问题就是上边提到,当使用 imports 导入 commonjs 模块的时候,会调用 n 方法。
| 1 | __webpack_require__.n = (module) => { | 
这里会挂一个 a 属性,原因的话如下:

主要是兼容 webpack 混用的情况,场景可能如下:

第二个问题,还是 import 导入 commonjs 模块的时候,打包产物如下:
| 1 | var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( | 
这里会触发 __webpack_require__.n 方法去生成 _add__WEBPACK_IMPORTED_MODULE_0___default 变量。触发这个逻辑的原因并不是因为我们使用了 import xxx from 'yyy' 的格式,而是因为导出 commonjs 模块的时候直接使用 module.exports = xxx 进行了覆盖,这种情况 webpack 就会认为等效于 export default 的情况。

但对于代码,因为 _add__WEBPACK_IMPORTED_MODULE_0__ 和 _add__WEBPACK_IMPORTED_MODULE_0___default 是同一个值,我们不处理 default 逻辑其实也是通的:

webpack 为什么会这样处理,具体原因就不知道了,欢迎大家一起来讨论,下边是杰哥的猜测:

后记
算上这篇,总结了三篇 webpack 的产物的文章  Webpack 打包 commonjs 和 esmodule 模块的产物对比 、Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比,可以加深平常开发中对于模块之间的理解。
大家如果还对 Webpack 原理感兴趣的话,可以去看杰哥的 Webpack 原理系列。目前我没有总结这个系列的计划了,哈哈。
