上篇文章介绍了 webpack 打包文件的分析,其中用到的例子是 commonjs规范的模块作为依赖,分析了webpack对 commonjs 规范模块的处理。
转念一想,如果依赖是ES Module规范呢?这就带来了两个问题:
- webpack 针对 ES Module 模块是如何处理的?
- index.js 是否还能用 require 来进行加载ES Module模块?反过来说,是否还能用 import 来加载 commonjs 规范的模块
当然了,这里第二个问题的答案是显而易见的,webpack 下是可以使用混用各JavaScript模块化规范的,至于原理得要从第一个问题的剖析开始讲起。
关于 webpack 的模块
- An ES2015 import statement
- A CommonJS require() statement
- An AMD define and require statement
- An @import statement inside of a css/sass/less file.
- An image url in a stylesheet (url(…)) or html (
) file.
webpack文档显示,webpack 支持 ES Module、Commonjs、AMD 模块化规范,以及 css里的@import、img url/src。这里就不针对 css img 做深入研究了,毕竟JavaScript模块化是大头。
ES Module 规范
项目结构
1 | // index.js |
webpack配置
1 | // webpack.config.js |
打包结果
1 | /******/ (function(modules) { // webpackBootstrap |
分析
看过本系列的第一篇文章的知道,对比 commonjs规范的打包代码,ES Modules下的打包代码只有模块数组部分的代码产生了变化,而IIFE 函数本身是没有变化的,所以重点说模块数组部分。
先说入口模块,去除注释:
1 | Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); |
首先给__webpack_exports__
添加属性__esModule
为 true,说明这是个 ES Module 模块。这个属性得跟__webpack_require__.n
结合起来,针对 ES Modules的 export default
以及 commonjs 的export
的处理,这在后面 commonjs与 ES Modules 混淆使用会谈到。
接着调用__webpack_require__
函数,moduleId 为1,加载es.js
模块,缓存模块 module里的 exports 赋值为:
1 | __webpack_require__.d(__webpack_exports__, "a", function() { return counter; });; |
接着返回 modules.exports
,赋值到__WEBPACK_IMPORTED_MODULE_0__es_js__
,从而加载了 es模块,接着1
2
3console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 3
Object(__WEBPACK_IMPORTED_MODULE_0__es__["b" /* incCounter */])();
console.log(__WEBPACK_IMPORTED_MODULE_0__es__["a" /* counter */]); // 4
打印 counter 变量,执行 incCounter 函数,再一次打印 counter 变量。
这里有一个重点,是commonjs 和 ES Module的重要差异之一,CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
所以,在counter
的赋值,这里是采取了__webpack_require__.d
1
2
3
4
5
6
7
8
9
10
11
12__webpack_require__.d(__webpack_exports__, "a", function() { return counter; });;
let counter = 3;
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
这里输出的counter属性实际上是一个取值器函数,保证 counter 变量取的是值的引用。
结论
- ES Module的
export
export default
都转换成了类似 commonjs 规范的modules.exports
,在这里是modules.exports["a"] modules.exports["b"]
- ES Module导出的依赖,赋值到
__WEBPACK_IMPORTED_MODULE_0__es_js__
,缓存到缓存模块数组- 保证ES Module 输出的是值的引用,采用的是取值器函数
ES Module与 Commonjs规范混用
这里还是举出两个例子,尝试解答本文开头提出的第二个问题
情景一:ES Module 加载 Commonjs 模块
1 | // index.js |
webpack配置
1 | // webpack.config.js |
打包结果(截取模块数组部分)
1 | [ |
前面分析 ES Module 说过,入口模块 index为 ES 模块,添加__esModule
属性为true
, 接着调用__webpack_require__
导入 cj模块,cj 模块为 commonjs 规范,webpack 对其处理没多大变化,完成对__WEBPACK_IMPORTED_MODULE_0__cj__
赋值,接着比 ES Module 调用 ES Module 多出的一步是:1
var __WEBPACK_IMPORTED_MODULE_0__cj___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__cj__);
其中__webpack_require__.n
定义为:1
2
3
4
5
6
7
8
9
10
11__webpack_require__.n = function (module) {
var getter = module && module.__esModule ?
function getDefault() {
return module['default'];
} :
function getModuleExports() {
return module;
};
__webpack_require__.d(getter, 'a', getter);
return getter;
};
这里判断依赖模块是否为 ES Module,当__esModule为true时,那么返回的是该模块的default属性的值,如果传入的模块原来就是commonjs模块,返回模块本身,并且令该模块的a属性等于模块本身。
这里调用 cj1模块也是一样的,不同的地方在于,es模块调用cj模块取的是 default 属性,调用 cj1模块取的是 foo1属性,所以在执行这一步又是不同的:1
2__WEBPACK_IMPORTED_MODULE_0__cj___default()()
Object(__WEBPACK_IMPORTED_MODULE_1__cj1__["foo1"])();
可以看出 cj1模块是原样输出,执行foo1
属性
结论
- ES Module 引用commonjs模块时,因为import foo from ‘./cj’想取的是模块的default属性,而commonjs模块没有暴露default的方法,所以webpack将整个模块作为default属性的值输出
- 如果只是引入commonjs模块 exports的某个属性,则 commonjs 模块是原样输出
情景二:Commonjs 加载 ES Module 模块
1 | // index.js |
webpack配置
1 | // webpack.config.js |
打包结果(截取模块数组部分)
1 | [ |
入口模块,调用两次__webpack_require__
分别赋值给变量es
、es1
,es、es1模块中分别声明__esModule
为 true,但是 es 模块中,给__webpack_exports__
添加default
属性,赋值 es 函数,es1模块则是给__webpack_exports__
添加es1
属性,赋值 es1函数。
所以在入口模块中,调用函数方式如下:1
2es.default();
es1.es1();
结论
- ES Modules依赖,
export default
转为__webpack_exports__["default"]
,所以 commonjs 模块调用其默认值时,需调用default
属性
这部分内容虽然看起来比较困难,但是专研代码的同时参考本文,还是很容易搞懂的~ 下一篇文章,会探讨Code Splitting的实现,