vue3.5.x 单文件组件(SFC)样式编译过程

Vue 3 SFC(单文件组件,Single-File Component)中的样式编译,是一个将 <style> 块内的内容转换为浏览器可执行代码的构建时(build-time) 过程。

vite ------ vite-plugin-vue - @vue/compiler-sfc

cssPlugin插件

vite-pugin-vue 插件

doCompileStyle

doCompileStyle 函数负责编译 Vue 单文件组件中的样式块,其核心流程为:

  1. 预处理 :根据 preprocessLang(如 scss、less)调用对应预处理器将源码转换为 CSS,并收集预处理阶段的依赖与错误。
  2. PostCSS 处理 :配置并执行 PostCSS 插件链(包括内置的 cssVarsPlugintrimPluginscopedPlugin 以及可选的 postcssModules),对 CSS 进行变量注入、代码修剪、作用域化及 CSS Modules 转换。
  3. 结果生成 :获取 PostCSS 处理后的 CSS 代码和 Source Map,并遍历处理消息中的 dependency 类型,收集所有依赖文件路径。
  4. 异步/同步分支 :根据 options.isAsync 决定返回 Promise 或同步结果,最终产出一个包含 codemaperrorsmodules(启用 CSS Modules 时)和 dependencies 的对象。

插件 cssVarsPlugin

js 复制代码
/**
 * 将 CSS 中的 v-bind() 表达式重写为 CSS 变量引用
 * @param opts
 * @returns
 */
export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  const { id, isProd } = opts!
  return {
    postcssPlugin: 'vue-sfc-vars',
    // 处理 CSS 声明
    Declaration(decl) {
      // rewrite CSS variables
      const value = decl.value
      // 检查值中是否包含 v-bind() 表达式
      if (vBindRE.test(value)) {
        vBindRE.lastIndex = 0
        let transformed = ''
        let lastIndex = 0
        let match
        // 使用 while 循环匹配所有 v-bind() 表达式
        while ((match = vBindRE.exec(value))) {
          const start = match.index + match[0].length
          const end = lexBinding(value, start)
          if (end !== null) {
            // 提取绑定表达式并标准化
            const variable = normalizeExpression(value.slice(start, end))
            transformed +=
              value.slice(lastIndex, match.index) +
              `var(--${genVarName(id, variable, isProd)})`
            lastIndex = end + 1
          }
        }
        // 生成的格式为 var(--变量名)
        decl.value = transformed + value.slice(lastIndex)
      }
    },
  }
}

插件 trimPlugin

js 复制代码
/**
 * 处理 CSS 根节点时执行一次性的操作。
 * 具体来说,这个函数的功能是标准化 CSS 规则和 @规则周围的空白,确保它们前后都只有一个换行符
 * @returns
 */
const trimPlugin: PluginCreator<{}> = () => {
  return {
    postcssPlugin: 'vue-sfc-trim',
    Once(root) {
      // 遍历 CSS 根节点下的所有节点
      root.walk(({ type, raws }) => {
        // 检查节点类型是否为 rule(规则,如 .class { ... })或 atrule(@规则,如 @media { ... })
        if (type === 'rule' || type === 'atrule') {
          // 设置为单个换行符 '\n'
          if (raws.before) raws.before = '\n'
          // 设置为单个换行符 '\n'
          if ('after' in raws && raws.after) raws.after = '\n'
        }
      })
    },
  }
}

示例 style 标签不带属性

全局样式。直接生效于整个应用,可能引发命名冲突。

js 复制代码
<style lang="less">
.title {
  color: blue;
  font-size: 24px;
}
</style>

示例 style 标签带有 scoped

局部样式 。通过添加data-v-xxx唯一属性实现样式隔离,仅作用于当前组件,默认不影响子组件。

js 复制代码
<style lang="less" scoped>
.title {
  color: red;
  font-size: 24px;
}
</style>
css 复制代码
.title[data-v-ee85f79b] { 
  color: blue; 
  font-size: 24px; 
} 

示例 style 标签 带有 scoped

js 复制代码
<style lang="less" scoped>
.cloud-index {
  color: red;

  .title {
    font-size: 24px;
  }
}
:global(.inner-dom) {
  color: palegreen;
}
</style>

vite 工具处理后

css 复制代码
.cloud-index {
  color: red;
}
.cloud-index .title {
  font-size: 24px;
}
:global(.inner-dom) {
  color: palegreen;
}

编译后

css 复制代码
.cloud-index[data-v-60451c3f] {
  color: red;
}

.cloud-index .title[data-v-60451c3f] {
  font-size: 24px;
}
.inner-dom {
  color: palegreen;
}

示例 style 标签带有 module

CSS Modules 则通过编译时改写类名,实现彻底的样式隔离。

编译后,Vue 会自动注入一个名为 $style 的计算属性。你需要通过 $style.className 的方式访问样式

js 复制代码
<template>
  <div>
    <p :class="$style.title">这里是StyleA</p>
  </div>
