Webpack CSS 顺序问题 #13
JSerFeng
started this conversation in
Deep Dive CN
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Webpack CSS 顺序问题
english version
前言
许多朋友反馈在 Webpack 中 CSS 顺序与自己的预期不一致,导致样式错误的问题,实际上在 Webpack 中,CSS 的顺序与 splitChunks 是相关的,并且是非常不稳定的,一个 js 模块先引入 a.css 再 b.css,配置 splitChunks 后,最终的 a 和 b 的顺序与其引入的顺序很有可能完全不一致。
一个 Webpack CSS 小例子
首先我们看一下代码。
可以看到该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 中
然后我们再次打包,打开页面,你会神奇地发现页面变成了 红色 !
更多情况,可以参考我们核心成员之一的 ahabhgk 创建的 仓库 来说明 CSS 在 Webpack 中的顺序是多么的不确定。上面的代码也来自于该仓库。
下面我来对这个仓库做一个说明:
这里有一个全排列表格
mini-css-extract-plugin
以及 Webpack 自带的experiments.css
(没错,webpack 其实并不需要任何 loader 或 plugin 就能打包css🤩style.js
文件是怎样被入口文件引入的,如果是import './shared/style.js'
那么是 static,如果是import('./shared/style.js')
那么是 dynamic从上述表格可以看出,页面最终是蓝色还是红色,根本无法简单的知道😅
预备知识: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 Splitting 和 splitChunks。 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 中,模块的加载和执行是两个阶段,同一个模块可以同时出现在多个 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
这俩原理其实是相似的,可以放在一起讲。
首先看这三条,第一条 static 代表入口模块静态引入
style.js
模块,all 表示 splitChunks.chunks 配置为 all,也就是拆包的配置对所有 Chunk 都会生效,= 代表两个 cacheGroup 的优先级是一样的,最终页面为红色。引入关系如下
配置 splitChunks 如下
最终产生的 ChunkGroup 以及 Chunk 如下
可以看到
a.css
以及b.css
被 splitChunks 从 main chunk 中拆了出去,他们在同一个组中,因此加载的时候会一起加载,问题来了,一起加载总会有先后顺序,同一个 Group 中的顺序如何决定呢?答案是:并没有可靠顺序保证。
SplitChunks 的原理是首先遍历所有的模块,找到该模块满足的所有 CacheGroups 规则
a.css
对应 cacheGroups['a']b.css
对应 cacheGroups['b']然后按照对应的 cacheGroups 来做分组,每一个组最终就会组成 Chunk。
在 Webpack 中,分组的策略还有更多的因素,包括该模块所属 Chunk,以及该模块所属 Chunk 组成的集合等等
分组后结果类似于:
然后会根据该分组拆分 Chunk,而首先需要选择一个组来拆,每一次选择组的策略如下
回到我们的例子中,由于我们并没有配置 priority,并且
a.css
和b.css
所属的 Chunk 数量也一样,由于他们都只有一个 Chunk,因此他们的总 size 和 chunk-1 的乘积都为 0,由于他们的 cacheGroups 都只满足一个,因此索引也都是0,模块数量也一样,那么最终就只能依靠模块的名字排序来决定了最终 b 所在的组先进行拆分。在拆分的时候,先拆分的 Chunk 会排在前面。
再拆分 a 组,拆分后
最终页面中加载的顺序,是按照该 ChunkGroup 下 Chunk 的顺序,也就是先 b.css 再 a.css,最终页面呈现红色。
再看第二条,其他条件不变的情况下,a 和 b 都设置了 priority,并且 a 的优先级大于 b,那么也就是 a 先拆分,b 再拆分,最终就是先 a 再 b,最终页面呈现蓝色。
再看第三条,b 的优先级大于 a,因此 b 先拆分,页面呈现 红色
接下来的三条是
由于 splitChunks.chunks 配置成了 async,只会对由 import() 创建出的 Chunk 进行拆分,而 a.css 和 b.css 都属于 main chunk 也就是入口 chunk,因此并没有被 splitChunks 拆分,最终 a b 在同一个 Chunk 中,顺序也是有保证的。
再看接下来几条,将引入变成了动态引入,那么都会经过拆包处理,因此结果和我们第一次分析的三条是一样的。
最后的
mini-css-extract-plugin
和experiments.css
是类似的,因此他们的结果是一样的。总结
问题的本质其实是,CSS 的加载同时会执行副作用,即样式生效,如果使用 splitChunks 将某些 CSS 放到特定的文件中,就必然会打乱原来的顺序,并且产物的顺序也很难自动推断得到用户想要的结果,这就导致了副作用(样式生效)的顺序被打乱。最根本的解决方案就是让 CSS 的加载和样式生效分开,使用 style-loader 是一种不完美的解决方案,他可以让 CSS 的样式挂载随着 JS 的执行而运行,但相对的造成了运行时的开销,以及 splitChunks、minimize 等优化不会对 CSS 生效,对 Server Side Rendering 也不友好。
Webpack 对 experiments.css 支持的 issue 中提到了 5 种引入 CSS 的方式,其中有两种能比较好的解决这一问题:
import stylesheet from "./style.css" asset { type: "css" };
即 Native CSS Module,目前是正在推进的标准,import 引入只会加载 CSS,而样式的生效需要开发者手动调用document.adoptedStyleSheets.push(stylesheet);
。new URL("./style.css", import.meta.url);
,已经是符合规范的写法,相较于 Native CSS Module 更为原始,只会 resolve 出样式的 URL,样式的加载则需要开发者手动发起网络请求。但这两种都需要开发者自己处理更多的事,写法上比现在常用的
import './style.css';
更为冗余,值得注意的是,现在常用的import './style.css';
其实并不是符合规范的写法。Beta Was this translation helpful? Give feedback.
All reactions