在使用了 mini-css-extract-plugin 的项目中,有时会遇到如下的 Warning 输出:
WARNING in chunk styles [mini-css-extract-plugin]
Conflicting order between:
* css xxx/css-loader/dist/cjs.js!./e1.css
* css xxx/css-loader/dist/cjs.js!./e2.css
* css xxx/css-loader/dist/cjs.js!./e3.css
tl;dr
上面这段 Warning 的实际含义是:
由于没法找到最合适的解,plugin 被迫使用了相对最优的解,将
e1.css
放到了编译产物中。在某些 chunk group 中,e2.css
或e3.css
这两个文件引用位置在e1.css
之前;而在 plugin 将e1.css
加入编译产物的时候,e2.css
和e3.css
还没有被添加。
换句话说,如果 e1.css
本来的用途是覆盖 e2.css
的内容,且覆盖依靠的是同优先级下 CSS 定义出现的先后顺序,那么在 plugin 编译完成之后,这个覆盖就会失效。
造成 Warning 的例子
以下通过一个直观的例子来说明 plugin 在何时可能会输出上述 Warning:
假设有两个文件 entry1.js
:
import './e1.css';
import './e2.css';
entry2.js
:
import './e2.css';
import './e1.css';
且 Webpack 配置了需要将 e1.css
和 e2.css
打包到同一个文件中。如果 plugin 选择将 e1.css
放在 e2.css
的前面,那么就不满足 entry2.js
的使用顺序;反之则不满足 entry1.js
的。正是在这样的“矛盾”情况下,plugin 输出了一个 Warning,并选择了一个相对最优的解。
更好的 Warning
上面提到的 mini-css-extract-plugin Warning 并不是非常直观,也很难知道具体是哪一个 chunk group 出现了问题。新提交的 PR #465 以及 #468 对此做了优化。优化后的输出类似:
WARNING in chunk styles [mini-css-extract-plugin]
Conflicting order. Following module has been added:
* css xxx!./e1.css
despite it was not able to fulfill desired ordering with these modules:
* css xxx!./e2.css
- couldn't fulfill desired order of chunk group(s) entry2
- while fulfilling desired order of chunk group(s) entry1
根据 Warning 的提示,只需要观察 entry2 中对应的代码,就可以找到不满足的引入顺序了。
是否需要关心
这个 Warning 是否需要关注,取决于 CSS 在项目中是如何被使用的:只要在项目的使用过程中,没有 CSS 是通过加载的顺序进行优先级覆盖的,那么就可以忽略 plugin 给出的 Warning;反之,如果有这样的情况,则一定需要根据 Warning 修正输出,防止编译导致的意外。
当然,最保险的做法是启用 CSS Module,从源头上保证各个模块间的 CSS 是不会相互覆盖的。
代码分析
以下从这个 Warning 入手,分析 plugin 是如何从 modules 生成最终的打包产物的。
观察 mini-css-extract-plugin 的源代码,不难找到这个 Warning 的具体输出代码。接下来,将针对这段代码所在的 renderContentAsset
进行分析,了解 plugin 生成打包产物的算法。
首先观察 renderContentAsset
函数最开始的比较语句:
if (typeof chunkGroup.getModuleIndex2 === 'function') {
// ...
} else {
// ...
}
这里,chunkGroup.getModuleIndex2
是 Webpack 4 中的 API,在之前的版本中不存在(Webpack 3 及之前使用的是 extract-text-webpack-plugin)。接下来,重点关心 Webpack 4 对应的代码段。
首先看 moduleDependencies
变量的初始化(源码):
const moduleDependencies = new Map(modules.map((m) => [m, new Set()]));
moduleDependencies
为每一个 module 都定义了一个对应的空 Set(具体 Set 内的值会在后续填充)。
接下来看 modulesByChunkGroup
变量的定义(源码):
const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => {
// ...
return sortedModules;
});
可以看到,modulesByChunkGroup
本质上,是将所有的 chunk group(chunk.groupsIterable
)转化成了对应的 sortedModules
。这里 sortedModules
变量的定义为(源码):
const sortedModules = modules
.map((m) => {
return {
module: m,
index: cg.getModuleIndex2(m),
};
})
// eslint-disable-next-line no-undefined
.filter((item) => item.index !== undefined)
.sort((a, b) => b.index - a.index)
.map((item) => item.module);
简单来说,做了几件事情:
- 将
modules
中不属于当前 chunk group(cg
)的部分剔除,对应的是.filter((item) => item.index !== undefined)
; - 根据 module 在 chunk group 中实际出现的位置,按从后往前进行排序,对应的是
.sort((a, b) => b.index - a.index)
这里,越是先出现的 module 在最终的 sortedModules
数组中排的越靠后。换句话说,出现在数组最后的一个 module,没有任何前置的依赖(在该 chunk group 中,这个 module 是第一个被引入的);而理论上来说,数组的第一个 module 依赖了数组后面的所有 modules(从 CSS 的角度来说,这个模块出现在最后。在所有选择器优先级一样的前提下,这个 module 理论上可以对之前所有的 module 进行覆盖)。这里采用倒叙的方式组织数组,是为了后续可以方便的使用 Array.prototype.pop
函数去获取当前没有依赖的 module。
接下来,代码对 sortedModules
变量值进行了填充:
for (let i = 0; i < sortedModules.length; i++) {
const set = moduleDependencies.get(sortedModules[i]);
for (let j = i + 1; j < sortedModules.length; j++) {
set.add(sortedModules[j]);
}
}
如上所述,sortedModules
中后出现的 module 是先出现 module 的依赖。这里的 moduleDependencies
变量记录了各个 module 的所有依赖(不仅仅是当前 chunk group 的依赖,所有 chunk group 的依赖最终都会被写入到这个 Set 中),其中 key 是各个 module,而对应的 value 则是一个 Set,Set 中的每个元素都是当前这个 module 的依赖。
之后定义了 usedModules
和 unusedModulesFilter
函数(源码),目的是为了判断某一个 module 是否已经被当前的 plugin 使用了。
接下来的代码需要确保所有的 modules 都会根据某个具体的算法在最终的编译产物中被使用到。这里判断的方法就是 usedModules
是否包含了 modules
中所有的内容。
while (usedModules.size < modules.length) {
let success = false;
let bestMatch;
let bestMatchDeps;
// ...
}
至此,准备工作都做完了。接下来就是核心的部分:如何在各个 chunk group 中选取合适的 module,依次放到最终生成的 CSS 文件中。代码如下:
for (const list of modulesByChunkGroup) {
// skip and remove already added modules
while (list.length > 0 && usedModules.has(list[list.length - 1])) {
list.pop();
}
// skip empty lists
if (list.length !== 0) {
const module = list[list.length - 1];
const deps = moduleDependencies.get(module);
// determine dependencies that are not yet included
const failedDeps = Array.from(deps).filter(unusedModulesFilter);
// store best match for fallback behavior
if (!bestMatchDeps || bestMatchDeps.length > failedDeps.length) {
bestMatch = list;
bestMatchDeps = failedDeps;
}
if (failedDeps.length === 0) {
// use this module and remove it from list
usedModules.add(list.pop());
success = true;
break;
}
}
}
理想情况下,两个 CSS module 在最终编译产物中的先后顺序,应该和这两个 CSS module 在某一个 chunk group 中的先后顺序是一致的。如果顺序上无法保证一致,那么应该尽可能将不一致的情况降到最低。mini-css-extract-plugin 就是遵循这一个原则来对 module 进行排序的。在每一次的选取步骤中,算法都会依次遍历每一个 chunk group,做下面几个事情:
- 拿到当前该 chunk group 中没有依赖的那个 CSS module(
const module = list[list.length - 1];
); - 判断这个拿到的 CSS module 是否有前置的依赖还没有被放到最终产物中去(
const failedDeps = Array.from(deps).filter(unusedModulesFilter)
); - 如果没有前置依赖了,那么这个 CSS module 就可以被“安全”的放到当前的编译产物中去(
if (failedDeps.length === 0) {
对应的部分); - 如果有不满足的前置依赖,那么就去寻找不满足情况最少的一个(
bestMatchDeps.length > failedDeps.length
),然后记下来(bestMatchDeps = failedDeps;
)
如果找到了没有前置依赖的 module,代码就直接 break,跳到下一次 while 循环中去了;如果没有找到完美匹配的情况,就会进入接下来的代码:
if (!success) {
// no module found => there is a conflict
// use list with fewest failed deps
// and emit a warning
const fallbackModule = bestMatch.pop();
if (!this.options.ignoreOrder) {
compilation.warnings.push(
new Error(
`chunk ${chunk.name || chunk.id} [${pluginName}]\n` +
'Conflicting order between:\n' +
` * ${fallbackModule.readableIdentifier(
requestShortener
)}\n` +
`${bestMatchDeps
.map((m) => ` * ${m.readableIdentifier(requestShortener)}`)
.join('\n')}`
)
);
}
usedModules.add(fallbackModule);
}
上面代码中的 bestMatch
就是对应了最佳情况时候 modules 排序的数组,这里 fallbackModule = bestMatch.pop()
就可以拿到当前这个最佳情况的 chunk group 中,没有依赖的那个 CSS module。和之前代码中 module = list[list.length - 1]
拿到的数据是一样的。
同时,bestMatchDeps
对应的就是上面代码里的 failedDeps
数组,表示的是当前这个 fallbackModule
被选中时,有哪些该 module 的前置依赖并没有被事先放到编译产物中去。
接下来就是向 Webpack 输出 Warning 的代码了。再来看下面这段 Warning:
WARNING in chunk styles [mini-css-extract-plugin]
Conflicting order between:
* css xxx/css-loader/dist/cjs.js!./e1.css
* css xxx/css-loader/dist/cjs.js!./e2.css
* css xxx/css-loader/dist/cjs.js!./e3.css
可知道,具体的含义是:由于没法找到最合适的解,plugin 被迫使用了相对最优的解,将 e1.css
放到了编译产物中。在某些 chunk group 中,e2.css
或 e3.css
这两个文件引用位置在 e1.css
之前;而在 plugin 将 e1.css
加入编译产物的时候,e2.css
和 e3.css
还没有被添加。