普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月20日首页

CSS Modules完全指南:CSS模块化的特性,生态工具和实践

作者 漂流瓶jz
2026年3月19日 00:39

简介

在之前的文章中,我们了解了很多CSS命名规范:BEM、OOCSS、SMACSS、ITCSS、AMCSS、SUITCSS:CSS命名规范简介。它们可以解决CSS样式全局生效容易引发污染和冲突的问题。但这些方案基本都是写一个前缀或后缀,通过手写命名的方式避免类名重复。但这在多人协作或引入大量外部库时,依然不能完全避免问题,还需依赖团队规范管理。那么,是否有工具可以自动做这件事,而且完全避免组件内的类名与其它组件重复?有的,这就是CSS Modules。

CSS Modules中文叫做CSS模块。默认情况下,我们定义的CSS类名标识符是全局的。使用CSS Modules之后,每个类名将变为唯一的全局名称,包含不会重复的哈希值。引入CSS文件时,我们可以拿到CSS文件导出的类名到全局名称的对应关系,从而在HTML中提供相应的类名。

对于希望共享的类名,CSS Modules也提供了方案使其全局生效。同时CSS Modules还提供了定制标识符,class组合等功能。要想实现CSS Module的功能,代码需要经过打包,而且由于不同前端框架代码的组织方式不一样,CSS Module的具体使用也有区别,我们逐一介绍一下。

纯JavaScript使用方式

首先我们抛开各种前端框架,在纯粹的JavaScript代码中演示CSS Modules的效果。这里选用Vite,首先命令行执行代码,创建工程:

npm init -y
npm add -D vite

然后在package.json的scripts中增加几个构建相关命令。dev开发模式,build生产模式构建,preview生产模式预览。

{
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
}

然后创建index.html,为浏览器入口文件,里面引入index.js。

<html>
  <script src="./index.js" type="module"></script>
  <body>
    <div>jsplp CSS Modules</div>
  </body>
</html>

然后创建两个CSS文件,分别是使用CSS Modules的index.module.css和没有使用的index.css:

/* index.css */
.class1 {
  color: red;
}

/* index.module.css */
.class2 {
  background-color: yellow;
}
.abcDef {
  background-color: yellow;
  .qazwsx {
    background-color: yellow;
  }
}
#id1 {
  background-color: yellow;
}

然后是index.js文件,引入这两个CSS文件,并在DOM中增加几个div元素,使用这些CSS类:

import './index.css';
import styles from './index.module.css';

console.log(styles)

const test1 = document.createElement('div');
test1.textContent = 'test1';
document.body.appendChild(test1);

const test2 = document.createElement('div');
test2.className = 'class1';
test2.textContent = 'test2';
document.body.appendChild(test2);

const test3 = document.createElement('div');
test3.className = styles.class2;
test3.textContent = 'test3';
document.body.appendChild(test3);

const test4 = document.createElement('div');
test4.className = 'class1' + " " + styles.class2;
test4.textContent = 'test4';
document.body.appendChild(test4);

/* 输出结果
{
  abcDef: "_abcDef_1wjui_7",
  class2: "_class2_1wjui_1",
  id1: "_id1_1wjui_1",
  qazwsx: "_qazwsx_1wjui_11",
}
*/

我们在index.module.css中列举了几个场景,分别是class名,嵌套class名,id名。将其引入为一个对象并输出结果,发现它是一个key为原来的标识符名称,value为包含哈希值的新标识符的对象。要使用类名时,需要将这个新标识符提供给DOM进行渲染。而对比普通CSS文件,只需要简单引入即可生效。在index.js中我们还创建了四个场景,分别是:

  • test1 没有类名
  • test2 普通CSS类名 class1
  • test3 CSS Modules类名 class2
  • test4 普通 class1 + 模块的 class2

因为新标识符实际上也是个字符串,因此可以和普通类名结合使用,中间加个空格即可。当然也可以使用classnames等辅助工具组合类名。执行 npm run dev,看一下效果:

css-modules-1.png

通过浏览器可以看到,CSS Modules类名不仅成功作为CSS类名,而且还能和对应的CSS文件里面的规则对应上。test4这种结合class属性也可以生效。我们再执行npm run build,看一下打包后的文件内容:

css-modules-2.png

查看dist目录中打包后的文件内容,可以看到CSS文件和JS文件被分开单独引入到HTML文件中。两个CSS文件被合并为一个,普通CSS文件还是维持原来的类名,index.module.css则变为了带哈希值的新标识符名。对应的JS文件中引入的styles变为了常量对象,内容也是新标识符的映射关系。

通过上面的代码演示,我们能够了解CSS Modules的核心思路,即改变CSS标识符的名称,使其不会重复;需要使用对应标识符的地方要用JavaScript手动引入;同时更改CSS文件中的标识符以匹配新的名称。

CSS Modules特性

CSS Modules除了上面的核心特性之外,还包含一些特性。这里我们介绍和尝试一下它的主要特性:

多文件引用CSS模块

前面我们在同一个JavaScript文件中引入了CSS Modules的CSS文件,多次使用引入的标识符,发现值实际是一样的。那么如果在不同的文件中引入CSS Modules的CSS文件,新标识符会一样么?这里来试一下。首先创建两个CSS文件:

/* index1.module.css */
.class1 {
  color: red;
}

/* index2.module.css */
.class1 {
  color: yellow;
}

可以看到两个CSS文件中类名标识符是一致的,都是class1。然后是两个JavaScript文件index1.js和index2.js,里面总共举了三个例子:

// index1.js
import styles1 from "./index1.module.css";
import styles2 from "./index2.module.css";

const test1 = document.createElement("div");
test1.className = styles1.class1;
test1.textContent = "test1";
document.body.appendChild(test1);

const test2 = document.createElement("div");
test2.className = styles2.class1;
test2.textContent = "test2";
document.body.appendChild(test2);

//index2.js
import styles1 from "./index1.module.css";

const test3 = document.createElement("div");
test3.className = styles1.class1;
test3.textContent = "test3";
document.body.appendChild(test3);

最后是index.html,引入两个JavaScript文件:

<html>
  <script src="./index1.js" type="module"></script>
  <script src="./index2.js" type="module"></script>
  <body>
    <div>jsplp CSS Modules</div>
  </body>
</html>

经过Vite打包后,在浏览器看下输出结果:

css-modules-3.png

  • test1和test3对比,分别在两个JavaScript文件中引入了同一个CSS模块文件index1.module.css,最后生成的类标识符是一致的,样式效果也一致。这是因为CSS文件只有一个,最后只会生成一份CSS规则。而且既然引入同一文件,规则肯定是一样的,没有必要分开两个类名。
  • test1和test2对比,在同一个JavaScript文件中引入了两个CSS模块文件,虽然各自CSS文件中类名是一样的,但因为所属文件不同,因此生成的新类名不一样,这样有效避免了同名的样式冲突问题。
  • test2和test3对比,分别在两个JavaScript文件中引入了两个CSS模块文件,生成的新类名也不一样,也避免了同名的样式冲突问题。

global全局规则

通过前面的例子可以看到,使用CSS Modules之后,所有自定义标识符名都变成了新的,只有引用才能生效的局部CSS规则。如果希望在这个CSS文件内定义部分全局都能生效的规则,CSS Modules也给出了方法,而且允许全局规则和局部规则混合嵌套使用。使用:global,就可以在CSS模块文件中使用全局规则。我们来看下例子。首先是index.module.css文件:

/* index.module.css */
.class1 {
    color: red;
}
:global(.class1) {
    color: blue;
}
:global(.class2) {
    .class3 {
        color: yellow;
    }
}
:local(.class4) {
    color: grey;
}

然后是index.js,这里给出了四种情况。可以看到带:global的标识符是不会被JavaScript文件导入的:

import styles from "./index.module.css";

console.log(styles);

function genEle(test, className) {
  const div = document.createElement("div");
  div.className = className;
  div.textContent = test;
  document.body.appendChild(div);
}
genEle("test1", styles.class1);
genEle("test2", "class1");

const div = document.createElement("div");
div.className = "class2";
div.innerHTML = `<div class='${styles.class3}'>test3</div>`;
document.body.appendChild(div);

genEle("test4", styles.class4);

/* 输出结果
{
  class1: '_class1_8tmyt_1',
  class3: '_class3_8tmyt_8',
  class4: '_class4_8tmyt_12'
}
*/

css-modules-4.png

  • test1: 正常的模块化CSS规则,做对比用
  • test2: 与test1一样都用class1做类名,但这里没有使用导出的新类名,因此匹配到了带:global的全局CSS规则
  • test3: 外层class2是全局类名,里面的class3没有用:global,因此还是局部规则。这是一个混合使用的例子,在CSS模块文件中,只有包裹在:global里面的类名才是全局规则,嵌套选择器和组合选择器需要单独包裹, 或者这样包裹在一起也可以::global(.cls1 + .cls2)
  • test4: :local表示模块化的CSS规则,与不增加标识效果一致。一般为了强调才使用。

composes组合规则

使用CSS Modules,使用composes属性,在规则中可以组合另一个类选择器的规则。这里举个例子看一下是如何组合的。首先是index.module.css文件:

.class1 {
  background: yellow;
}
.class2 {
  color: red;
  composes: class1;
}
.class1:hover {
  border: 1px solid blue;
}

