人人都会AI编程

CSS Modules 样式隔离方案

更新时间:2026-07-09

CSS Modules 不是 Vue 独有的功能,而是一种将 CSS 类名局部作用域化的通用方案。它的核心思路很简单:你写的 .button 类名,在最终输出的样式表中会被自动替换成一个带哈希的唯一类名,比如 .button_3a8f2b。不同组件中即使写了同名的 .button,最终生成的类名也不会冲突。

Vue 对 CSS Modules 提供了开箱即用的支持——只要把 <style> 标签加上 module 属性,样式就会被当作一个模块导出,在模板中通过 $style 对象访问。

基本用法

<template>
  <button :class="$style.button">提交</button>
</template>

<style module>
.button {
  background-color: #409eff;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
}
</style>

编译后,$style.button 的值会是一个类似 button_3a8f2b 的唯一字符串,最终 HTML 变成:

<button class="button_3a8f2b">提交</button>

对应的 CSS 也变成了:

.button_3a8f2b { ... }

命名模块
如果有多个 <style module>,或者想让输出的对象按键明确区分,可以给模块起个名字:

<template>
  <div :class="layout.header">头部</div>
</template>

<style module="layout">
.header { ... }
</style>

此时通过 layout.header 访问。

组合多个类
$style 是一个普通对象,用数组或字符串拼接即可组合:

<div :class="[$style.base, isActive && $style.active]">内容</div>

也可以用对象语法:

<div :class="{ [$style.active]: isActive }">内容</div>

与 Scoped 的区别

Vue 也有自己的样式隔离方案——scoped,它通过给组件内所有元素添加 data-v-xxxx 属性和对应属性选择器来限制样式作用范围。两者的关键差异:

| 特性 | Scoped | CSS Modules |
|------|--------|-------------|
| 隔离原理 | 自定义属性 + 属性选择器 | 编译时重命名类名 |
| 类名是否可读 | 原始类名保留,增加属性选择器 | 类名变成哈希字符串 |
| 样式穿透 | 需要 :deep() 等组合器 | 直接通过 :global() 声明全局类 |
| 外部组件样式覆盖 | 较难,需要穿透或更高优先级 | 同样需要 :global() 或变量混合 |
| 对子组件的根节点影响 | 会自动穿透到子组件根节点 | 不会,完全隔离 |
| JS 侧引用方式 | 直接写字符串类名 | 通过 $style 对象引用,有 TS 提示 |

一个直观的场景:你封装了一个通用按钮组件,外部想覆盖它的样式。用 scoped 时,外部需要写 :deep(.button) 才能命中;用 CSS Modules 时,类名已被哈希化,外部根本无法用原有类名选中,只能通过组件暴露的 props 或 CSS 变量来控制。这种“绝对隔离”对组件库开发非常有利,但对项目内部的样式调整就不太方便。

全局样式与混用

CSS Modules 允许你声明一个类名为全局的,即使它在 module 块内:

:global(.clearfix) {
  /* 这个类名不会被哈希化,全局可用 */
}

也可以同时使用 scoped 和 module,但通常没这个必要,选一种即可。

在实际项目中的选择

  • 用 Scoped:绝大多数业务组件,开发速度快,类名直观,配合 CSS 变量和 :deep() 就能满足大部分定制需求。
  • 用 CSS Modules:组件库、多人协作的大型项目,或者你对“样式绝对隔离”有执念。它能通过 $style 利用 TypeScript 的类型检查,避免写错类名。此外,Vite 对 CSS Modules 有极佳的支持,启动和 HMR 几乎无额外开销。
  • 混合使用:一些团队把 CSS Modules 用于“原子化样式”或高复用的基础组件,Scoped 用于页面级组件,两者在同一个项目中可以共存。

小坑与注意点

  • 动态类名:如果你需要根据 props 拼接类名,比如 $style[size-${size}],要确保所有可能的组合都在模板中被用到,否则 CSS Modules 可能因为未使用的类没有被保留而产生缺失。解决方案是显式声明类名字符串映射,或使用 :global() 兜底。
  • CSS 预处理:完全可以和 Less / Sass 一起用。比如 <style module lang="scss">,里面的变量、混入仍然有效,只是最终输出的类名会被哈希化。
  • Vue 2 支持:Vue 2 也支持 CSS Modules(需配合 vue-loader),用法略有不同:<style module> 会自动注入一个计算属性 $style,和 Vue 3 行为一致。

总的来说,CSS Modules 提供了一种“像写 JS 模块一样写 CSS”的体验,它的隔离等级比 Scoped 更高,代价是失去了类名的可读性和全局覆盖的便利性。了解了两者差异后,根据你对“隔离纯度”和“开发灵活度”的取舍来选型,大概率不会踩坑。