Vue CSS scoped 的实现原理
通过 vue-loader 为组件添加唯一属性选择器,实现样式隔离
问题
Vue 组件中使用 <style scoped> 后,样式为什么不会相互污染?
解答
渲染结果
当使用 <style scoped> 时,Vue 会为每个组件生成唯一的属性标识:
- 每个组件的 HTML 标签会添加
data-v-[hash:8]属性 - 子组件的根标签同时拥有父组件和自身的
data-v-[hash:8]属性 - CSS 选择器会自动添加对应的属性选择器
[data-v-hash:8]
例如:
<!-- 父组件 -->
<div data-v-2a183b78>
<child data-v-2a183b78 data-v-5e4a9c3d></child>
</div>
/* 原始样式 */
.container { color: red; }
/* 编译后 */
.container[data-v-2a183b78] { color: red; }
实现流程
vue-loader 处理 .vue 文件时的关键步骤:
1. 解析 .vue 文件
vue-loader 会将 .vue 文件拆分成 template、script、style 三部分,并为带 scoped 属性的 style 生成唯一 id:
// lib/index.js
const descriptor = parse({
source: content,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// 为 scoped 样式生成 id
const id = hash(
isProduction
? (shortFilePath + '\n' + content)
: shortFilePath
)
2. 生成样式导入请求
对于 scoped 样式,生成特殊的 import 请求:
let stylesCode = ``
if (descriptor.styles.length) {
descriptor.styles.forEach((style, i) => {
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
const scoped = style.scoped ? `&scoped=true` : ``
const query = `?vue&type=style&index=${i}${scoped}`
stylesCode += `\nimport ${src}${query}`
})
}
3. 处理样式内容
style-post-loader 负责为 CSS 选择器添加属性选择器:
// stylePostLoader.js
const { compileStyle } = require('@vue/component-compiler-utils')
module.exports = function (source, inMap) {
const query = qs.parse(this.resourceQuery.slice(1))
const { code, map, errors } = compileStyle({
source,
filename: this.resourcePath,
id: `data-v-${query.id}`,
map: inMap,
scoped: !!query.scoped,
trim: true
})
this.callback(null, code, map)
}
4. 注入到模板
template 编译时,会在根元素上添加 data-v-[hash] 属性:
// 编译选项
const finalOptions = {
scopeId: query.scoped ? `data-v-${id}` : null,
// ...
}
样式优先级问题
如果父子组件有相同的选择器,子组件样式可能被父组件覆盖,因为:
- 子组件先于父组件 mounted
- 父组件的样式后插入,优先级更高
解决方法是使用更具体的选择器或深度选择器 ::v-deep。
关键点
- vue-loader 为每个组件生成唯一的 hash 值作为
data-v-[hash]属性 - 通过 PostCSS 为 CSS 选择器自动添加属性选择器,实现样式隔离
- 子组件根元素会同时拥有父组件和自身的 data 属性,使父组件可以控制子组件根元素样式
- 相同选择器时,父组件样式会覆盖子组件样式(因为父组件后 mounted)
- scoped 只影响当前组件,不影响全局样式和子组件内部元素
目录