然后是index.js中引入CSS文件,这里仅使用class2做类名:

import styles from "./index.module.css";

function genEle(test, className) {
  const div = document.createElement("div");
  div.className = className;
  div.textContent = test;
  document.body.appendChild(div);
}
genEle("test1", styles.class2);

css-modules-5.png

我们的test1元素只定义了class2这个类名,但在浏览器中,却同时有了class1的类名。这时因为在CSS文件中定义class2的规则时,增加了composes属性,值为class1的类名。这相当于让class2继承class1,因此元素也具有了class1的类名和样式。同时还举了一个伪类的例子,这个组合规则对于伪类/为元素和选择器组合等都可以生效。composes属性也支持全局规则和跨文件引用,这里也举下例子:

/* index.module.css */
:global(.class1) {
  background: yellow;
}
.class2 {
  color: red;
  composes: class1 from global;
  composes: class3 from './index2.module.css';
}

/* index2.module.css */
.class3 {
  border: 1px solid blue;
}

这里仅更改了index.module.css文件,新增了index2.module.css文件,index.js文件内容没有变化。然后我们查看浏览器效果:

css-modules-6.png

通过这个例子我们发现,CSS Modules可以组合全局规则,composes的类名后面加from global即可。同时composes可以在同一个类中使用很多次,都会生效。另外composes也可以跨文件组合,直接from文件名即可。

实现主题功能

使用CSS Modules主动引入类名的特性,通过不同场景下的类名切换,可以实现主题功能。首先定义两个CSS文件,其中的选择器一致,但是主题不一样:

/* red.module.css */
.class1 {
  color: red;
}
.class2 {
  border: 1px solid red;
}

/* blue.module.css */
.class1 {
  color: blue;
}
.class2 {
  border: 1px solid blue;
}

然后是index.js文件:

import styleRed from "./red.module.css";
import styleBlue from "./blue.module.css";

function genEle(test, className) {
  const div = document.createElement("div");
  div.className = className;
  div.textContent = test;
  document.body.appendChild(div);
}

function componentJz(styles) {
  genEle("test1", styles.class1);
  genEle("test2", styles.class2);
}

// 渲染红色主题
componentJz(styleRed);
// 渲染蓝色主题
componentJz(styleBlue);

可以看到,将引入的CSS Modules标识符对象传递给组件,组件中的元素使用这个对象作为类名。这样可以实现根据不同的条件传入不同的CSS文件对象,页面主题样式也随之变化。这里其实使用React组件举例更合适,但React使用方式要留到下面介绍,因此先使用纯JS示意。

React使用方式

React中使用CSS Modules与纯JavaScript使用基本一致。这里我们使用Vite创建一个React工程,展示在React中使用CSS Modules。首先执行命令行:

# 提示中选择React
npm create vite
# 进入工程
cd vite-react
# 安装依赖
npm install
# 增加依赖
npm add -S classnames 
# 开发模式运行工程
npm run dev

创建App.module.css文件,内容如下:

.class1 {
  color: red;
}
.class2 {
  color: blue;
}
:global(.class3) {
  border: 1px solid yellow;
}

然后将App.jsx中的内容删掉,替换为下面的代码。这就是React中的使用方式,CSS文件引入的标识符对象作为className属性。同时这里演示了classnames的用法,可以方便的组合多种类名。

import styles from './App.module.css';
import cn from 'classnames';

export default function App() {
  return (
    <div>
      <div className={styles.class1}>test1</div>
      <div className='class3'>test2</div>
      <div className={cn(styles.class2, 'class3')}>test3</div>
    </div>
  )
}

css-modules-7.png

Vue使用方式

Vue框架对于组件的组织方式比较特别,使用一个“单文件组件”的方式来组织代码,将所属同一个组件的HTML模板,JavaScript代码和CSS样式同时写到一个组件中。而且单文件组件中最流行的写法是“组件作用域CSS”,不是CSS Modules。下面我们分别介绍一下。

组件作用域CSS

组件作用域CSS,可以做到本组件的CSS样式就只影响本组件,不会影响别的组件;即使非类名选择器,例如标签选择器,属性选择器等,都仅限在本组件范围内生效。注意组件作用域CSS并不是CSS Modules,只不过功能上有部分相似之处。我们看一下例子,首先使用命令行创建Vue工程:

# 根据提示创建Vue工程
npm create vue@latest
# 进入工程
cd vite-vue
# 安装依赖
npm install
# 开发模式运行工程
npm run dev

然后我们删除App.vue中的内容,填充下面的代码。作为父组件。

<script setup>
import Comp1 from './comp1.vue'
import Comp2 from './comp2.vue'
</script>

<template>
  <div>
    父组件
    <div class="class1"> 父组件元素 </div>
    <p> 父组件p元素 </p>
  </div>
  <Comp1 />
  <Comp2 />
</template>

<style scoped>
p {
  color: yellow;
}
.class1 {
  background-color: aqua;
}
</style>

然后是样式选择器与父组件一致的子组件comp1.vue:

<template>
  <div>
    子组件1
    <div class="class1"> 子组件1元素 </div>
    <p> 子组件1p元素 </p>
  </div>
</template>

<style scoped>
p {
  color: red;
}
.class1 {
  background-color: blue;
}
</style>

最后是没有开启组件作用域CSS的comp2.vue组件:

<template>
  <div>
    子组件2
    <div class="class1"> 子组件2元素 </div>
    <p> 子组件2p元素 </p>
  </div>
</template>

<style>
div {
  border: 1px solid brown;
}
.class1 {
  color: brown;
}
</style>

在代码中可以看出,单文件组件将<template> <script> <style>在同一个vue文件中封装。如果使用作用域CSS,就在style标签上加scoped属性。有什么效果呢?我们看下浏览器截图:

css-modules-8.png

可以看到,在设置了scoped属性之后,组件生成的HTML代码中便会多了dat-v-xxxx的属性,每个组件的属性是单独的不会重复。对应的CSS选择器中也添加了属性选择器的条件。这样不管是类选择器还是标签选择器等,都只有匹配到了对应的data-v属性才会生效。

对于组件内CSS样式污染全局的问题,组件作用域CSS比CSS Modules的隔离更全面,基本可以做到完全不污染全局。例如App.vue组件和comp1.vue组件,两个选择器一致,但是样式却没有被污染。不过要注意,在父组件中引入子组件,子组件的根元素会同时被附加上父组件和子组件的data-v属性,例如comp1.vue组件的根结点。

comp2.vue组件没有使用组件作用域CSS,因此它的CSS能影响全局。包括使用scoped属性的组件内部,如果符合规则也能匹配上。这与CSS Modules不一致,因为CSS Modules修改了类名,因此源码中的符合规则的元素类名,生成代码中就不符合规则了。

特殊选择器

与CSS Modules一样,组件作用域CSS也有一些特殊的选择器用于处理一些特殊场景,主要有这几个:

  • :deep() 深度选择器 样式可以影响子组件
  • :slotted() 插槽选择器 样式可以影响插槽内容
  • :global() 全局选择器 样式可以影响全局

下面我们举个例子演示一下选择器的使用方法。首先是父组件App.vue文件:

<script setup>
import Comp1 from './comp1.vue'
</script>

<template>
  <div>
    父组件
    <div class="class1"> 父组件元素 </div>
    <Comp1>
      <div class="class2"> slot元素 </div>
    </Comp1>
  </div>
  <Comp1 />
</template>

<style scoped>
:global(.class1) {
  color: red;
}
:deep(.class1) {
  background-color: aqua;
}
</style>

然后是子组件comp1.vue:

<template>
  <div>
    子组件1
    <div class="class1"> 子组件1元素 </div>
    <slot> </slot>
  </div>
</template>

<style scoped>
:slotted(.class2) {
  background-color: blue;
}
</style>

css-modules-9.png

上面例子中展示了三种选择器的使用方式,其中全局选择器的效果和CSS Modules基本一致;深度选择器只能影响自己和子组件;插槽选择器影响的父组件中被包括在子组件插槽中的部分。通过对于组件作用域CSS的介绍,可以发现它虽然实现原理与CSS Modules不一致,但作用却有些相似,而且扩展了CSS Modules的作用范围。

Vue与CSS Modules

Vue中不仅有组件作用域CSS,单文件组件也可以直接集成CSS Modules开发。在style标签上加module属性,即可开启CSS Modules。我们继续举个例子演示用法,首先是父组件App.vue:

<script setup>
import Comp1 from './comp1.vue'
</script>

<template>
  <div>
    父组件
    <div :class="$style.class1"> 父组件元素 </div>
    <Comp1>
      <div :class="$style.class2"> slot元素 </div>
    </Comp1>
  </div>
</template>

<style module>
.class1 {
  color: red;
}
.class2 {
  background: yellow;
  composes: class1;
}
</style>

然后是子组件comp1.vue:

<script setup>
import { useCssModule } from 'vue'

const styles = useCssModule();
console.log(styles);
</script>

<template>
  <div>
    子组件1
    <div class="class1"> 子组件1元素 </div>
    <slot> </slot>
    <div :class="styles.class3"> 子组件1元素2 </div>
  </div>
</template>

<style module>
.class3 {
  border: 1px solid blue;
}
</style>

<!-- 输出结果
{ class3: "_class3_1wpng_3" }
-->

css-modules-10.png

