CSS Modules 与 scoped
CSS Modules 通过生成后的类名隔离样式;Vue scoped 为组件模板和选择器添加作用域属性。
核心结论
两种方案都在构建阶段避免局部类名冲突。CSS Modules 把类名映射显式导入 JavaScript;Vue SFC 的 scoped 同时改写模板属性和 CSS 选择器。
构建流程
组件关系与数据流
ComponentBuildStyle
差异焦点CSS 以模块对象进入 JavaScript,类名引用是显式的。
实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。
flowchart TB
accTitle: React CSS Modules 显式导入流程
accDescr: 独立 module CSS 文件在构建时生成唯一类名和 JavaScript 映射,ReactDemo 必须 import styles 并通过 className 显式读取映射。
source[("ReactDemo.module.css")] --> build{{"构建时生成局部类名"}}
build --> mapping[("styles 对象映射")]
mapping -->|"import styles"| demo["ReactDemo.tsx"]
demo -->|"className={styles.card}"| dom[("DOM 唯一类名")]
class demo component
class build process
class source,mapping,dom resource
classDef state fill:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4px样式文件与组件文件分离,通过 import 和 styles.* 显式绑定生成类名。
组件关系与数据流
ComponentBuildStyle
差异焦点作用域由 SFC 编译器注入,无需在脚本中导入样式映射。
实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。
flowchart TB
accTitle: Vue SFC scoped 编译流程
accDescr: template 和 style scoped 位于同一个 Vue SFC,编译器同时改写模板属性与 CSS 选择器,普通 class 无需通过 JavaScript 导入。
sfc["VueDemo.vue SFC"] --> build{{"同时编译 template + style"}}
build --> template[("模板添加 data-v-*")]
build --> styles[("选择器追加 data-v-*")]
template --> dom[("匹配当前组件 DOM")]
styles --> dom
class sfc component
class build process
class template,styles,dom resource
classDef state fill:#fffdf8,stroke:#2f9d6b,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f4eb,stroke:#2f9d6b,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f2f8f4,stroke:#2f9d6b,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4pxSFC 编译器同时改写模板和 scoped 选择器,组件内继续使用普通 class。
完整 Demo 源码
以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。
React 源码
import styles from "./ReactDemo.module.css";export function ReactDemo() { return ( <section className={styles.card}> <span className={styles.badge}>CSS Modules</span> <h2>局部作用域卡片</h2> <p> JSX 使用 <code>styles.card</code>,构建后生成唯一类名。 </p> <span className={styles.button}> 仅影响当前组件 </span> </section> );}Vue 3 源码
<template> <section class="card"> <span class="badge">Vue scoped</span> <h2>局部作用域卡片</h2> <p>模板使用普通 class,编译器自动添加作用域属性。</p> <span class="button">仅影响当前组件</span> </section></template><style scoped>.card { display: grid; gap: 0.75rem; padding: 1.1rem; border: 1px solid #83d7b1; border-left: 3px solid #2f9d6b; border-radius: 0.25rem; color: #25221d; background: #f2f8f4;}.card h2,.card p { margin: 0;}.badge { width: max-content; border-radius: 999px; padding: 0.2rem 0.55rem; color: #176947; font-size: 0.7rem; font-weight: 800; background: #c9f5df;}.button { width: max-content; border: 0; border-radius: 0.25rem; padding: 0.55rem 0.75rem; color: #fff; font: inherit; font-size: 0.78rem; font-weight: 750; background: #2f855f;}</style>在线观察
两个卡片具有相同结构和视觉效果,但绑定生成类名的方式不同。
React
局部作用域卡片
JSX 使用 styles.card,构建后生成唯一类名。
Vue 3
局部作用域卡片
模板使用普通 class,编译器自动添加作用域属性。
使用边界
| 问题 | CSS Modules | Vue scoped |
|---|---|---|
| 类名如何引用 | styles.name | 普通字符串 class |
| 样式位置 | 通常独立 CSS 文件 | 通常与组件同一 SFC |
| 隔离方式 | 构建时重命名类名 | 属性选择器限定范围 |
| 深层子组件 | 需显式共享类名/API | 使用 :deep() 时要谨慎 |
主题值和运行时数据不要生成大量动态类名,可继续阅读CSS 变量动态样式。