</template>
<style lang="less" module>
.title {
  color: blue;
  font-size: 24px;
}
</style>
css 复制代码
._title_1jekv_47 { 
  color: blue; 
  font-size: 24px; 
} 
js 复制代码
import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
	return _openBlock(), _createElementBlock("div", null, [_createElementVNode(
		"p",
		{ class: _normalizeClass(_ctx.$style.title) },
		"这里是StyleA",
		2
		/* CLASS */
	)]);
}

示例 style 标签具名 module

为模块指定一个自定义名称,组件会注入一个与此同名的对象。

js 复制代码
<p :class="cloudPlan.bg">这是cloud-plan模块的背景颜色</p>

<style lang="less" module="cloudPlan">
.bg {
  background-color: #f5f5f5;
}

示例 :deep() -- 深度选择器(穿透子组件)

强制让父组件的 scoped 样式穿透到子组件内部的元素上

  • 旧写法 ::v-deep/deep/>>> 已不推荐,Vue 3 中统一使用 :deep()
  • 不要在 :deep() 前后加空格,:deep (.class) 是错误的。
  • 仅限 scoped 中使用

【示例】

js 复制代码
// 父组件
<template>
  <StyleA />
</template>
 
<style lang="less" scoped>
:deep(.inner-dom) {
  color: red;
}
</style>

编译结果

js 复制代码
[data-v-60451c3f] .inner-dom {
  color: red;
}
js 复制代码
// 子组件
<p class="inner-dom" :class="[$style.title, $style['inner-dom']]" >这里是StyleA</p>

<style lang="less" module>
.inner-dom {
  color: blue;
}
.title {
  font-size: 24px;
}
</style>

如果上述 style 将 scoped 改为 module,编译结果如下。:deep()选择器不做处理。

示例 :slotted() -- 插槽内容选择器

在子组件中,为父组件传入的插槽内容设置样式。

  • :slotted() 只能选择顶层插槽内容 ,不能穿透插槽内容内部的更深嵌套(例如 :slotted(.parent .child) 无效)。
  • 旧语法 ::v-slotted 已废弃,使用 :slotted() 即可。
  • :slotted() 选择器只能在 scoped 样式中使用,而不能在 module 样式中使用
js 复制代码
<div>
    <StyleA>
      <template #default>
        <p>这里是StyleA 插槽</p>
      </template>
    </StyleA>
 </div>
 
<style lang="less" scoped>
:deep(.inner-dom) {
  color: red;
}
</style>
js 复制代码
// 子组件
<template>
  <div>
    <p class="inner-dom title">这里是StyleA</p>
    <slot></slot>
  </div>
</template>

<style lang="less" scoped>
.inner-dom {
  color: blue;
}
.title {
  font-size: 24px;
}
:slotted(p) {
  background-color: #e3e1e1;
}

编译效果 (父组件 style 设置 scoped)

编译结果 (父组件 style 设置module)

css 复制代码
.inner-dom[data-v-ee85f79b] {
  color: blue;
}
.title[data-v-ee85f79b] {
  font-size: 24px;
}
p[data-v-ee85f79b-s] {
  background-color: #e3e1e1;
}

插槽的作用域ID是如何插入的?

如果将 scoped 改为 module, 编译结果如下。:slotted 选择器不做处理。

示例 :global() -- 全局样式

scoped 样式块中,标记某条规则为全局,不对其添加 scoped 属性限制。

js 复制代码
<style lang="less" module>
:global(.inner-dom) {
  color: palegreen;
}

/* 也可以作用在多个选择器上 */
:global(.header, .footer) {
  background: gray;
}
</style>

节点上的 scoped 是什么时候渲染上去的?

当组件渲染时,Vue 根据渲染函数生成虚拟 DOM(VNode)。

相关推荐
肥羊zzz1 小时前
Vue2 vs Vue3 中 v-for 的 key 用法对比
前端·vue.js
前端那点事3 小时前
深度解析:Vue中computed的实现原理(易懂不晦涩)
前端·vue.js
前端那点事3 小时前
Vue中深克隆的3种实现方案(附详细注释+测试)
前端·vue.js
❆VE❆3 小时前
基于 contenteditable 实现变量插入富文本编辑器
前端·javascript·vue.js
ZC跨境爬虫4 小时前
前端实战复盘:从零完成Apple中国大陆官网UI第一阶段全量静态复刻
前端·css·ui·html
栀栀栀栀栀栀6 小时前
强迫症犯了(゚∀゚) 2026/4/26
前端·javascript·vue.js
Ruihong6 小时前
Vue 的 :deep/:global/:slotted 怎么转成 React ?一份对照指南?
vue.js·react.js·面试
用户78937733908537 小时前
Vue3 + Three.js 仓储数字孪生:按需渲染架构与五大核心功能复盘
vue.js·three.js
Cobyte7 小时前
9.响应式系统演进:effectScope 的作用与实现原理(Vue3.2)
前端·javascript·vue.js