在父组件的template中,使用$style就可以拿到CSS Modules引入的标识符映射对象。如果希望在JavaScript文件中使用,则可以参考子组件的方式,使用useCssModule拿到标识符映射对象,进行处理后再提供给template。

Webpack使用方式

前面介绍了两种前端框架中CSS Modules的使用方式。因为它最终还需要构建工具进行处理才能生效,因此我们再关注一下它在不同构建工具中的使用方式。首先看下在Webpack中的使用。

接入CSS Modules

先创建一个Webapck工程,执行如下命令行:

npm init -y
npm install webpack webpack-cli style-loader css-loader html-webpack-plugin --save-dev

然后修改package.json中的scripts,增加"build": "webpack",后面执行npm run build即可构建结果。然后创建src/index.js,内容如下:

import * as styles from "./index.module.css";

console.log(styles);

function genEle(test, className) {
  const div = document.createElement("div");
  div.className = className;
  div.textContent = test;
  document.body.appendChild(div);
}

genEle("test1", styles.class1);
genEle("test2", styles.class2);

然后创建index.module.css,包含内容如下:

.class1 {
    color: red;
}
.class2 {
    background: yellow;
    composes: class1;
}

然后创建webpack.config.js配置文件,内容如下:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "管理输出",
    }),
  ],
  output: {
    path: path.resolve(__dirname, "dist"),
  },
};

/* 输出结果
{
  class1:  "qbnq8h84_UYLiRusvV1S",
  class2:  "cofz5uKLVjU6TCKJqeBc qbnq8h84_UYLiRusvV1S",
  default: undefined,
  __esModule: true
  ...其它内容
}
*/

然后执行构建命令,生成dist目录。在浏览器中打开dist/index.html,可以看到CSS Modules已经接入成功,类名变成了带哈希的标识符,test2元素因为使用了composes特性,还包含了两个类名,在console输出时也带着。

css-modules-11.png

提供CSS Modules功能的,实际上是css-loader。默认情况下,当CSS文件的中间包含.module或者.icss时,css-loader会将其识别为CSS模块处理。可以看到JavaScript代码中引入CSS标识符时使用的是import * as,这也是因为css-loader并不支持默认导出所有标识符到一个对象中,而是只能单个导入,类似于import { class1 } from "./index.module.css";

css-loader可以配置全部CSS文件开启CSS模块,配置modules选项为true即可。然后将index.module.css改为index.css也能使用CSS Modules功能。这里展示webpack.config.js中的配置改动:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: true,
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "管理输出",
    }),
  ],
  output: {
    path: path.resolve(__dirname, "dist"),
  },
};

value变量

在css-loader文档中描述了开启CSS模块后,可以使用value变量的功能。虽然CSS Modules自己的主文档中没写,但是Webpack和Vite实际上都支持。value变量有点像CSS变量,一次定义,多处使用。这里我们举个例子,直接在前面的基础上修改CSS文件:

@value varRed red;
@value borderBlue 1px solid blue;

.class1 {
  color: varRed;
}
.class2 {
  background: varRed;
  border: borderBlue;
}

css-modules-12.png

可以看到,使用方式和CSS变量类似,预先定义变量,随后引用标识符。但CSS变量是运行时,CSS Modules的变量是编译时。由于没有CSS变量好用,而且LESS和SCSS等都提供了更完善的编译时变量功能,因此CSS Modules的变量功能好像用的人不多。

模式

css-loader的modules配置表示CSS Modules相关配置,其中有一个mode配置,表示模式,有四种选项值,这里列举一下:

  • local 将所有标识符编译为局部规则标识符
  • global 将所有标识符编译为全局标识符,相当于所有标识符使用:global
  • pure 与local模式一致,但是检测每一个CSS选择器是否存在局部标识符,如果不存在报错
  • icss 仅处理composes特性,但是不转换局部标识符

local模式既是默认模式,与上面正常使用CSS Modules一致。global标识符会处理value变量,但不会转换成局部标识符。icss模式处理composes特性,但是也不转换局部标识符。pure模式则在普通local模式的基础上,增加了对于每个选择器的判断,这么判断的原因为:检测CSS模块文件中是否存在可以全局生效的CSS选择器。这里设置pure模式试一下,我们修改webpack.config.js中的css-loader配置:

{
  loader: "css-loader",
  options: {
    modules: {
      mode: 'pure'
    },
  },
}

然后创建修改index.module.scss文件:

.class1 {
  color: red;
}
.class2 {
  background: blue;
}
div {
  color: yellow;
}

执行npm run build,结果失败,命令行输出结果如下:

css-modules-13.png

可以看到选择器div被标出来了,说它不“pure”,应该至少包含一个局部class或者id。这是因为div属性选择器,无法对属性选择器进行局部标识符的处理,因此这个选择器会全局生效,使用pure模式后,css-loader会寻找CSS模块文件中的全局选择器并报错,防止意外影响全局。

自定义标识符格式

在前面用Vite尝试CSS Modules时生成的新标识符格式,与Webpack生成的新标识符格式,看起来有点区别:Vite生成的带原来的类名,但是Webpack不带。这种标识符格式,实际上是可以更改的,使用localIdentName配置项即可:

{
  loader: "css-loader",
  options: {
    modules: {
      localIdentName: '[hash:base64]'
    },
  },
}

默认配置是[hash:base64],即原标识符的哈希值。还可以扩展其他格式:

  • [name] 源文件名称
  • [path] 目录名称
  • [file] 目录和文件名
  • [ext] 文件拓展名
  • [hash] 原标识符的哈希值
  • [local] 原来的标识符名
  • [hash:base64] 将hash做Base64处理
  • [hash:5] hash的长度限制为5

这些配置可以组合成模板字符串,例如[name]_[ext]_[hash]。这里举几个配置和对应的生成标识符的例子:

配置项 举例1 举例2
[name]_[ext]_[hash:7] index-module_-css_a9b9eaf index-module_-css_f3e6e28
[path]_[local]_[hash:base64:5] src-_class1_qbnq8 src-_class2_cofz5

使用postcss-modules

Webpack使用css-loader来支持CSS Modules,Vite背后则采用postcss-modules来支持CSS Modules。如果开启了lightningcss,则使用Lightning CSS来支持CSS Modules。postcss-modules是一个PostCSS插件,这里我们介绍一下。

引入postcss-modules

首先我们创建一个工程,引入PostCSS和postcss-modules,使其可以成功编译CSS Modules。首先执行命令行:

npm init -y
npm add -D postcss postcss-cli postcss-modules
# 后面执行下面命令行,可以编译CSS
# src 源文件目录 output 生成文件目录
npx postcss src -d output --no-map

创建PostCSS的配置文件postcss.config.js,里面引入postcss-modules插件。

const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [postcssModules],
};

创建src/index.module.css,里面包含如下内容:

.class1 {
  color: red;
}
.class2 {
  composes: class1;
  background: blue;
}

css-modules-14.png

执行编译后,生成结果如上图。首先看左边的目录树,生成了output/index.module.css,内容是标识符改变后的CSS规则。但是在src目录中却还生成了一个index.module.css.json文件,内容为原标识符和新标识符的映射关系。因为PostCSS没有编译JavaScript文件的能力,因此这个映射关系作为额外的JSON文件导出了。

获取标识符映射数据

前面引入postcss-modules中,我们发现生成的标识符映射关系文件被放到了src源文件目录中。这有点让人不适,源代码目录不应该被生成的内容污染。postcss-modules提供了我们自行控制标识符映射数据的方式,需要修改配置文件postcss.config.js:

const path = require("path");
const fs = require("fs");
const postcssModules = require("postcss-modules");

module.exports = {
  plugins: [
    postcssModules({
      getJSON: (cssFileName, json, outputFileName) => {
        // 源CSS文件路径
        console.log(cssFileName);
        // 标识符映射数据
        console.log(json);
        // 默认要输出的文件路径 可以弃用
        console.log(outputFileName);
        console.log("---");

        // 创建目录 如果已经创建则静默成功
        fs.mkdirSync(path.resolve("./classMap"), { recursive: true });
        // 获取源文件名
        const cssName = path.basename(cssFileName);
        // 拼合新的路径
        const jsonFileName = path.resolve("./classMap/" + cssName + ".json");
        // 文件写入新路径
        fs.writeFileSync(jsonFileName, JSON.stringify(json));
      },
    }),
  ],
};

可以看到,对getJSON配置项传入函数,可以拿到文件路径和标识符数据,可以对它进行任意处理。上面给出了一个写入其它目录的例子。这里创建两个CSS文件index.module.css和index2.module.css,执行编译输出结果如下:

css-modules-15.png

自定义标识符格式

在介绍Webpack使用方式时,我们提到css-loader支持自定义标识符格式,同样的postcss-modules也支持,而且格式也一样,通过修改generateScopedName配置:

const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [
    postcssModules({
      generateScopedName: '[name]_[ext]_[hash:base64:5]'
    }),
  ],
};

因此,我们可以参考上面css-loader的文档来修改generateScopedName配置。generateScopedName还支持自定义函数,可以随心所欲的配置:

const path = require("path");
const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [
    postcssModules({
      generateScopedName: (name, filename, css) => {
        // name 文件路径 filename 文件路径
        console.log(name, filename);
        // CSS文件内容
        console.log(css);
        console.log('-----');
        return `${path.basename(filename).replaceAll('.', '-')}_${name}`;
      }
    }),
  ],
};

