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 更高,代价是失去了类名的可读性和全局覆盖的便利性。了解了两者差异后,根据你对“隔离纯度”和“开发灵活度”的取舍来选型,大概率不会踩坑。