支持 Scoped CSS
关于本章
本章介绍如何实现 Vue 的 Scoped CSS 功能.
学习如何为每个组件隔离样式,防止样式冲突.
什么是 Scoped CSS?
Scoped CSS 是将 <style scoped> 中定义的样式仅应用于该组件的功能.
<template>
<p class="message">Hello</p>
</template>
<style scoped>
.message {
color: red;
}
</style>此样式不会影响其他组件中具有相同类名的元素.

在大型应用中,不同组件可能使用相同的类名.
没有 Scoped CSS,样式可能会意外影响其他组件.
通过为每个组件隔离样式,可以安全地进行样式设计!
工作原理
Scoped CSS 通过以下步骤实现:
- 生成作用域 ID:为每个组件创建唯一 ID
- 转换模板:为元素添加
data-v-xxx属性 - 转换样式:为选择器添加
[data-v-xxx]
转换示例
<!-- 输入 -->
<template>
<p class="message">Hello</p>
</template>
<style scoped>
.message {
color: red;
}
</style><!-- 输出 (HTML) -->
<p class="message" data-v-7ba5bd90>Hello</p>
<!-- 输出 (CSS) -->
<style>
.message[data-v-7ba5bd90] {
color: red;
}
</style>生成作用域 ID
为每个组件生成唯一 ID.通常使用文件路径的哈希值.
// packages/compiler-sfc/src/parse.ts
import { createHash } from 'crypto'
export function parse(
source: string,
{ filename = DEFAULT_FILENAME }: SFCParseOptions = {},
): SFCParseResult {
const descriptor: SFCDescriptor = {
id: undefined!,
filename,
source,
template: null,
script: null,
scriptSetup: null,
styles: [],
}
// 生成作用域 ID
descriptor.id = createHash('sha256')
.update(filename + source)
.digest('hex')
.slice(0, 8)
// ... 其余解析处理
}扩展 SFCStyleBlock
为样式块添加 scoped 信息.
// packages/compiler-sfc/src/parse.ts
export interface SFCStyleBlock extends SFCBlock {
type: "style"
scoped?: boolean // 添加
}
function createBlock(node: ElementNode, source: string): SFCBlock {
// ...
node.props.forEach((p) => {
if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true
if (type === "style") {
if (p.name === "scoped") {
(block as SFCStyleBlock).scoped = true
}
}
}
})
return block
}模板转换
在模板编译期间为元素添加 scopeId 属性.
// packages/compiler-core/src/codegen.ts
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, scopeId } = context
const { tag, props, children } = node
// 如果存在 scopeId,添加到 props
let propsWithScope = props
if (scopeId) {
const scopeIdProp = `"data-v-${scopeId}": ""`
if (props) {
// 与现有 props 合并
propsWithScope = `{ ...${props}, ${scopeIdProp} }`
} else {
propsWithScope = `{ ${scopeIdProp} }`
}
}
push(helper(CREATE_ELEMENT_VNODE) + `(`)
genNodeList(genNullableArgs([tag, propsWithScope, children]), context)
push(`)`)
}样式转换
为 CSS 选择器添加作用域属性选择器.
// packages/compiler-sfc/src/compileStyle.ts
import postcss from 'postcss'
export interface SFCStyleCompileOptions {
source: string
filename: string
id: string
scoped?: boolean
}
export function compileStyle(options: SFCStyleCompileOptions): string {
const { source, id, scoped } = options
if (!scoped) {
return source
}
// 使用 PostCSS 转换选择器
const result = postcss([scopedPlugin(id)]).process(source, { from: undefined })
return result.css
}
function scopedPlugin(id: string) {
const scopeId = `data-v-${id}`
return {
postcssPlugin: 'vue-sfc-scoped',
Rule(rule) {
// 为选择器添加 [data-v-xxx]
rule.selectors = rule.selectors.map((selector) => {
return `${selector}[${scopeId}]`
})
},
}
}Vite 插件集成
// packages/@extensions/vite-plugin-chibivue/src/main.ts
async function genStyleCode(descriptor: SFCDescriptor): Promise<string> {
let stylesCode = ``
for (let i = 0; i < descriptor.styles.length; i++) {
const style = descriptor.styles[i]
const src = descriptor.filename
const scoped = style.scoped ? '&scoped=true' : ''
const query = `?chibivue&type=style&index=${i}${scoped}&lang.css`
const styleRequest = src + query
stylesCode += `\nimport ${JSON.stringify(styleRequest)}`
}
return stylesCode
}
// 在 Vite 插件的 load 中编译样式
load(id) {
const { filename, query } = parseChibiVueRequest(id)
if (query.chibivue && query.type === "style") {
const descriptor = getDescriptor(filename, options)!
const style = descriptor.styles[query.index!]
if (query.scoped) {
return {
code: compileStyle({
source: style.content,
filename,
id: descriptor.id,
scoped: true,
})
}
}
return { code: style.content }
}
}
我们使用 PostCSS 进行样式转换.
PostCSS 是一个可以将 CSS 作为 AST 处理的工具,使选择器转换变得简单.
Vue.js 内部也使用 PostCSS!
测试
<!-- ComponentA.vue -->
<template>
<p class="text">Component A</p>
</template>
<style scoped>
.text {
color: red;
}
</style><!-- ComponentB.vue -->
<template>
<p class="text">Component B</p>
</template>
<style scoped>
.text {
color: blue;
}
</style>两个组件使用相同的类名 .text,但显示不同的颜色.
特殊选择器
Scoped CSS 支持几个特殊的选择器.
:deep() 选择器
用于修改子组件的样式.
<style scoped>
:deep(.child-class) {
color: blue;
}
</style>转换后:
[data-v-xxx] .child-class {
color: blue;
}::v-slotted() 选择器
为插槽内容应用样式.
<style scoped>
::v-slotted(.slot-content) {
font-weight: bold;
}
</style>转换后:
.slot-content[data-v-xxx-s] {
font-weight: bold;
}-s 后缀表示"slotted(插槽)". 由于插槽内容来自父组件, 使用特殊的插槽作用域 ID 而不是常规的作用域 ID.
:global() 选择器
在 scoped 样式块中定义全局样式.
<style scoped>
:global(.global-class) {
margin: 0;
}
</style>转换后:
.global-class {
margin: 0;
}使用 v-bind() 的动态样式
可以在 CSS 中使用组件状态.
<script setup>
import { ref } from 'vue'
const color = ref('red')
</script>
<style scoped>
.text {
color: v-bind(color);
}
</style>转换后:
.text[data-v-xxx] {
color: var(--xxx-color);
}v-bind() 被转换为 CSS 自定义属性(CSS 变量). 在运行时,CSS 变量的值作为组件的内联样式设置.
使用复杂表达式
通过引号包裹可以使用复杂的表达式.
<style scoped>
.box {
width: v-bind('size + "px"');
background: v-bind('theme.colors.primary');
}
</style>
v-bind() 是一个方便的功能,但有性能影响:
- 每个
v-bind()作为 CSS 自定义属性设置在内联样式中 - 每次值更改时都会触发样式重新计算
- 对于频繁更改的值,直接使用内联样式可能更高效
对于动画或频繁更新,请考虑使用内联样式或 CSS 动画代替 v-bind().
未来扩展
还可以考虑以下功能:
- CSS Modules:自动类名生成
- CSS-in-JS 集成:增强动态样式

参考本章介绍的原理,尝试自己实现 Scoped CSS!
这也是学习如何使用 PostCSS 的好机会.
到此为止的源代码: chibivue (GitHub)
总结
- Scoped CSS 为每个组件隔离样式
- 生成唯一的 scopeId 并应用于模板和样式
- 模板获得
data-v-xxx属性,CSS 获得[data-v-xxx]选择器 - 使用 PostCSS 转换选择器
参考链接
- Vue.js - Scoped CSS - Vue 官方文档
- PostCSS - CSS 转换工具
