这篇文章不涉及 Webpack 的原理,只是观察下 Webpack 对 commonjs 和 esmodule 模块打包后的产物,读完后会对模块系统有个更深的了解。
环境配置
Webpack 只配置入口和出口,并且将 devtool 设置为 false,把 sourcemap 关掉。
1 | // webpack.config.js |
npm 安装三个 node 包。
1 | npm i -D webpack webpack-cli webpack-dev-server |
更详细的过程可以参考 2021年从零开发前端项目指南
小试牛刀
先简单写行代码测试一下:
1 | // src/commonjs/index.js |
打包产物:
1 | (() => { |
只是简单的包了层 IIFE 。
commonjs 模块
写一个 add 模块函数
1 | // src/commonjs/add.js |
然后 index.js 进行调用。
1 | // src/commonjs/index.js |

分析一下打包产物。
变成了 key、value 的键值对,key 是文件名,value 是封装为一个函数的模块,提供 module 和 exports参数。
这里我们只有一个模块,所以只有一个 key 。
1 | var __webpack_modules__ = { |
提供一个 __webpack_require__ 方法用来导入上边 __webpack_modules__ 中的模块。
1 | function __webpack_require__(moduleId) { |
因为 module 和 exports 都是对象,所以在 __webpack_modules__ 中给 exports 添加值就是改变这里外边的值。
最后把 module.exports 返回即可。
此外,我们可以添加一个 __webpack_module_cache__ 变量来保存已经导出过的对象。
1 | var __webpack_module_cache__ = {}; |
然后看下整体代码,index.js 中通过 __webpack_require__ 方法导入模块即可。
1 | (() => { |
esmodule 模块
我们把上边的 commonjs 模块改写一下。
1 | // src/esmodule/add.js |
然后是 index.js 。
1 | // src/esmodule/index.js |
此时运行一下会发现和 commonjs 不同的地方,代码并没有按照我们写的顺序执行,屏幕中先输出的是 add开始引入 然后才是 esmodule开始执行。

看一下打包产物应该就可以理解为什么了。
和之前一样,会提供一个 __webpack_require__ 方法来引入模块。
1 | var __webpack_module_cache__ = {}; |
不同之处在于,额外提供了几个看起来比较奇怪的方法。
第一个是 d 方法,用来将 definition 上边的属性挂到 exports 上。
1 | __webpack_require__.d = (exports, definition) => { |
第二个是 o 方法,判断 exports 方法是否有 key 属性。
1 | __webpack_require__.o = (obj, prop) => |
第三个是 r 方法,给 exports 加一个 Symbol.toStringTag 属性,这样 exports.toString 返回的就是 '[object Module] 。
此外,再加一个 __esModule 属性,用来标识该模块是 esmodule 。
1 | __webpack_require__.r = (exports) => { |
这几个方法啥时候用呢,会在我们的模块代码之前调用。
1 | var __webpack_modules__ = { |
我们把 add、PI、__WEBPACK_DEFAULT_EXPORT__ 属性都包了箭头函数 () => add ,因此可以先在 __webpack_require__.d 函数中使用它们,__webpack_require__.d 函数之后才去定义 add、PI、__WEBPACK_DEFAULT_EXPORT__ 这些变量的值。
然后是 index.js 的使用。
1 | var __webpack_exports__ = {}; |
可以看到我们是通过 _add__WEBPACK_IMPORTED_MODULE_0__ 变量把 ./src/esmodule/add.js 的所有方法都拿到,然后再使用 _add__WEBPACK_IMPORTED_MODULE_0__.add 调用具体的方法。
上边还有一个奇怪的用法 (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1) ,通过逗号表达式可以改变 this 指向,参考 Why does babel rewrite imported function call to (0, fn)(…)?,至于为什么这么用还不清楚,目前不重要先跳过了。
然后看下整体代码:
1 | (() => { |
commonjs 和 esmodule 的不同
两个的打包产物对比:
1 | // commonjs |
一个最大的区别就是 commonjs 导出的就是普通的值,一旦导入就不会改变了。而 esmodule 导出的值通过函数包装了一层,因此是动态的,导入之后再次使用可能会变化。
举个例子,对于 esmodule
1 | // src/esmodule/add.js |
如果只看 src/esmodule/index.js 的代码,我们并没有改变 PI 的值,但执行会发现 add 函数执行后 PI 的值就发生了改变:

对于原始值, commonjs 就做不到上边的事情了,一般情况下也不要这样搞,以防出现未知 bug 。
此外,esmodule 在挂载属性的时候只定义了 get 。
1 | __webpack_require__.d = (exports, definition) => { |
所以我们如果在 esmodule 模块中的去修改导入的值,会直接抛错。
1 | console.log("esmodule开始执行"); |

在 commonjs 中就无所谓了,但同样也不要这样搞,以防出现未知 bug 。
总
简单对比了下 commonjs 和 esmodule 模块的产物,其中 commonjs 比较简单,就是普通的导出对象和解构对象。但对于 esmodule 的话,导出的每一个属性会映射到一个函数,因此值是可以动态改变的。
此外 require 会按我们代码中的顺序执行,但 import 会被提升到代码最前边首先执行。
还会继续对比一下两者的动态导入、混合导入,本来想一篇文章总结完的,但有点长了,那就下篇继续吧,哈哈。