4
人生当中总是有你能力所不及的范围,但是如果在你能力所及的范畴内,你尽到了自己全部的努力,那你还有什么可以遗憾呢?

问题描述

在某次项目开发中,我发现了一个有趣的问题:当我在本地环境中访问页面时,标签元素的布局看起来很正常;但是当我将项目部署到开发环境后,标签元素的布局有些偏上,不再是之前的正常布局。这个问题引起了我的注意,并且我开始进行排查。

image.png

定位分析

为了找出标签布局不一致的原因,我比较了两个环境下作用于标签的样式,以确定它们之间是否有差异:

本地环境开发环境
image.pngimage.pngimage.pngimage.png

对比可以发现,作用于标签的类样式.bre-label-inner_container.brand-item_discountTag__SlxDf在两个环境下加载顺序不一样,而它们都存在一个样式属性vertical-align,会互相覆盖,所以才导致两个环境下标签布局不一致。

为什么类样式的加载顺序会有所不同呢?

两个环境下代码都是一样的,唯一的区别是构建过程中对样式处理方式不同。

本地环境处理样式

本地环境构建采用Development模式,采用style-loader处理样式,当 webpack 处理 CSS 样式文件时,style-loader 会将 CSS 样式文件转换为 JavaScript 模块,并将这些模块嵌入到生成的 JavaScript bundle 文件中。在浏览器加载 JavaScript bundle 文件时,style-loader 会在 HTML 页面中动态创建 <style> 标签,并将 CSS 样式插入到这些标签中,从而使样式生效。

image.png

image.png

实际项目中组件嵌套层级结构和样式引用如以下示例:

// Main组件
import Label from "@/components/Label";
import "@casstime/bre-label/styles/index.scss"; // 含有.bre-label-inner_container样式
const Main = () => {
    return <Label />
}

// Labe组件
import styles from "./index.module.scss"; // 含有.brand-item_discountTag__SlxDf样式
const Label = () => {
    return <div className=`bre-label-inner_container ${styles.discountTag}`>...</div>
}

当上面代码构建后,会编译成一个个代码块结构,由webpack内置函数__webpack_require__深度调用执行,调用栈过程如下:

image.png

不难发现./index.module.scss样式文件先于@casstime/bre-label/styles/index.scss样式文件通过style标签注入到页面中,因此,本地环境会看到.bre-label-inner_container { vertical-align: middle; }覆盖.brand-item_discountTag__SlxDf { vertical-align: top; }

<html>
    <head>
        <style>.brand-item_discountTag__SlxDf { vertical-align: top; }</style>
        <style>.bre-label-inner_container { vertical-align: middle; }</style>
    </head>
</html>

开发环境处理样式

开发环境构建采用Production模式,采用mini-css-extract-plugin处理样式,MiniCssExtractPlugin.loader 用于将 CSS 样式从 webpack 打包生成的 JavaScript 文件中提取到单独的文件中,这些文件的名称和路径由 MiniCssExtractPlugin 插件的 filenamechunkFilename 配置选项决定。为了确保生成的 CSS 文件能够正确地应用到 HTML 页面中,结合插件html-webpack-plugin 自动生成 HTML 文件,并自动以 link 标签引入生成的 CSS 文件。

MiniCssExtractPlugin 插件本身并没有拆分 CSS 的功能,它只负责将 CSS 样式提取到单独的文件中,并将这些文件与 webpack 打包生成的 JavaScript 文件一起输出到指定的目录中。

如果您希望在使用 MiniCssExtractPlugin 插件时拆分生成的 CSS 文件,可以结合使用 optimization.splitChunks 配置选项来实现。具体来说,您可以将 optimization.splitChunks 配置选项设置为一个对象,并在其中指定要拆分的 chunk 类型、最小体积和最小使用次数等参数。这样,webpack 将会自动将符合条件的 chunk 拆分成多个文件,并将其中的 CSS 样式提取到单独的文件中。

需要注意的是,为了正确地使用 MiniCssExtractPlugin.loader,您必须先在 webpack 配置文件中引入 MiniCssExtractPlugin 插件,并将其添加到插件列表中。只有在插件被实例化并添加到插件列表中后,MiniCssExtractPlugin.loader 才能正确地将 CSS 样式提取到文件中。

image.png