在上面的例子中,我们将class名改为文件名+class名(仅供示例,实际使用还是会重复)。我们看下效果,成功的将class类名改为了我们自定义的格式。

css-modules-16.png

原始标识符转换

CSS Modules推荐使用camelCase驼峰命名法来写CSS标识符,因为可以轻松在JavaScript中使用,例如styles.abcDef。但如果用kebab-case中划线命名法,在JavaScript中使用就稍微麻烦一些,但也不是不能用,例如:styles['abc-def']。但如果已有的CSS规则,postcss-modules提供了localsConvention配置项,可以让我们将原始标识符转换为驼峰命名法的形式。这样即使我们在CSS文件中使用中划线命名法,在JavaScript代码中也能用驼峰命名法引入。它共有四个选项可以配置。

  • camelCase 输出为骆驼命名法,输出保留原标识符
  • camelCaseOnly 输出为骆驼命名法,不保留原标识符
  • dashes 仅转换中划线为骆驼命名法,输出保留原标识符
  • dashesOnly 仅转换中划线为骆驼命名法,不保留原标识符

这里我们构造一个CSS文件,带有几种命名法:

.abcDef {
  color: red;
}
.bcd-efg {
  color: red;
}
.cde_fgh {
  color: red;
}

对于不同配置项,我们看一下输出结果:

// camelCase
{
  "abcDef": "_abcDef_cmy82_1",
  "bcd-efg": "_bcd-efg_cmy82_7",
  "bcdEfg": "_bcd-efg_cmy82_7",
  "cde_fgh": "_cde_fgh_cmy82_13",
  "cdeFgh": "_cde_fgh_cmy82_13"
}

// camelCaseOnly
{
  "abcDef": "_abcDef_cmy82_1",
  "bcdEfg": "_bcd-efg_cmy82_7",
  "cdeFgh": "_cde_fgh_cmy82_13"
}

// dashes
{
  "abcDef": "_abcDef_cmy82_1",
  "bcd-efg": "_bcd-efg_cmy82_7",
  "bcdEfg": "_bcd-efg_cmy82_7",
  "cde_fgh": "_cde_fgh_cmy82_13"
}

// dashesOnly
{
  "abcDef": "_abcDef_cmy82_1",
  "bcdEfg": "_bcd-efg_cmy82_7",
  "cde_fgh": "_cde_fgh_cmy82_13"
}

localsConvention配置还可以接受函数入参,这样我们可以自定义JavaScrript引入的标识符名,这里我们举了个例子,将标识符后面加了固定的后缀。

const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [
    postcssModules({
      localsConvention: (originalClassName, generatedClassName, filPath) => {
        // 原标识符 转换后标识符
        console.log(originalClassName, generatedClassName);
        // CSS文件路径
        console.log(filPath);
        console.log("-----");
        return originalClassName + "_jzplp";
      },
    }),
  ],
};

/* 输出结果
{
  "abcDef_jzplp": "_abcDef_cmy82_1",
  "bcd-efg_jzplp": "_bcd-efg_cmy82_7",
  "cde_fgh_jzplp": "_cde_fgh_cmy82_13"
}
*/

处理路径别名

在CSS Modules的composes特性中,可以引入其它CSS文件中选择器的规则。很多人喜欢使用路径别名,例如@common/css等,postcss-modules也提供了resolve这个配置项,可以让我们自行控制别名,甚至修改路径。首先我们构造一个文件路径的例子:

/* src/common/com.css */
.classCommon {
  color: red;
}

/* src/copm1/index.module.css */
.class1 {
  background: yellow;
  composes: classCommon from '@common/com.css';
}

可以看到,我们先在common目录中设置了CSS文件,然后在copm1目录中,使用@common将其引入。如果不处理,@common这个路径CSS Module肯定是识别不了的。这里我们设置postcss.config.js:

const path = require("path");
const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [
    postcssModules({
      resolve: (file, importer) => {
        // composes特性引入的文件路径
        console.log(file);
        // 当前处理的CSS文件
        console.log(importer);
        const newPath = path.resolve(
          process.cwd() + file.replace("@common", "/src/common"),
        );
        return newPath;
      },
    }),
  ],
};

/*
命令行输出结果
@common/com.css
E:\testProj\css-modules\postcss-proj\src\comp1\index.module.css

index.module.css.json输出结果
{"class1":"_class1_u2c4w_1 _classCommon_pc5si_1"}
*/

可以看到,提供自定义的resolve函数,可以接收composes中的路径与当前处理的文件路径,然后将我们的路径别名转化为真正的路径名。

解析PostHTML模板

postcss-modules导出的JSON文件,可以被posthtml-css-modules使用,用来提供给PostHTML模板提供新的标识符。首先我们将postcss-modules中的所有选项删除(为了不干扰这个例子)。然后创建一个CSS文件src/index.module.css:

.class1 {
  background: yellow;
}
.class2 {
  color: red;
  composes: class1;
}

然后创建index.js,处理PostHTML模板:

const posthtml = require('posthtml');
const posthtmlCssModules = require("posthtml-css-modules");

const template = `
<div css-module="class1">test1</div>
<div css-module="class2">test2</div>
`
posthtml([posthtmlCssModules("./src/index.module.css.json")])
.process(template)
.then(function (result) {
console.log(result.html);
});

我们还没安装新依赖呢。执行如下命令行,安装依赖和拿到编译结果:

# 安装依赖
npm add posthtml posthtml-css-modules
# 编译CSS 处理CSS Modules
postcss src -d output --no-map
# 执行上面代码,处理PostHTML模板
node index.js

最后输出结果如下。可以看到关键在于css-module属性,我们将其设置为原始的标识符,经过处理后就变为了转换后的标识符。

<div class="_class1_go5lk_1">test1</div>
<div class="_class2_go5lk_7 _class1_go5lk_1">test2</div>

posthtml-css-modules还支持传入目录,可以处理多个文件。同时在css-module中使用点符号分隔文件和属性。但由于postcss-modules默认生成的文件中肯定会出现点,例如 index1.css -> index1.css.json,因此我们先要处理postcss.config.js:

const path = require("path");
const fs = require("fs");

const postcssModules = require("postcss-modules");
module.exports = {
  plugins: [
    postcssModules({
      getJSON: (cssFileName, json) => {
        // 创建目录 如果已经创建则静默成功
        fs.mkdirSync(path.resolve("./classMap"), { recursive: true });
        // 获取源文件名
        const cssName = path.basename(cssFileName, ".css");
        // 拼合新的路径
        const jsonFileName = path.resolve("./classMap/" + cssName + ".json");
        // 文件写入新路径
        fs.writeFileSync(jsonFileName, JSON.stringify(json));
      },
    }),
  ],
};

例如我们有src目录,里面有两个JSON文件insex1.json和insex2.json,此时处理模板的代码修改为:

const posthtml = require('posthtml');
const posthtmlCssModules = require("posthtml-css-modules");

// 文件名.标识符
const template = `
<div css-module="index1.class1">test1</div>
<div css-module="index2.class2">test2</div>
`
posthtml([posthtmlCssModules("./classMap/")])
.process(template)
.then(function (result) {
console.log(result.html);
});

/* 输出结果
<div class="_class1_1c28r_1">test1</div>
<div class="_class2_10f6k_1">test2</div>
*/

Lightning CSS

Lightning CSS是一个用Rust编写的CSS编译工具,可以解析,编译,打包和压缩CSS代码,性能比用JavaScript 的同类工具要强很多。Lightning CSS也支持CSS modules,这里我们简单讲一下用法。

接入Lightning CSS

首先执行命令行新建工程,安装lightningcss依赖。然后创建index.mjs,内容如下:

import { transform } from 'lightningcss';

const cssData = `
.class1 { color: red }
.class2 { color: blue }
`;
let { code, exports } = transform({
  cssModules: true,
  code: Buffer.from(cssData),
});

console.log(code.toString());
console.log('-----');
console.log(exports);

/* 输出结果
._8Z4fiW_class1 {
  color: red;
}

._8Z4fiW_class2 {
  color: #00f;
}

-----
{
  class1: { name: '_8Z4fiW_class1', composes: [], isReferenced: false },
  class2: { name: '_8Z4fiW_class2', composes: [], isReferenced: false }
}
*/

从上面代码可以看到,Lightning CSS接收和返回的都是Buffer对象;设置cssModules为true可以处理CSS modules。其中code是返回代码,exports是返回的映射关系。Lightning CSS返回的映射关系格式与其它工具不同,它一个标识符为一个对象,其中的name为转换后的标识符。

composes特性的映射关系

与其它工具不一样,Lightning CSS处理composes特性并不将其直接作为name,而是单独放到composes数组中。这里我们举个例子试一下:

.class1 { color: red }
.class2 {
  color: blue;
  composes: class1;
  composes: jzplp from global;
  composes: abc from './style.css';
}

/* 输出结果
._8Z4fiW_class1 {
  color: red;
}

._8Z4fiW_class2 {
  color: #00f;
}

-----
{
  class1: { name: '_8Z4fiW_class1', composes: [], isReferenced: false },
  class2: {
    name: '_8Z4fiW_class2',
    composes: [
      { type: 'local', name: '_8Z4fiW_class1' },
      { type: 'global', name: 'jzplp' },
      { type: 'dependency', name: 'abc', specifier: './style.css' }
    ],
    isReferenced: false
  }
}
*/

