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

cssPlugin插件

vite-pugin-vue 插件

doCompileStyle
doCompileStyle 函数负责编译 Vue 单文件组件中的样式块,其核心流程为:
- 预处理 :根据
preprocessLang(如 scss、less)调用对应预处理器将源码转换为 CSS,并收集预处理阶段的依赖与错误。 - PostCSS 处理 :配置并执行 PostCSS 插件链(包括内置的
cssVarsPlugin、trimPlugin、scopedPlugin以及可选的postcssModules),对 CSS 进行变量注入、代码修剪、作用域化及 CSS Modules 转换。 - 结果生成 :获取 PostCSS 处理后的 CSS 代码和 Source Map,并遍历处理消息中的
dependency类型,收集所有依赖文件路径。 - 异步/同步分支 :根据
options.isAsync决定返回 Promise 或同步结果,最终产出一个包含code、map、errors、modules(启用 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)。


