likes
comments
collection
share

Webpack 中薛定谔的 CSS 顺序

作者站长头像
站长
· 阅读数 28

前言

许多同学反馈在 Webpack 中 CSS 顺序与自己的预期不一致,导致样式错误的问题,实际上在 Webpack 中,CSS 的顺序与 splitChunks 是相关的,并且是非常不稳定的,一个 js 模块先引入 a.css 再 b.css,配置 splitChunks 后,最终的 a 和 b 的顺序与其引入的顺序很有可能完全不一致,轻易地击碎小小前端开发者的心。 声明:任何时候都请尽量避免依赖 style 的顺序和覆盖来决定最终样式

一点小小的 Webpack CSS 震撼

首先我们看一下代码。

import "./a.css"; // body { background: red }
import "./b.css"; // body { background: blue }

可以看到该js文件先引入了 a.css 来将浏览器背景变成 红色,然后又引入了 b.css 将浏览器背景变成 蓝色 到这里按照同学们的经验,最终页面应该是蓝色对吧,因为 b.css 应该覆盖 a.css 的样式。

没问题,确实是蓝色,接下来我想要配置 splitChunks,来让 a.css 和 b.css 在不同的 Chunk 中

我们使用 mini-css-extract-plugin 来处理 css,配置 splitChunks 来让 a.css 和 b.css 在不同 chunk 中

module.exports = {
  optimization: {
    splitChunks: {
      minSize: 0,
      chunks: 'all',
      cacheGroups: {
        a: {
          test: /a\.css/,
          name: "a",
        },
        b: {
          test: /b\.css/,
          name: "b",
        },
      },
    },
  }
 }

然后我们再次打包,打开页面,你会神奇地发现页面变成了 红色 !!!

更多情况,可以参考 ahabhgk 创建的 仓库 来说明 CSS 在 Webpack 中的顺序是多么的不确定。上面的代码也来自于该仓库。 下面我来对这个仓库做一个说明:

Webpack 中薛定谔的 CSS 顺序