这里举了三种composes例子,有不同的类型:

  • local 本文件中的标识符
  • global 全局标识符
  • dependency 其它文件的标识符

注意dependency类型,这里没有实际读取另一个CSS文件(因为我们使用transform来编译,不能读取其它文件),而且这个name值也并没有变成转换后的值。Lightning CSS的文档中要求调用方自行处理。

使用bundle方法

前面我们提到,在使用composes特性引入另一个文件的标识符,最后映射关系中只给我们返回了文件路径,没有帮我们实际引入。这时候我们不用transform方法,转为使用bundle,Lightning CSS就可以帮我们读取文件了。我们构造一个例子。首先创建两个CSS文件:

/* index.css */
.class2 {
  background-color: red;
  composes: abc from './style.css';
}

/* style.css */
.abc {
  color: blue;
}
.bcd {
  font-size: 14px;
}

然后修改index.mjs:

import { bundle } from "lightningcss";

let { code, exports } = bundle({
  cssModules: true,
  filename: "./index.css",
});

console.log(code.toString());
console.log("-----");
console.dir(exports, { depth: null });

/* 输出结果
.Zvw1Mq_abc {
  color: #00f;
}

.Zvw1Mq_bcd {
  font-size: 14px;
}

.vkZoAa_class2 {
  background-color: red;
}

-----
{
  class2: {
    name: 'vkZoAa_class2',
    composes: [ { type: 'local', name: 'Zvw1Mq_abc' } ],
    isReferenced: false
  }
}
*/

可以看到,虽然我们只输入了一个index.css文件,但两个CSS文件实际上都被编译了。但映射关系还是只输出了index.css。之前的dependency类型消失了,因为拿到了转换后的类名,所以类型也被转为了local。

模块化CSS变量

Lightning CSS并不提供value变量功能,理由是CSS本身已经支持变量了。但Lightning CSS支持了将CSS变量标识符转换为hash标识符的功能,即局部CSS变量。这里我们试一下:

import { transform } from "lightningcss";

const cssData = `
.root {
  --abc: red;
}
.class1 {
  color: var(--abc);
  background: var(--bcd from global);
  font-size: var(--def from './style.css');
}
`;
let { code, exports } = transform({
  cssModules: {
    dashedIdents: true,
  },
  code: Buffer.from(cssData),
});

console.log(code.toString());
console.log("-----");
console.log(exports);

/* 输出结果
._8Z4fiW_root {
  --_8Z4fiW_abc: red;
}

._8Z4fiW_class1 {
  color: var(--_8Z4fiW_abc);
  background: var(--bcd);
  font-size: var(--tCZyqW);
}

-----
{
  class1: { name: '_8Z4fiW_class1', composes: [], isReferenced: false },
  '--abc': { name: '--_8Z4fiW_abc', composes: [], isReferenced: true },
  root: { name: '_8Z4fiW_root', composes: [], isReferenced: false }
}
*/

开启dashedIdents选项才能够局部化CSS变量。 这里列举了三个情形,与composes非常类似:

  • 本文件的CSS变量 使用方法与CSS写法一致,工具会自己转换。exports中也导出了对应标识符。
  • 全局CSS变量 使用from global可以设置为全局变量
  • 从其它文件引入局部CSS变量

注意看从其它文件引入的情形,exports中并没有导出这个标识符,但是CSS文件却被转换了。这也是因为transform方法没有访问文件的能力,但这种“自行转换”的形式有些不妥,如果没有和另一个文件匹配,这个CSS变量引入就无法生效。这里我们换成bundle方法试一下。还是创建两个CSS文件:

/* index.css */
.root {
  --abc: red;
}
.class1 {
  color: var(--abc);
  background: var(--bcd from global);
  font-size: var(--def from './style.css');
}

/* style.css */
.root {
  --def: 14px;
}

然后修改index.mjs。通过执行结果可以看到,bundle方法将两个文件一起编译,保证结果的正确性。

import { bundle } from "lightningcss";

let { code, exports } = bundle({
  cssModules: {
    dashedIdents: true,
  },
  filename: "./index.css",
});

console.log(code.toString());
console.log("-----");
console.dir(exports, { depth: null });

/* 
.Zvw1Mq_root {
  --Zvw1Mq_def: 14px;
}

.vkZoAa_root {
  --vkZoAa_abc: red;
}

.vkZoAa_class1 {
  color: var(--vkZoAa_abc);
  background: var(--bcd);
  font-size: var(--Zvw1Mq_def);
}

-----
{
  root: { name: 'vkZoAa_root', composes: [], isReferenced: false },
  '--abc': { name: '--vkZoAa_abc', composes: [], isReferenced: true },
  class1: { name: 'vkZoAa_class1', composes: [], isReferenced: false }
}
*/

Lightning CSS的CSS Modules还支持一些其它功能,例如自定义标识符,自定义标识符转换范围,pure模式等,这里就不赘述了。

背后的Postcss插件

观察css-loader和postcss-modules的依赖,发现它们都引用了四个前缀一致的PostCSS插件:

  • postcss-modules-local-by-default
  • postcss-modules-scope
  • postcss-modules-extract-imports
  • postcss-modules-values

这四个插件名称都以postcss-modules-开头,都是实现CSS Modules相关的插件,而且也在css-modules自己的仓库列表中。我们逐一介绍一下这几个插件。

postcss-modules-local-by-default

postcss-modules-local-by-default插件的作用是将标识符增加:local(),同时将:global()去掉。这样所有应该被局部化处理的标识符都有:local()标志,没有标志则说明无需处理。首先修改postcss.config.js,引入插件:

const postcssModules = require("postcss-modules-local-by-default");
module.exports = {
  plugins: [postcssModules],
};

然后是要编译的CSS文件和编译结果:

.class1 {
  color: red;
}
.class2:hover {
  color: blue;
}
:global(.class3) {
  color: blue;
}

/* 输出结果
:local(.class1) {
  color: red;
}
:local(.class2):hover {
  color: blue;
}
.class3 {
  color: blue;
}
*/

postcss-modules-scope

postcss-modules-scope插件的作用是将带:local()的CSS标识符转换成新标识符,同时输出一个:export,里面包含标识符转换关系。首先修改postcss.config.js,引入插件:

const postcssModules = require("postcss-modules-scope");
module.exports = {
  plugins: [postcssModules],
};

通过功能说明,我们发现postcss-modules-scope插件的输入实际上就是postcss-modules-local-by-default插件的输出,因此我们把前面的结果拿过来继续编译。可以看到输出的转换关系还是放在CSS文件中。

:local(.class1) {
  color: red;
}
:local(.class2):hover {
  color: blue;
}
.class3 {
  color: blue;
}
/* 输出结果
._E_testProj_css_modules_postcss2_proj_src_index__class1 {
  color: red;
}
._E_testProj_css_modules_postcss2_proj_src_index__class2:hover {
  color: blue;
}
.class3 {
  color: blue;
}
:export {
  class1: _E_testProj_css_modules_postcss2_proj_src_index__class1;
  class2: _E_testProj_css_modules_postcss2_proj_src_index__class2;
}
*/

postcss-modules-extract-imports

postcss-modules-extract-imports插件的作用是处理跨文件的composes,但并不是实际引入文件。首先修改配置文件postcss.config.js:

const postcssModules = require("postcss-modules-extract-imports");
module.exports = {
  plugins: [postcssModules],
};

然后是要编译的CSS文件和编译结果。通过结果可以看到,插件将跨文件的composes输出为了一个特殊标识符,然后在文件上面增加了:import,给出了对应CSS文件中文件标识符的映射关系。

.class1 {
  color: red;
  composes: abc from './style.css';
}
.class2 {
  color: red;
  composes: class1;
}

/* 输出结果
:import("./style.css") {
  i__imported_abc_0: abc;
}
.class1 {
  color: red;
  composes: i__imported_abc_0;
}
.class2 {
  color: red;
  composes: class1;
}
*/

postcss-modules-values

postcss-modules-values插件的作用是处理value变量特性的。首先修改配置文件postcss.config.js:

const postcssModules = require("postcss-modules-values");
module.exports = {
  plugins: [postcssModules],
};

然后是要编译的CSS文件和编译结果。通过结果可以看到,本文件的value变量直接替换成对应值,跨文件的处理方式则类似于postcss-modules-extract-imports,在文件上面增加了:import,标识出了对应关系。

@value colorRed: red;
@value colorBlue: from './styles.css';

.class1 {
  color: colorRed;
  background-color: colorBlue;
}

/* 输出结果
:import('./styles.css') {
  i__const_colorBlue_0: colorBlue;
}
:export {
  colorRed: red;
  colorBlue: i__const_colorBlue_0;
}
.class1 {
  color: red;
  background-color: i__const_colorBlue_0;
}
*/

总结

CSS Modules是非常流行的CSS工具,大部分构建工具都提供了对它的支持。这是因为:

  1. 较好的解决了前端组件化中CSS的问题,避免了CSS污染和冲突问题。
  2. 写法上就是普通CSS文件,不需要修改语法,容易被大众接收。
  3. 与现代前端框架开发集成较好,尤其是React。
  4. 与SCSS和Less等CSS预处理工具都兼容。

相对的,像是一些和CSS语法有区别的特性,例如value变量等,使用的人就少了。甚至新的Lightning CSS干脆不支持。

