一、概念:Scoped 样式的本质
在 Vue 单文件组件(SFC)中,开发者经常在 <style scoped> 中书写局部样式,使其仅作用于当前组件。
例如:
xml
<template>
<div class="foo">Hello</div>
</template>
<style scoped>
.foo { color: red }
</style>
经过编译后,Vue 会自动为 .foo 添加一个独特的属性选择器,如:
css
.foo[data-v-123abc] { color: red }
同时在模板渲染时,Vue 会在元素上加上同样的属性:
ini
<div class="foo" data-v-123abc>Hello</div>
从而实现样式作用域隔离 。
而这一机制的核心逻辑,就在这个 PostCSS 插件 vue-sfc-scoped.ts 中。
二、原理:PostCSS 插件的核心逻辑
该插件通过 AST(抽象语法树)遍历与选择器重写 实现样式"加作用域"功能。
其核心思想如下:
- 检测与修改
@keyframes动画名,避免样式冲突。 - 重写 CSS 选择器 ,在每个选择器上插入
[data-v-xxx]属性。 - 特殊伪类处理 :支持
:deep(),:global(),:slotted()。 - 兼容性警告 :检测废弃的
/deep/与>>>。
三、代码结构总览
typescript
const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null)
const shortId = id.replace(/^data-v-/, '')
return {
postcssPlugin: 'vue-sfc-scoped',
Rule(rule) { processRule(id, rule) },
AtRule(node) { /* 处理 keyframes */ },
OnceExit(root) { /* 重写动画引用 */ },
}
}
核心函数包括:
processRule():解析每条 CSS 规则并调用选择器重写。rewriteSelector():核心算法,负责插入[data-v-id]。extractAndWrapNodes():提取声明与注释,生成包装规则。isSpaceCombinator():判断是否为空格型组合符。
四、详细拆解与逐段注释
1. 处理动画命名空间
javascript
const animationNameRE = /^(?:-\w+-)?animation-name$/
const animationRE = /^(?:-\w+-)?animation$/
const keyframesRE = /^(?:-\w+-)?keyframes$/
这三条正则用于匹配:
- CSS 动画声明(如
animation、animation-name)- 以及
@keyframes定义,包括带厂商前缀的形式(如@-webkit-keyframes)。
2. 注册 @keyframes 并改名
csharp
AtRule(node) {
if (keyframesRE.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
keyframes[node.params] = node.params = node.params + '-' + shortId
}
}
每个组件的动画名都会被加上
-shortId后缀:
less@keyframes fade-in → @keyframes fade-in-123abc这样不同组件的动画不会相互覆盖。
3. 遍历样式树末尾阶段:重写动画名引用
less
OnceExit(root) {
root.walkDecls(decl => {
if (animationNameRE.test(decl.prop)) { ... }
if (animationRE.test(decl.prop)) { ... }
})
}
遍历所有声明,当
animation或animation-name属性的值匹配到被重命名的keyframes时,替换为带-id的新名称。
4. 处理普通 CSS 选择器
scss
function processRule(id: string, rule: Rule) {
if (processedRules.has(rule) || isInsideKeyframes(rule)) return
processedRules.add(rule)
rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => {
rewriteSelector(id, rule, selector, selectorRoot, deep)
})
}).processSync(rule.selector)
}
解释:
processedRules:防止重复处理。- 通过
postcss-selector-parser解析每个选择器。 - 最终调用
rewriteSelector()注入作用域属性。
5. 核心函数 rewriteSelector()
bash
function rewriteSelector(id, rule, selector, selectorRoot, deep, slotted = false)
核心逻辑:
- 遍历选择器节点(class、combinator、pseudo等)。
- 检测特殊伪类:
:deep(),:global(),:slotted()。 - 按逻辑调整选择器结构并在合适位置插入属性选择器。
5.1 处理废弃写法
ini
if (n.value === '>>>' || n.value === '/deep/') {
n.value = ' '
warn('the >>> and /deep/ combinators have been deprecated...')
}
Vue3 已弃用
/deep/和>>>,推荐使用:deep()。
5.2 处理 :deep() 深度选择器
ruby
if (value === ':deep' || value === '::v-deep') {
rule.__deep = true
if (n.nodes.length) {
// 替换 ::v-deep(.bar) -> .bar
} else {
// 兼容废弃的 combinator 用法
}
}
:deep()允许穿透作用域边界,如:
css.foo :deep(.bar) { ... }
5.3 处理 :slotted()
ini
if (value === ':slotted' || value === '::v-slotted') {
rewriteSelector(id, rule, n.nodes[0], selectorRoot, deep, true)
shouldInject = false
}
:slotted()用于样式化插槽内容,会注入[data-v-id-s],区别于普通作用域属性。
5.4 处理 :global()
ini
if (value === ':global' || value === '::v-global') {
selector.replaceWith(n.nodes[0])
}
:global()会让内部选择器脱离作用域,不添加[data-v-id]。
5.5 插入作用域属性
less
selector.insertAfter(
node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
quoteMark: `"`,
})
)
最终插入选择器属性,如:
css.foo → .foo[data-v-123abc]
五、拓展:支持的特殊伪类对比
| 伪类 | 功能 | 是否作用域隔离 | 示例 |
|---|---|---|---|
:deep() |
穿透作用域 | 否 | .a :deep(.b) |
:global() |
全局样式 | 否 | :global(.c) |
:slotted() |
插槽样式 | 特殊作用域 | :slotted(.d) |
六、实践:在 Vue 编译流程中的位置
vue-sfc-scoped 插件在 SFC 编译阶段 由 Vue 的构建工具(如 @vue/compiler-sfc)注入到 PostCSS 流程中:
rust
SFC -> parse <style> -> PostCSS(plugins: [vue-sfc-scoped]) -> CSS Output
编译器通过传入
data-v-xxxx作为插件参数,从而为每个组件生成独立的样式命名空间。
七、潜在问题与优化方向
- 局限于同一个
<style>块内的动画
若动画定义与使用分属不同<style>标签,动画名重写失效。 - 性能问题
对复杂选择器或大规模样式文件,postcss-selector-parser的多层遍历可能影响性能。 - 复杂伪类兼容性
嵌套使用:is()或:where()时逻辑需特别处理,否则可能遗漏作用域。 - 与 CSS Modules 共用时的冲突
两者都是作用域隔离机制,混用时需谨慎设计。
八、总结
vue-sfc-scoped 是 Vue 实现局部样式的底层关键之一。
它通过 PostCSS AST 操作精确插入 [data-v-id] 属性,并兼顾多种伪类写法和动画作用域隔离。
其价值在于:
- 设计简洁:基于选择器注入,兼容原生 CSS。
- 工程友好:与 PostCSS 无缝集成。
- 扩展性强:可轻松支持未来的伪类拓展。
本文部分内容借助 AI 辅助生成,并由作者整理审核。