MiniCssExtractPlugin 插件提取样式的顺序是按照 webpack 打包时模块的依赖关系顺序提取的。具体来说,如果一个样式文件 A 依赖了另一个样式文件 B,那么在打包时,会先提取样式文件 B,然后再提取样式文件 A。这是因为在 webpack 的打包过程中,每个模块的依赖关系会被分析出来,并按照依赖关系顺序生成依赖图,这个依赖图中包括了样式文件之间的依赖关系。因此,在提取样式文件时,会按照依赖图的顺序依次提取,以确保样式文件的依赖关系被正确地处理。

需要注意的是,在提取样式文件时,还会考虑样式文件的引用顺序。例如,如果在 HTML 页面中先引用了样式文件 A,再引用样式文件 B,那么在提取样式文件时,会按照这个引用顺序先提取样式文件 A,再提取样式文件 B。这样可以确保在页面加载时,样式文件的加载顺序与 HTML 页面中的引用顺序一致,避免了样式被错误地覆盖或重写的问题。

假设有三个样式文件:a.scssb.scssc.scss,其中 a.scss 依赖于 b.scssa.scssc.scss 前面引入:

// a.scss

@import 'b.scss';

/* ... */

// c.scss

/* ... */

webpack 的配置文件中,使用 MiniCssExtractPlugin 插件提取样式文件:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ]
};

在提取样式文件时,b.scss 会先被提取,然后是 a.scss,最后是 c.scss。在生成的 CSS 文件中,样式规则的顺序则取决于它们在样式文件中的出现顺序。

假设 b.scss 定义了以下样式规则:

/* b.scss */

.foo {
  color: red;
}

a.scss 依赖于 b.scss,并定义了以下样式规则:

// a.scss

@import 'b.scss';

.bar {
  font-size: 16px;
}

c.scss 定义了以下样式规则:

/* c.scss */

.baz {
  text-align: center;
}

在生成的 CSS 文件中,样式规则的顺序会按照它们在样式文件中的出现顺序排列,即先是 b.scss 中定义的样式规则,然后是 a.scss 中定义的样式规则,最后是 c.scss 中定义的样式规则。因此,生成的 CSS 文件中样式规则的顺序如下:

/* styles.css */

/* b.scss */
.foo {
  color: red;
}

/* a.scss */
.bar {
  font-size: 16px;
}

/* c.scss */
.baz {
  text-align: center;
}

我们实际项目引用顺序是这样的:

image.png

最终提取到 CSS 文件中的样式规则顺序应当是这样子:

/* @casstime/bre-label/styles/index.scss */

.bre-label-inner_container { 
    vertical-align: middle; // 被覆盖
}

/* ./index.module.scss */

.brand-item_discountTag__SlxDf {
    vertical-align: top; 
}

.brand-item_discountTag__SlxDf { vertical-align: top; }覆盖.bre-label-inner_container { vertical-align: middle; },因此开发环境标签布局有些偏上。

解决方案

我们需要制定相应的规则来优化样式文件引用顺序,以防止类似问题再次发生。这些规则应该不仅仅解决当前问题,而且应该考虑到长远的解决方案。

为了优化样式文件引用顺序,我们建议将基础组件和业务组件等第三方组件的样式文件放置于项目入口文件头部进行引入。这些样式规则的优先级应该是最低的,以便业务代码中的样式规则可以根据实际情况进行修改和覆盖。这样可以确保样式规则的继承和覆盖关系得到正确的处理,同时也有助于提高项目的可维护性和可扩展性。

// Main组件
import "@casstime/bre-label/styles/index.scss"; // 含有.bre-label-inner_container样式
import Label from "@/components/Label"; // 第三方业务组件

const Main = () => {
    return <Label />
}

// Labe组件
import styles from "./index.module.scss"; // 含有.brand-item_discountTag__SlxDf样式

const Label = () => {
    return <div className=`bre-label-inner_container ${styles.discountTag}`>...</div>
}

// 无论在本地环境还是开发环境,`.brand-item_discountTag__SlxDf { vertical-align: top; // 为了布局正确,可以改动此样式属性 }`都会覆盖`.bre-label-inner_container { vertical-align: middle; }`,

参考

style-loader
MiniCssExtractPlugin
optimization.splitChunks


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。