通过【背后的Postcss插件】这部分,我们了解到CSS Modules功能的实现是由css-modules提供的PostCSS插件实现的。但这些插件不能读取文件,也不能实现将CSS文件导出为JavaScript对象。处理文件和打包是构建工具负责的内容,因此还要构建工具自行适配,完成整个功能的开发提供给使用者。

参考

昨天以前首页

Sass 进阶:当 CSS 学会了编程,变量函数循环全都安排上

作者 kyriewen
2026年3月18日 18:05

昨天我们用Sass告别了手工作坊,学会了变量和嵌套。今天咱们继续深入,看看Sass还有哪些骚操作——模块化、内置函数、条件循环,让你的CSS代码像程序一样聪明。准备好,我们要从“会用”进化到“玩出花”了。

前言

还记得你第一次写CSS时的样子吗?一个属性一个属性地敲,一个颜色一个颜色地复制,改个主题色就像在玩“大家来找茬”。昨天学了Sass基础,你已经能像模像样地用变量和嵌套了,感觉自己有点东西了是吧?

但Sass的真正威力远不止于此。今天我们要学的这些东西,会让你忍不住喊出:“卧槽,CSS还能这样写?”——模块化让你的代码像乐高一样拼装,内置函数让颜色和数字自动计算,循环让你批量生成样式就像打印传单。准备好了吗?上车!

一、模块化:别再写一个几千行的巨型文件了

你有没有见过那种一个文件几千行的CSS?看到就想吐对吧?维护起来更是噩梦。Sass的模块化功能就是来拯救你的。

1. @use:新一代的模块引入

以前Sass用@import,但@import有个问题:它会把你引入的所有东西都混到一个全局作用域里,变量重名就覆盖,混乱不堪。现在推荐用@use,它创建了命名空间,隔离了变量。

// _variables.scss
$primary-color: #8A2BE2;
$font-stack: 'Helvetica', sans-serif;

// _buttons.scss
@use 'variables';
.button {
  background: variables.$primary-color;  // 通过命名空间访问
  font-family: variables.$font-stack;
}

看到没,通过variables.前缀访问,清清楚楚,妈妈再也不用担心我变量重名了。

2. 下划线开头的“部分文件”

你发现上面文件名是_variables.scss,有个下划线。这是Sass的约定:下划线开头的文件是“部分文件”,不会被单独编译成CSS,只用来被别的文件引入。就像厨房里的半成品食材,不直接上桌,但做菜要用。

3. @forward:合并转发

如果你有一堆工具函数,想打包成一个入口文件让别人用,就用@forward

// _mixins.scss
@mixin flex-center { ... }

// _variables.scss
$primary: #8A2BE2;

// _index.scss
@forward 'variables';
@forward 'mixins';

// main.scss
@use 'index' as *;  // 直接使用所有转发的成员,无需命名空间

这样别人只要引入index,就能用你所有的变量和混入,方便又优雅。

二、内置函数:Sass自带的神兵利器

Sass内置了很多函数,让你能操作颜色、数字、字符串,就像JavaScript一样。这些函数能帮你省掉无数计算和手工调整。

1. 颜色函数:调色盘在手

颜色是CSS里最烦人的东西之一。你要一个颜色变暗10%?用darken()。变亮?用lighten()。混合两个颜色?用mix()

$primary: #8A2BE2;

.btn {
  background: $primary;
  
  &:hover {
    background: darken($primary, 10%);  // 变暗10%
  }
  
  &.disabled {
    background: lighten($primary, 20%);  // 变亮20%
  }
}

.card {
  border: 1px solid rgba($primary, 0.3);  // 转成半透明
}

还有adjust-hue()调整色相,saturate()增加饱和度,desaturate()降低饱和度……总之,调色不再靠肉眼,全交给Sass计算。

2. 数字函数:算清楚

percentage(0.3) 转成30%,round(3.14) 取整,min(1, 2, 3) 取最小值,max()取最大值。这些函数在处理响应式尺寸时尤其好用。

$container-width: 1200px;
$gutter: 20px;

.item {
  width: percentage(1/3);  // 33.33333%
  margin-right: $gutter;
  
  &:nth-child(3n) {
    margin-right: 0;
  }
}

3. 字符串函数:玩文字

quote()加引号,unquote()去引号,str-index()查找位置,str-insert()插入。虽然不是天天用,但需要的时候真香。

4. 检查函数:知己知彼

type-of($var)返回变量类型,unit(10px)返回单位,unitless(10px)判断是否有单位。写mixin时常用到,比如:

@mixin size($value) {
  @if unitless($value) {
    // 如果没单位,默认px
    width: #{$value}px;
  } @else {
    width: $value;
  }
}

三、控制指令:让CSS长脑子

这才是Sass最像编程语言的地方——有了条件判断和循环,你就能批量生成样式,再也不用一个一个手写了。

1. @if:聪明的条件判断

根据不同的情况输出不同的样式。

@mixin theme($mode) {
  @if $mode == 'light' {
    background: white;
    color: black;
  } @else if $mode == 'dark' {
    background: #333;
    color: white;
  } @else {
    background: gray;
    color: black;
  }
}

.light-theme { @include theme(light); }
.dark-theme { @include theme(dark); }

这个例子在真实项目中很有用,比如根据主题切换颜色。

2. @for:循环造样式

你写过这样的代码吗?

.m-1 { margin: 4px; }
.m-2 { margin: 8px; }
.m-3 { margin: 12px; }
.m-4 { margin: 16px; }
.m-5 { margin: 20px; }

写了五行手就酸了。用@for:

@for $i from 1 through 5 {
  .m-#{$i} {
    margin: #{$i * 4}px;
  }
}

一行代码生成了五个类,想生成到100也是分分钟的事。through包括结束值,to不包括。

3. @each:遍历列表

如果你要基于一个列表生成样式,比如不同颜色的按钮:

$colors: (primary: #8A2BE2, success: #28a745, danger: #dc3545);

@each $name, $color in $colors {
  .btn-#{$name} {
    background: $color;
    color: white;
    
    &:hover {
      background: darken($color, 10%);
    }
  }
}

一键生成三个按钮样式,想加新的颜色?往列表里加一项就行。

4. @while:用条件控制循环

虽然不如for常用,但遇到动态条件时很有用。比如生成一个步长递增的系列:

$i: 6;
@while $i > 0 {
  .item-#{$i} {
    width: 2px * $i;
  }
  $i: $i - 2;
}

四、实战:用Sass生成一个完整的工具类库

我们来做点实在的。比如你要做一个工具类库,包含外边距、内边距、文字颜色、背景色,而且要有不同的尺寸和状态。手动写?那得写到明年。用Sass的循环和函数,分分钟搞定。

// 定义配置
$spacing-sizes: (0, 4, 8, 12, 16, 20, 24);
$colors: (
  primary: #8A2BE2,
  success: #28a745,
  danger: #dc3545,
  warning: #ffc107
);

// 生成外边距工具类
@each $size in $spacing-sizes {
  .m-#{$size} {
    margin: #{$size}px !important;
  }
  
  .mt-#{$size} {
    margin-top: #{$size}px !important;
  }
  
  .mb-#{$size} {
    margin-bottom: #{$size}px !important;
  }
  
  .ml-#{$size} {
    margin-left: #{$size}px !important;
  }
  
  .mr-#{$size} {
    margin-right: #{$size}px !important;
  }
  
  .mx-#{$size} {
    margin-left: #{$size}px !important;
    margin-right: #{$size}px !important;
  }
  
  .my-#{$size} {
    margin-top: #{$size}px !important;
    margin-bottom: #{$size}px !important;
  }
}

// 生成颜色工具类
@each $name, $color in $colors {
  .text-#{$name} {
    color: $color !important;
  }
  
  .bg-#{$name} {
    background-color: $color !important;
  }
  
  .border-#{$name} {
    border-color: $color !important;
  }
}

这段代码编译后会生成上百个工具类,够你在项目里用一辈子。而且改一个配置,所有类自动更新,爽不爽?

五、进阶技巧:让Sass更上一层楼

1. 使用&选择器的高级玩法

&代表父选择器,除了用在伪类,还能用来生成BEM风格的类名。

.block {
  background: #f5f5f5;
  
  &__element {
    padding: 10px;
  }
  
  &--modifier {
    border: 1px solid red;
  }
}

编译成:

