开发要点:
>>>
选择器在Sass/Scss等预处理器中无法使用,仅在原生CSS中有效/deep/
已被废弃,且现代Vue项目中基本不再使用::v-deep
是Vue 2的语法,Vue 3推荐使用:deep()
- 对于第三方组件,考虑通过CSS变量覆盖样式
配置过程
- 依赖PostCSS :Vue CLI内部使用PostCSS的
postcss-scope
或postcss-selector-namespace
插件 - Webpack配置 :在
vue-loader
的options中配置:
css
{
css: {
loaderOptions: {
postcss: {
plugins: [
require('postcss-selector-namespace')({
namespace(css) {
// 添加scoped处理逻辑
}
})
]
}
}
}
}
穿透选择器的编译结果
1. :deep()
(Vue 3推荐)
css
:deep(.child) {
color: red;
}
编译为:
css
[data-v-f3f3eg9] .child {
color: red;
}
2. ::v-deep
(Vue 2语法)
css
::v-deep .child {
color: red;
}
编译结果与:deep()
相同
3. >>>
(原生CSS支持)
css
.parent >>> .child {
color: red;
}
编译为:
css
.parent[data-v-f3f3eg9] .child {
color: red;
}
Vue scoped CSS 完整处理流程
1. 整体构建流程(以Webpack为例):
rust
Vue SFC文件 -> vue-loader解析 -> 拆解为三部分
├─ template -> 转换为render函数
├─ script -> 转换为组件选项
└─ style(scoped) -> PostCSS处理 -> 生成带scoped的CSS
2. 关键处理阶段:
- SFC解析阶段
vue-loader
使用@vue/compiler-sfc
解析.vue
文件,分离出<style scoped>
块 - PostCSS处理阶段
将提取的CSS送入PostCSS处理管道,应用postcss-scope
或postcss-selector-namespace
插件 - 选择器转换阶段
插件遍历CSS AST,为每个选择器添加[data-v-hash]
属性选择器 - 模板处理阶段
在组件的根元素上自动添加相同的data-v-hash
属性
源码级实现解析
1. 核心源码位置(Vue 2.x):
vue-loader/lib/loaders/stylePostLoader.js
@vue/component-compiler-utils/lib/stylePlugins/scoped.js
2. 关键实现步骤:
- 生成唯一哈希:
bash
const id = 'data-v-' + hash(content + filename)
- AST遍历:
ini
postcss(root => {
root.walkRules(rule => {
// 处理选择器
})
})
- 选择器重写:
ini
const selector = selectorParser(selectors => {
selectors.nodes.forEach(selector => {
// 在最后一个简单选择器后插入属性选择器
let lastNode = null
selector.each(node => {
if (node.type !== 'pseudo' && node.type !== 'combinator') {
lastNode = node
}
})
if (lastNode) {
selector.insertAfter(
lastNode,
selectorParser.attribute({
attribute: id
})
)
}
})
}).processSync(rule.selector)
-
CSS选择器节点类型解析
- 在PostCSS的选择器AST中,每个选择器节点都有特定类型:
gotype SelectorNode = | ClassName // 类选择器 .class | Id // ID选择器 #id | Tag // 标签选择器 div | Attribute // 属性选择器 [type="text"] | Pseudo // 伪类/伪元素 :hover / ::after | Combinator // 组合符 > + ~ 空格
-
判断条件的核心目的
- 排除伪类/伪元素 :避免在
:hover
、::before
等伪类后插入属性选择器 - 排除组合符 :避免在
>
、+
等组合符后插入属性选择器 - 定位真实元素节点:只在真实的DOM元素选择器后添加scoped属性
- 排除伪类/伪元素 :避免在
-
示例分析
原始选择器:
css
.container > .button:hover::after
AST节点结构:
ruby
Selector [
ClassName(.container),
Combinator(>),
ClassName(.button),
Pseudo(:hover),
Pseudo(::after)
]
遍历过程:
.container
→ 非伪类/组合符 → 标记为lastNode>
→ 是组合符 → 跳过.button
→ 非伪类/组合符 → 更新lastNode:hover
→ 是伪类 → 跳过::after
→ 是伪类 → 跳过
最终插入位置:在.button
后插入[data-v-hash]
转换结果:
css
.container[data-v-hash] > .button[data-v-hash]:hover::after
selectorParser.attribute
方法解析
csharp
// 创建属性选择器节点示例
const attrNode = selectorParser.attribute({
attribute: 'data-v-123'
})
// 生成的AST节点
{
type: 'attribute',
attribute: 'data-v-123',
operator: '',
value: ''
}
// 转换为CSS字符串
attrNode.toString() // => "[data-v-123]"
3. 简化版实现代码:
ini
const postcss = require('postcss')
const selectorParser = require('postcss-selector-parser')
module.exports = postcss.plugin('add-scope', options => {
const id = options.id // 例如 'data-v-123456'
return root => {
root.walkRules(rule => {
rule.selector = selectorParser(selectors => {
selectors.each(selector => {
let lastNode = null
selector.each(node => {
if (node.type !== 'pseudo') {
lastNode = node
}
})
if (lastNode) {
selector.insertAfter(
lastNode,
selectorParser.attribute({
attribute: id
})
)
}
})
}).processSync(rule.selector)
})
}
})
编译前后对比的底层逻辑
1. 原始代码:
xml
<style scoped>
.button {
padding: 10px;
}
</style>
2. 转换过程:
- 生成哈希 :
data-v-61zlt9
- AST解析:
json
{
"type": "rule",
"selector": ".button",
"nodes": [
{
"prop": "padding",
"value": "10px"
}
]
}
- 选择器转换:
css
selectorParser转换流程:
.button → .button[data-v-61zlt9]
- 最终输出:
css
.button[data-v-61zlt9] {
padding: 10px;
}
设计亮点
-
哈希生成算法:
- 基于文件内容+文件路径的哈希
- 保证同一组件不同实例相同哈希
- 不同组件哈希绝对唯一
-
性能优化:
- 通过PostCSS的AST操作避免正则替换
- 缓存处理过的选择器
- 并行处理多个样式块
-
伪元素处理:
css
/* 原始 */
::placeholder { color: #999; }
/* 转换后 */
[data-v-f3f3eg9]::placeholder { color: #999; }
Q1: 为什么必须跳过伪类和组合符?
-
伪类原理 :
:hover
等伪类依附于元素本身,单独添加属性选择器会导致匹配失败ruby/* 错误示例 */ .button:hover[data-v-123] {} /* 匹配 <div class="button" data-v-123:hover> */ /* 正确方式 */ .button[data-v-123]:hover {} /* 匹配 <div class="button" data-v-123> 的悬停状态 */
-
组合符原理:组合符描述的是元素间关系,不是元素本身属性
css/* 错误示例 */ .container >[data-v-123] .item {} /* 正确方式 */ .container[data-v-123] > .item[data-v-123] {}
Q2: 边界情况处理
1. 多级伪类
css
input:-webkit-autofill:focus
处理结果:
ruby
input[data-v-123]:-webkit-autofill:focus
2. 属性选择器共存
css
button[disabled].primary
处理结果:
css
button[disabled][data-v-123].primary[data-v-123]
3. 媒体查询中的选择器
css
@media (max-width: 768px) {
.responsive-box {
width: 100%;
}
}
处理结果:
css
@media (max-width: 768px) {
.responsive-box[data-v-123] {
width: 100%;
}
}