Vue CSS scoped 的实现原理

通过 vue-loader 为组件添加唯一属性选择器,实现样式隔离

问题

Vue 组件中使用 <style scoped> 后,样式为什么不会相互污染?

解答

渲染结果

当使用 <style scoped> 时,Vue 会为每个组件生成唯一的属性标识:

  1. 每个组件的 HTML 标签会添加 data-v-[hash:8] 属性
  2. 子组件的根标签同时拥有父组件和自身的 data-v-[hash:8] 属性
  3. 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 只影响当前组件,不影响全局样式和子组件内部元素