.block { background: #f5f5f5; }
.block__element { padding: 10px; }
.block--modifier { border: 1px solid red; }

完美符合BEM命名规范,还不用手写冗长的类名。

2. 使用@error做校验

在mixin里加参数校验,提前报错,省得调试半天不知道错在哪。

@mixin size($width, $height: $width) {
  @if unitless($width) or unitless($height) {
    @error "width和height必须带单位!";
  }
  
  width: $width;
  height: $height;
}

3. 使用@debug和@warn

调试时输出变量值,或者在即将弃用的样式上给警告。

@debug $primary-color;  // 控制台输出变量值
@warn "这个mixin快过期了,别用了";  // 警告信息

六、总结

Sass真正强大之处,在于它把CSS从“描述语言”变成了“编程语言”。通过今天的内容,你学会了:

  • 模块化:用@use@forward组织代码,告别混乱
  • 内置函数:操作颜色、数字、字符串,让样式自动计算
  • 控制指令@if判断,@for循环,@each遍历,批量生成样式
  • 高级技巧&的妙用,参数校验,调试工具

掌握了这些,你写CSS的效率能提升好几倍。更重要的是,你的样式代码会变得像程序一样有逻辑、可维护。别人还在手动改颜色,你已经用循环生成了整个主题;别人还在复制粘贴,你已经用mixin封装了所有复用逻辑。

明天我们将进入JavaScript的世界,从基础开始重新认识这门“前端灵魂语言”。无论你是想巩固基础,还是查漏补缺,都值得期待。

如果你觉得今天的文章够骚够实用,点个赞让更多人看到。有问题评论区见,我们明天见!

CSS 几何美学:从基础图形到创意绘制的艺术之旅

作者 Lee川
2026年3月15日 15:38

CSS 几何美学:从基础图形到创意绘制的艺术之旅

在 Web 开发的浩瀚星空中,CSS(层叠样式表)往往被视作排版与配色的工具。然而,在资深前端工程师的眼中,CSS 更是一支神奇的画笔。无需依赖任何外部图片资源,仅凭几行代码,我们就能在浏览器画布上勾勒出千变万化的几何图形。

本文将深入解析代码中的图形奥秘,并在此基础上拓展更多高阶画法,带您领略“纯 CSS 绘图”的无限可能。


第一章:代码解码——基础图形的构建逻辑

您提供的代码片段虽然简短,却蕴含了 CSS 绘图的三大核心原理:边框 Trick(Border Trick)圆角裁剪(Border-Radius)变换旋转(Transform)。让我们逐一拆解。

1. 三角形的魔法:边框的障眼法

代码中的 .triangle 类展示了经典的“边框绘图法”。

.triangle {
    width: 5px;
    height: 5px;
    border: 15px solid transparent;
    border-top-color: #f00;
}

原理解析: 当一个元素的宽和高极小(甚至为 0),而边框(border)很宽时,浏览器的渲染引擎会将四个边框渲染为四个梯形,并在中心交汇。

  • border 设为 transparent(透明),意味着我们只保留了边框的“形状”,隐藏了颜色。
  • 单独设置 border-top-color 为红色,就只留下了上方的梯形。
  • 由于底边极窄,这个梯形最终变成了一个完美的等腰三角形注:代码中注释掉的部分展示了四色边框的效果,那是四个不同颜色的三角形拼接成的正方形,是理解此原理的绝佳实验。

2. 扇形与圆弧:圆角的极致运用

代码提供了两种扇形画法,分别代表了两种不同的思路。

思路 A:宽高比控制 (.sector)

.sector {
    width: 100px;
    height: 100px;
    border-radius: 100px 0 0; /* 左上角半径极大,其余为0 */
    background-color: #00f;
}

通过设置 border-radius 的四个值(左上、右上、右下、左下),我们可以独立控制每个角的曲率。当左上角的半径值大于元素本身宽高时,它就会形成一个 90 度的扇形(四分之一圆)。

思路 B:边框与圆角结合 (.sector2)

.sector2 {
    border: 100px solid transparent;
    width: 0;
    border-radius: 100px;
    border-top-color: #f00;
}

这是在三角形原理基础上的进化。给一个宽为 0、边框透明的元素加上 border-radius,会让原本尖锐的边框交汇处变得圆润,从而切割出弧形边缘,形成扇形。

3. 箭头与椭圆:变换与比例

  • 箭头 (.arrow):利用 border 只保留右边和下边,再通过 transform: rotate(45deg) 旋转 45 度,两条边瞬间合二为一,形成一个指向右下方的箭头。这是对话框气泡尾部的经典实现方式。
  • 椭圆 (.oval):最简单的图形。只要 width 不等于 height,再配合 border-radius: 50%,正方形就会拉伸成完美的椭圆。

第二章:进阶扩展——解锁更多 CSS 图形秘籍

基于上述原理,我们可以进一步探索更复杂的图形绘制,无需 SVG 或 Canvas,仅用 CSS 即可实现。

1. 平行四边形 (Parallelogram)

想要让矩形“倾斜”起来?不要直接旋转整个元素(否则内容也会歪斜),请使用 skew 变换。

.parallelogram {
    width: 150px;
    height: 60px;
    background: #8e44ad;
    transform: skew(-20deg); /* 水平倾斜 -20 度 */
}
/* 如果内部有文字,需要反向倾斜回来 */
.parallelogram span {
    display: block;
    transform: skew(20deg); 
}

应用场景:科技感的数据看板、动态按钮背景。

2. 六角星 (Hexagram) / 大卫之星

这是两个等边三角形的叠加。利用伪元素 ::before::after 可以轻松实现,无需额外 HTML 标签。

.star {
    position: relative;
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 80px solid #f1c40f; /* 正三角 */
}
.star::before, .star::after {
    content: "";
    position: absolute;
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
}
.star::before {
    border-bottom: 80px solid #f1c40f;
    top: 30px; /* 调整位置以重叠 */
    transform: rotate(60deg);
}
.star::after {
    border-bottom: 80px solid #f1c40f;
    top: 30px;
    transform: rotate(-60deg);
}

原理:利用绝对定位将三个三角形(一个本体,两个伪元素)以 60 度差值旋转叠加。

3. 心形 (Heart)

心形是浪漫网页设计的标配,它由两个圆形和一个旋转的正方形组合而成。

.heart {
    position: relative;
    width: 50px;
    height: 50px;
    background-color: #e74c3c;
    transform: rotate(-45deg); /* 整体旋转 45 度 */
}
.heart::before, .heart::after {
    content: "";
    position: absolute;
    width: 50px;
    height: 50px;
    background-color: #e74c3c;
    border-radius: 50%; /* 变成圆形 */
}
.heart::before {
    top: -25px; /* 向上移半个身位 */
    left: 0;
}
.heart::after {
    left: 25px; /* 向右移半个身位 */
    top: 0;
}

视觉效果:两个圆分别位于正方形的上方和右方,旋转后完美融合成心形。

4. 对话气泡 (Speech Bubble)

结合“三角形箭头”和“圆角矩形”,我们可以快速制作聊天气泡。

.bubble {
    position: relative;
    width: 120px;
    height: 80px;
    background: #3498db;
    border-radius: 10px;
    padding: 10px;
    color: white;
}
/* 利用伪元素制作尾巴 */
.bubble::after {
    content: "";
    position: absolute;
    bottom: -10px; /* 定位到底部下方 */
    left: 20px;
    width: 0;
    height: 0;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    border-top: 10px solid #3498db; /* 颜色与气泡一致 */
}

5. 加载动画:旋转的圆环 (Loader)

静态图形是基础,动态图形才是灵魂。利用 border 的部分透明化加上 animation,可以制作流畅的 Loading 效果。

.loader {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3; /* 灰色底色 */
    border-top: 4px solid #3498db; /* 蓝色高亮 */
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

原理:一个只有顶部有颜色的圆环,通过无限旋转,视觉上就像在“吃豆人”一样转动,给用户明确的等待反馈。


第三章:为什么选择纯 CSS 绘图?

在 SVG 和 Canvas 如此强大的今天,我们为什么还要钻研 CSS 画图?

  1. 性能极致:CSS 图形由浏览器原生渲染引擎直接绘制,无需下载额外的图片资源,减少了 HTTP 请求,提升了页面加载速度。
  2. 无限缩放:基于矢量的边框和圆角特性,CSS 图形在任何分辨率屏幕(Retina屏、4K屏)下都清晰锐利,绝无锯齿。
  3. 灵活可控:通过 CSS 变量(Custom Properties)和伪类(:hover, :active),图形可以轻易地响应交互、改变颜色或形状,这是静态图片难以比拟的。
  4. 代码即设计:设计师的意图直接转化为代码,减少了切图环节,让前端开发更加流畅。

结语

从那个小小的红色三角形开始,我们看到了 CSS 蕴含的巨大能量。它不仅仅是样式的描述语言,更是一套严谨的几何构建系统。

掌握这些技巧,意味着您不再受限于素材库。无论是需要一个简单的下拉箭头,还是一个复杂的动态徽章,您都可以信手拈来,用代码编织出视觉的奇迹。下一次,当您面对空白的设计稿时,请记得:您的键盘,就是最强大的画笔。

Grid 网格布局:二维世界的布局王者,像下围棋一样掌控页面

作者 kyriewen
2026年3月15日 11:22

如果说Flexbox是“一维战神”,擅长排排坐,那Grid就是“二维霸主”,能同时操控行和列。今天我们就来下这盘“布局围棋”,用网格思想彻底重构网页,让复杂布局变得像填格子一样简单。

前言

还记得小时候玩的方格本吗?一行一行,一列一列,规规矩矩。Grid布局就是把这种“方格本”思维带到了CSS里。你可以在页面上画出任意行、任意列,然后把元素放进去,想放哪格放哪格,甚至可以合并单元格——就像Excel表格,但比Excel灵活一万倍。

Grid是CSS布局的终极武器,尤其适合做页面整体架构、卡片墙、仪表盘这类需要同时控制行和列的场景。如果说Flexbox是特种兵,擅长单兵作战,那Grid就是指挥官,能调动千军万马。

一、Grid的核心概念:容器与项目,行与列

和Flexbox类似,Grid也是作用于父容器和直接子项目。只要在父元素上设置display: griddisplay: inline-grid,你就开启了一个网格世界。

.container {
  display: grid;
}

默认情况下,网格只有一列,行高由内容决定。要真正“画”出网格,你需要用grid-template-rowsgrid-template-columns定义行和列。

二、定义网格:画出你的棋盘

1. 固定行高和列宽

你可以用各种单位定义行列的尺寸,比如像素、百分比、em等。

.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;  /* 三列,每列200px */
  grid-template-rows: 100px 150px;           /* 两行,第一行100px,第二行150px */
}

这样你就画了一个3列2行的网格,一共6个格子。项目会按顺序自动填充每个格子,就像表格里从左到右、从上到下填数据一样。

2. fr单位:分蛋糕神器

Grid引入了fr单位(fraction的缩写),表示剩余空间的比例分配。这比Flexbox的flex-grow更直观。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;  /* 三列,中间占两份,两边各占一份 */
}