这里有一个全排列表格

  • Import 代表使用什么方式处理css,有 style-loader, mini-css-extract-plugin 以及 webpack 自带的 experiments.css(没错,webpack其实并不需要任何loader或plugin就能打包css🤩
  • CSS 代表的是,上述的 style.js 文件是怎样被入口文件引入的,如果是 import './shared/style.js' 那么是 static,如果是 import('./shared/style.js') 那么是 dynamic
  • splitChunks.chunks 是筛选哪些 chunk 应该被拆包,all 代表任意 chunk 都可被拆包,async代表仅有被 import() 引入而产生的 chunk 可以被拆包
  • splitChunks.priority 代表的是 splitChunks 中 cacheGroups 的优先级,也就是当某个模块同时满足多个 cacheGroups 的时候,会按照优先级来决定最后是被哪一个 cacheGroup 处理。此处的 a 大于或小于 b,则是对应了 cacheGroups 中,a 和 b 优先级的大小关系
  • Color 则是页面最终的颜色,是蓝色还是红色 从上述表格可以看出,页面最终是蓝色还是红色,根本无法简单的知道😅

接下来我来揭秘背后的原因

预备知识:CSS 的处理方式

我会先介绍 webpack 中 css 常见的三种处理方式,如果已经了解请直接跳过。 在 Webpack 中常见的 CSS 处理方式有 3 类。

  • style-loader + css-loader
  • mini-css-extract-webpack-plugin + css-loader
  • experiments.css

我先来介绍一下这三种方式的区别。

css-loader

css-loader 中内置了 postcss,这个 loader 的作用很纯粹,为了解析 css 语法,以及解析 css-modules,这个loader并不负责 css 最终如何展示在页面中。该 loader 是许多 css 处理方式的基础。该 loader 接收 css 字符串,返回 js 字符串,该 js 字符串中导出了 css 内容,css-modules 的名称映射关系等等。

style-loader

style-loader 是用来将 css 插入到浏览器中,该 loader 会在运行时创建 style 标签,然后将 css 通过 style 标签插入到页面中。该 loader 是在运行时插入到页面的,也就是说 执行 到对应的 css 模块时,才会 插入到页面,css 的插入顺序,是完全和引入顺序一致的,因此在上面的颜色表格中,使用 style-loader 后,最终页面的颜色是完全稳定的,都是蓝色。

mini-css-extract-plugin

该插件和 style-loader 在运行时动态插入 style 标签不同,该插件会在打包时将 css 文件组合成 chunk,最终输出到产物中去。如果你配置了 splitChunks,那么 css 组成的 chunk 也可以经过你的个性化拆包处理。

experiments.css

这是 Webpack 自带的 css 处理,在开启该配置后,css 和 js 一样,在 Webpack中是一等公民,内置支持,同样也会经过你的个性化拆包处理。总体上和 mini-css-extract-plugin 是很像的,其实对于一些简单的 demo 项目,大家完全可以直接开启 experiments.css,不需要再安装 css-loader style-loader 等其他loader或插件

预备知识: SplitChunks

在开始前还需要简单介绍一下 splitChunks 的原理,如果理解 Chunk Group 概念可以跳过。 这里需要区分一下 Code SplittingsplitChunks。 code splitting 是 Webpack 自带的功能,通过 import('package') 语句来将某些模块放到新的 Chunk 中,配置 splitChunks 不会影响到 code splitting 策略。

SplitChunks 实际上是对 code splitting 产生的 Chunk 再进行拆分。

Chunk Group

Webpack 中有个概念叫 Chunk Group,我们所说的 SplitChunks 和拆包说的就是将某个 Chunk 拆成一堆更小的 Chunk 组成一组,也就是一个 ChunkGroup 进行加载。我们可以定制 maxRequest 来控制浏览器每一次最多同时请求多少个 Chunk,也就是每一个 ChunkGroup 最多含有多少个 Chunk。

Code Splitting 后,会产生许多的 Chunk,每一个 Chunk 也对应一个 ChunkGroup,splitChunks 也是我们平时所说的拆包,则是将 Chunk 拆成更多的 Chunk,形成一个组,每次加载也按组加载,例如 http2 可以将某个 Chunk 拆成 20 个 Chunk 为一组,同时进行加载。

Webpack 中薛定谔的 CSS 顺序

在 Webpack 中,模块的加载执行是两个阶段,同一个模块可以同时出现在多个 Chunk 中,只会执行一次。可以随意将 Chunk 中的某些模块拆出去组成新的 Chunk。

而 Rollup,Esbuild 等轻运行时的打包工具,虽然有 code splitting,但他们的产物是加载即执行,因此拆包的灵活度相比 Webpack 会有更多的限制。

CSS 顺序不稳定原理

在介绍完之后,我再来对 CSS 顺序不稳定的表格一个个进行解释。

使用 style-loader

首先注意到表格中,使用 style-loader 的场景,最终页面的颜色都是稳定的蓝色,这是由于,style-loader 实际上将 CSS 变成了 JS 模块,运行的时候动态插入 Style 标签,因此使用 style-loader 最终 CSS 的顺序和引入 CSS 模块的顺序是稳定一致的。

使用experiments.css 和 mini-css-extract-plugin

这俩原理其实是相似的,可以放在一起讲。

Webpack 中薛定谔的 CSS 顺序

首先看这三条,第一条 static 代表入口模块静态引入 style.js 模块,all 表示 splitChunks.chunks 配置为 all,也就是拆包的配置对所有 Chunk 都会生效,· 代表两个 cacheGroup 的优先级是一样的,最终页面为红色。

引入关系如下

Webpack 中薛定谔的 CSS 顺序

配置 splitChunks 如下

Webpack 中薛定谔的 CSS 顺序

最终产生的 ChunkGroup 以及 Chunk 如下

Webpack 中薛定谔的 CSS 顺序

可以看到 a.css 以及 b.css 被 splitChunks 从 main chunk 中拆了出去,他们在同一个组中,因此加载的时候会一起加载,问题来了,一起加载总会有先后顺序,同一个 Group 中的顺序如何决定呢?

答案是:并没有可靠顺序保证。

SplitChunks 的原理是首先遍历所有的模块,找到该模块满足的所有 CacheGroups 规则

a.css 对应 cacheGroups['a'] b.css 对应 cacheGroups['b']

然后按照对应的 cacheGroups 来做分组,每一个组最终就会组成 Chunk。

在 Webpack 中,分组的策略还有更多的因素,包括该模块所属 Chunk,以及该模块所属 Chunk 组成的集合等等,如果想要看更多 SplitChunks 细节,请转评赞支持我😉

分组后结果类似于:

const ChunkInfoMap = {
    a: { modules: ['a.css'], chunks: ['main'], cacheGroup: {...} },
    b: { modules: ['b.css'], chunks: ['main'], cacheGroup: {...} },
}

然后会根据该分组拆分 Chunk,而首先需要选择一个组来拆,每一次选择组的策略如下

  1. 如果 cacheGroup 中有 priority,那么选择当前 priority 最高的一组开始拆分 如果 priority 没有配置的话,那么就全靠 Webpack 根据某些指标来自动选择了,这些指标包括:
  2. 根据组中的 Chunk 数量,选择当前 Map 中 Chunk 数量最多的一个组开始拆分
  3. 根据 Size 和 chunk 数量 -1 的乘积,选择 Map 中模块组成的 Size 最大的一个组开始拆分
  4. 某个模块满足多个 CacheGroups 时,会有一个索引,选择这个索引最小的拆分。。。
  5. 根据模块的数量,选择模块数量最多的一个组开始拆分
  6. 根据模块的名字排序决定。。。

因此大家最好靠 priority 来控制拆包的优先级,不要让 Webpack 来猜测,猜测的结果就非常不稳定。

回到我们的例子中,由于我们并没有配置 priority,并且 a.css 和 b.css 所属的 Chunk 数量也一样,由于他们都只有一个 Chunk,因此他们的总 size 和 chunk-1 的乘积都为 0,由于他们的cacheGroups都只满足一个,因此索引也都是0,模块数量也一样,那么最终就只能依靠模块的名字排序来决定了

Webpack 中薛定谔的 CSS 顺序

最终 b 所在的组先进行拆分。在拆分的时候,先拆分的 Chunk 会排在前面。

Webpack 中薛定谔的 CSS 顺序

再拆分 a 组,拆分后

Webpack 中薛定谔的 CSS 顺序

最终页面中加载的顺序,是按照该 ChunkGroup 下 Chunk 的顺序,也就是先 b.css 再 a.css,最终页面呈现红色。

再看第二条,其他条件不变的情况下,a 和 b 都设置了 priority,并且 a 的优先级大于 b,那么也就是 a 先拆分,b 再拆分,最终就是先 a 再 b,最终页面呈现蓝色。

再看第三条,b 的优先级大于 a,因此 b 先拆分,页面呈现 红色

接下来的三条是

Webpack 中薛定谔的 CSS 顺序

由于 splitChunks.chunks 配置成了 async,只会对由 import() 创建出的 Chunk 进行拆分,而 a.css 和 b.css 都属于 main chunk 也就是入口 chunk,因此并没有被 splitChunks 拆分,最终 a b 在同一个 Chunk 中,顺序也是有保证的。

Webpack 中薛定谔的 CSS 顺序

再看接下来几条,将引入变成了动态引入,那么都会经过拆包处理,因此结果和我们第一次分析的三条是一样的。

Webpack 中薛定谔的 CSS 顺序

最后的 mini-css-extract-plugin 和 experiments 是类似的,因此他们的结果是一样的。

总结

任何覆盖样式的行为,例如用自己的样式覆盖组件库样式,请将组件库样式的引入放在入口处,确保最早引入。

对于 JS 模块来说,当前的 splitChunks 算法完全是 OK 的,因为 JS 的加载和执行都由 Webpack 的运行时决定,而 CSS 加载即执行

解决方案也有,如果现有项目已经不好动,使用 style-loader,或者在拆包配置中,选择不拆包 css:

Webpack 中薛定谔的 CSS 顺序

该配置表示,只有 javascript 模块会被拆包

结语

时不时会写一些构建相关有意思的小东西,欢迎关注我的 github/jserfeng 和我的公众号 👏