如果父容器宽度是1200px,那么第一列占300px,第二列600px,第三列300px。fr可以和固定单位混用,比如200px 1fr 2fr,浏览器会先分配200px,剩下的按比例分。

3. repeat() 函数:偷懒必备

如果你要定义很多等宽的列,手动写很累。repeat()函数来救场。

.container {
  grid-template-columns: repeat(3, 1fr);  /* 三列等宽,相当于 1fr 1fr 1fr */
  grid-template-rows: repeat(4, 100px);   /* 四行,每行100px */
}

repeat()还可以组合不同模式,比如repeat(2, 100px 1fr)表示重复两次“100px 1fr”的序列,最终得到四列:100px、1fr、100px、1fr。

4. minmax():给尺寸一个范围

有时候我们希望列宽能在一定范围内弹性变化,比如最小200px,最大自适应。minmax()搞定。

.container {
  grid-template-columns: minmax(200px, 1fr) 2fr;  /* 第一列最小200px,可以放大到1fr,第二列固定2fr */
}

5. auto-fill 与 auto-fit:响应式利器

当列数不确定时,可以用auto-fillauto-fit配合minmax实现类似“流动布局”的效果。

  • auto-fill:尽可能多地填充列,即使某些列是空的。
  • auto-fit:也是尽可能多地填充,但会把空列收缩为0,让有内容的列伸展。
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

这段代码的意思是:每列最小200px,如果容器足够宽,就放尽可能多的列,并且每列等宽;当容器变窄时,列数自动减少,每列仍不小于200px。这就是纯CSS实现的响应式卡片墙!

三、网格线:定位的坐标系

每个网格都由网格线划分。比如3列有4条纵向网格线(从1开始编号),2行有3条横向网格线。你可以用这些线来精确放置项目。

.item {
  grid-column-start: 1;
  grid-column-end: 3;   /* 从第1条纵向线跨到第3条线,即占据前两列 */
  grid-row-start: 1;
  grid-row-end: 3;      /* 从第1条横向线跨到第3条线,即占据前两行 */
}

简写为:

.item {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

也可以从哪条线开始,并指定跨度:

.item {
  grid-column: 1 / span 2;  /* 从第1列开始,跨2列,等同于1/3 */
  grid-row: 1 / span 2;
}

网格线也可以命名,比如grid-template-columns: [main-start] 1fr [main-end],然后使用命名来定位。

四、网格区域:给格子起名字

如果你觉得用线编号不够直观,可以给网格区域命名。用grid-template-areas属性来划分区域。

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: 100px 1fr 100px;
  grid-template-areas:
    "header header header"
    "sidebar content aside"
    "footer footer footer";
}
.header {
  grid-area: header;
}
.sidebar {
  grid-area: sidebar;
}
.content {
  grid-area: content;
}
.aside {
  grid-area: aside;
}
.footer {
  grid-area: footer;
}

这个布局清晰得像图纸一样,每个区域的名字直接对应一个网格单元格。注意,grid-template-areas里的每个单元格必须填满,不能有空洞;可以用.表示空单元格。

五、间距与对齐:让网格透气

1. 行列间距

gap属性设置网格线之间的间距,可以分别设置行间距和列间距:

.container {
  gap: 20px;            /* 行列间距都是20px */
  row-gap: 10px;        /* 单独设置行间距 */
  column-gap: 15px;     /* 单独设置列间距 */
}

2. 项目在单元格内的对齐

项目默认填满整个单元格,但你可以控制它们的位置。

  • justify-items:控制项目在单元格内水平方向的对齐(左中右)。
  • align-items:控制项目在单元格内垂直方向的对齐(上中下)。
  • 取值:startendcenterstretch(默认)。
.container {
  justify-items: center;   /* 所有项目水平居中 */
  align-items: center;     /* 所有项目垂直居中 */
}

如果想单独控制某个项目,用justify-selfalign-self

3. 整个网格在容器内的对齐

如果网格的总尺寸小于容器,可以用justify-contentalign-content控制网格整体的对齐,类似于Flexbox。

.container {
  justify-content: center;   /* 网格整体水平居中 */
  align-content: center;     /* 网格整体垂直居中 */
}

取值同样是startendcenterspace-betweenspace-aroundspace-evenly

六、实战:用Grid搭建常见布局

1. 经典三栏布局(圣杯)

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  gap: 20px;
}

就这么简单,三栏就出来了,而且中间自适应。

2. 响应式卡片墙

我们希望卡片最小200px,尽量填满容器,而且自动换行。

.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

无论容器多宽,卡片都会自动调整列数,完美响应式。

3. 不规则布局:杂志风格

比如一个封面图占两列,下方三个卡片各占一列。

<div class="magazine">
  <div class="feature">封面大图</div>
  <div class="card">卡片1</div>
  <div class="card">卡片2</div>
  <div class="card">卡片3</div>
</div>
.magazine {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.feature {
  grid-column: 1 / -1;  /* 从第一列线到最后一列线,即占满整行 */
}

-1表示最后一条网格线,很方便。

4. 后台管理仪表盘

后台常有复杂区域划分,用grid-template-areas最合适。

.dashboard {
  display: grid;
  grid-template-columns: 250px 1fr 300px;
  grid-template-rows: 80px 1fr 60px;
  grid-template-areas:
    "header header header"
    "sidebar main widgets"
    "footer footer footer";
  height: 100vh;
}

各区域对号入座,结构一目了然。

5. 叠加效果

Grid还可以让多个元素重叠。通过给它们设置相同的网格区域,然后利用z-index控制层级。

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.item1 {
  grid-area: 1 / 1 / 2 / 3;  /* 占满整行 */
  background: red;
  z-index: 1;
}
.item2 {
  grid-area: 1 / 2 / 2 / 3;  /* 只占第二列 */
  background: blue;
  opacity: 0.5;
  z-index: 2;  /* 显示在上层 */
}

七、Grid vs Flexbox:怎么选?

很多人纠结什么时候用Grid,什么时候用Flexbox。其实很简单:

  • Flexbox:一维布局,适合控制元素在一条线上的排列(导航、按钮组、等分列表)。
  • Grid:二维布局,适合同时控制行和列(页面整体架构、卡片墙、仪表盘)。

它们不是替代关系,而是配合关系。你可以在Grid单元格里用Flexbox排列内部元素,也可以把Flexbox项目里再嵌套Grid。两者结合,天下无敌。

八、常见坑点与避坑指南

1. 默认不是严格的一行一列

如果只设置display: grid而不定义行列,默认只有一列,行数由项目数量决定(每个项目占一行)。所以一定要定义行列。

2. 项目会自动填充,但可能超出网格

如果项目数量超过网格单元格,会自动创建隐式网格(新行),行高默认auto。你可以用grid-auto-rows控制隐式行的高度。

.container {
  grid-auto-rows: 100px;  /* 隐式创建的行高100px */
}

3. fr 和 minmax 结合时注意死循环

minmax(200px, 1fr)的意思是:优先让列宽为1fr,但不会小于200px。这通常没问题,但如果你把所有列都设成这样,且容器总宽度小于列数*200px,就会出现溢出(因为每列都强制不小于200px)。这时可以改用auto-fitminmax的巧妙组合,或者用max-width限制。

4. gap 会占用 fr 空间

间隙是在分配fr之前扣除的。比如三列1fr,gap 20px,那么每列实际宽度 = (容器宽度 - 40px) / 3。所以计算时要考虑间隙。

5. 网格线编号从1开始,不是0

这个容易搞错。不过可以用-1表示最后一条线,比较方便。

九、总结

Grid布局是CSS给前端开发者的一份大礼,它把网页布局变成了一种直观、可预测的体验。核心要点:

  • grid-template-columnsgrid-template-rows定义网格结构。
  • frrepeat()minmax()灵活控制尺寸。
  • grid-column/grid-rowgrid-area放置项目。
  • gap控制间距,用justify/align控制对齐。
  • auto-fitminmax实现响应式。
  • 复杂布局用grid-template-areas命名,代码如设计图。

Grid不难,关键是多动手画格子。一旦你习惯了这种“下围棋”式的布局思维,你会发现以前那些棘手的布局都变成了填空题。

如果你喜欢这篇文章,欢迎点赞、收藏、分享。明天我们将进入CSS另一个重要话题——响应式设计与移动端适配,教你如何一套代码搞定手机、平板、电脑。


明日预告:响应式设计的核心:媒体查询、流式布局、移动端适配,从零构建一个全端兼容的页面。

❌
❌