从 CSS 到 Vue Scoped:一篇讲透前端样式的工程化认知

写这篇文章的主要原因是我在实习遇到这些问题:为什么 import './index.scss' 就能让样式生效?为什么组件里要写 scoped:deep() 到底在穿透什么?assets 里的资源会不会打包?Element Plus 的样式为什么能被我覆盖?Tailwind 和原子类是不是内联样式?

这篇文章尝试把这些问题归纳串成一条完整链路,从浏览器如何理解 CSS,到 Vite 如何处理样式,再到 Vue Scoped CSS 的实现原理,最后落到真实项目里的样式组织和组件库覆盖实践。

一、先建立一个核心模型

前端页面最终运行在浏览器里。浏览器真正认识的是:

txt 复制代码
HTML
CSS
JavaScript
静态资源,比如图片、字体、视频

而我们在 Vue 项目里写的这些东西:

txt 复制代码
.vue
.scss
TypeScript
别名路径 @/assets/xxx
组件库 import

都需要经过构建工具处理,最终变成浏览器能理解的 HTML、CSS、JS 和静态资源。

以 Vite + Vue 项目为例,入口通常是:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />(字符编码)
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />(视口view)
    <meta name="description" content="页面描述">(seo)
    <title>前端应用</title>(页面标题)
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

这里的 #app 是 Vue 应用的挂载点:

ts 复制代码
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

Vue 页面、组件、路由视图,绝大多数 DOM 都会渲染到:

txt 复制代码
body -> #app -> Vue 渲染出来的内容

但 CSS 不是渲染到 #app 里的。开发环境里,Vite 通常会把 CSS 注入到 <head> 里的 <style> 标签中;生产环境里,CSS 通常会被抽成 .css 文件,再通过 <link rel="stylesheet"> 放到 <head>

所以可以先记住这个模型:

txt 复制代码
Vue DOM 在 body 的 #app 里
CSS 样式表在 head 里
浏览器用 head 中的 CSS 选择器匹配整个 document 中的 DOM

CSS 不需要放在某个元素旁边才生效。只要样式表存在于当前 document 中,浏览器就会根据选择器去匹配页面上的 DOM。

二、CSS 和 SCSS 的区别

CSS 是浏览器原生支持的样式语言:

css 复制代码
.card {
  padding: 16px;
  color: #333;
}

.card .title {
  font-size: 16px;
  font-weight: 700;
}

SCSS 是 CSS 的增强版。浏览器不直接执行 SCSS,构建时会先把 SCSS 编译成 CSS。

scss 复制代码
$primary-color: #053dc8;
$text-color: #333;

.card {
  padding: 16px;
  color: $text-color;

  .title {
    font-size: 16px;
    font-weight: 700;

    &:hover {
      color: $primary-color;
    }
  }
}

编译后变成:

css 复制代码
.card {
  padding: 16px;
  color: #333;
}

.card .title {
  font-size: 16px;
  font-weight: 700;
}

.card .title:hover {
  color: #053dc8;
}

SCSS 的价值主要体现在工程化维护上:

txt 复制代码
变量
嵌套
mixin
函数
map
循环
拆分文件
批量生成工具类

尤其在中后台项目、组件库项目、设计系统项目里,SCSS 很常见。

SCSS 变量和 CSS 变量不是一回事

SCSS 变量是编译期变量:

scss 复制代码
$primary: #053dc8;

.title {
  color: $primary;
}

编译后:

css 复制代码
.title {
  color: #053dc8;
}

浏览器不知道 $primary 存在。

CSS 变量是运行时变量:

css 复制代码
:root {
  --primary: #053dc8;
}

.title {
  color: var(--primary);
}

浏览器知道 --primary,所以它可以被运行时修改:

js 复制代码
document.documentElement.style.setProperty('--primary', '#1677ff')

对比一下:

类型 阶段 浏览器是否知道 能否运行时修改
SCSS 变量 $primary 构建编译时
CSS 变量 --primary 浏览器运行时

所以现代组件库经常同时使用两者:

txt 复制代码
SCSS 负责生成 CSS
CSS 变量负责主题和运行时可配置能力

三、为什么 import './index.scss' 就能生效

在 Vue/Vite 项目里,经常会看到:

ts 复制代码
import '@/assets/styles/index.scss'

这不是 Vue 运行时的能力,而是 Vite 的构建能力。

这是一种"副作用导入":

ts 复制代码
import '@/assets/styles/index.scss'

意思不是从 SCSS 文件里拿一个变量,而是告诉构建工具:

txt 复制代码
请加载这份样式,让它参与当前应用构建

开发环境中,Vite 大致会做这些事:

txt 复制代码
解析 import 的 .scss 文件
调用 Sass 编译 SCSS 为 CSS
处理 @import、@use、url()、路径别名
把 CSS 包装成 JS 模块
浏览器执行模块后创建 style 标签
把 style 标签插入 document.head

概念上页面会变成:

html 复制代码
<head>
  <style data-vite-dev-id="/src/assets/styles/index.scss">
    .text-primary {
      color: var(--text-color-primary);
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- Vue 页面 DOM -->
  </div>
</body>

生产构建时,Vite 通常会把 CSS 抽成文件:

txt 复制代码
dist/assets/index-xxxx.css

然后在 HTML 中通过 <link> 引入:

html 复制代码
<link rel="stylesheet" href="./assets/index-xxxx.css">

所以结论是:

txt 复制代码
普通 .scss import 是 Vite 处理的
.vue 里的 style 是 @vitejs/plugin-vue + @vue/compiler-sfc 处理的
浏览器最终只看到 CSS

四、src/assetspublic 的区别

很多人会误以为 assets 是静态资源目录,所以不会打包。这个理解不准确。

在 Vite 项目里:

txt 复制代码
src/assets 里的资源会被构建工具处理
public 里的资源会被原样复制

比如:

txt 复制代码
src/assets/images/logo.png

如果你这样用:

vue 复制代码
<img src="@/assets/images/logo.png" />

或者:

scss 复制代码
.logo {
  background-image: url('@/assets/images/logo.png');
}

Vite 会处理它:

txt 复制代码
解析路径
检查文件是否存在
计算 hash
复制到 dist/assets
替换引用路径
小资源可能内联成 base64

最终可能变成:

txt 复制代码
dist/assets/logo-Dk39a8f2.png

public 目录里的资源通常原样复制:

txt 复制代码
public/favicon.ico -> dist/favicon.ico
public/vendor/editor/editor.js -> dist/vendor/editor/editor.js

引用时一般用绝对路径:

html 复制代码
<link rel="icon" href="/favicon.ico">

总结:

目录 是否经过构建处理 是否 hash 引用方式 适合放什么
src/assets 通常是 import@/assets/...、CSS url() 项目图片、字体、SCSS、参与构建的资源
public 否,原样复制 /xxx favicon、第三方静态包、固定路径资源

src/assets/styles 只是一个目录约定,没有框架魔法。真正让它生效的是你在入口文件里导入了它:

ts 复制代码
import '@/assets/styles/index.scss'

五、全局样式和组件样式应该怎么分

前端项目里通常同时存在两类样式:

txt 复制代码
全局样式
组件局部样式

全局样式一般放在:

txt 复制代码
src/assets/styles/index.scss
src/assets/styles/reset.scss
src/assets/styles/atom.scss
src/assets/styles/common.scss

它适合放:

txt 复制代码
reset
normalize
CSS 变量
主题变量
字体定义
原子类
公共布局类
组件库全局覆盖

比如:

scss 复制代码
:root {
  --color-primary: #053dc8;
  --text-color-primary: #333;
}

.text-primary {
  color: var(--text-color-primary);
}

.mt-12 {
  margin-top: 12px;
}

.flex-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

组件局部样式一般写在 .vue 文件里:

vue 复制代码
<template>
  <div class="order-page">
    <div class="order-page__title">订单列表</div>
  </div>
</template>

<style scoped lang="scss">
.order-page {
  padding: 24px;

  &__title {
    font-size: 20px;
    font-weight: 700;
  }
}
</style>

一个比较稳的规则是:

txt 复制代码
所有页面都要用的样式,放全局
只有当前组件需要的样式,放组件 scoped style

不要轻易在页面组件里写无 scoped 的:

vue 复制代码
<style>
.title {
  color: red;
}
</style>

因为这会变成全局 .title,很容易污染其他页面。

六、原子类、Tailwind 和"内联样式"的关系

很多项目会有类似这样的类:

html 复制代码
<div class="text-primary mt-12 flex-between"></div>

它们看起来很像 Tailwind,但不一定是 Tailwind。很多公司会用 SCSS 自己生成一套原子类。

例如:

scss 复制代码
$space-list: 0, 4, 8, 12, 16, 20, 24;

@each $space in $space-list {
  .mt-#{$space} {
    margin-top: #{$space}px;
  }

  .mb-#{$space} {
    margin-bottom: #{$space}px;
  }
}

会生成:

css 复制代码
.mt-0 {
  margin-top: 0px;
}

.mt-12 {
  margin-top: 12px;
}

.mb-12 {
  margin-bottom: 12px;
}

文字类也可以这样生成:

scss 复制代码
$fontSizeList: 12, 14, 16, 18, none;
$fontColorList: primary, sub, tip, theme;

$colorMap: (
  primary: var(--text-color-primary),
  sub: var(--text-color-regular),
  tip: var(--text-color-secondary),
  theme: var(--color-primary),
);

@each $fontSize in $fontSizeList {
  $baseName: text;

  @if $fontSize != none {
    $baseName: text + '-' + $fontSize;

    .#{$baseName} {
      font-size: #{$fontSize}px;
      line-height: 1.5;
    }
  } @else {
    .text {
      line-height: 1.5;
    }
  }

  @each $fontColor in $fontColorList {
    .#{$baseName}-#{$fontColor} {
      @extend .#{$baseName};
      color: map-get($colorMap, $fontColor);
    }
  }
}

最终可以使用:

html 复制代码
<div class="text-16-primary">16px 主文字色</div>
<div class="text-14-tip">14px 提示色</div>
<div class="text-theme">主题色</div>

Tailwind 的写法则更像:

html 复制代码
<div class="mt-3 flex items-center justify-between text-base font-bold text-gray-900"></div>

它和原子类的共同点是:

txt 复制代码
样式声明靠近 markup
用小粒度 class 组合 UI
减少为样式起名字的成本

所以有人会说 Tailwind/原子类很像内联样式。这个直觉有道理。

但它和传统 style="" 有几个关键区别:

对比项 inline style 原子类 / Tailwind
写在标签上 是,写在 class 上
样式来源 style="" CSS class
是否受设计 token 约束 通常不受 通常受
hover/focus 不方便 支持
响应式 不方便 支持
优先级 很高 普通 class
主题化 麻烦 更自然

更准确地说,原子类和 Tailwind 是:

txt 复制代码
受设计系统约束的、class 形式的、接近内联体验的样式写法

它不是传统 inline style,但确实把一部分视觉声明放回了模板里。

七、Vue Scoped CSS 是为了解决什么问题

CSS 默认是全局的。

如果 A 组件写:

vue 复制代码
<style>
.title {
  color: red;
}
</style>

B 组件也有:

html 复制代码
<div class="title">B 标题</div>

那么 B 的标题也可能变红。

Vue 提供了 scoped

vue 复制代码
<style scoped>
.title {
  color: red;
}
</style>

它的目标是:

txt 复制代码
让当前组件的样式只作用于当前组件

编译后,Vue 会为组件生成一个作用域 ID:

txt 复制代码
data-v-abc123

模板 DOM 会变成:

html 复制代码
<div class="title" data-v-abc123>标题</div>

CSS 会变成:

css 复制代码
.title[data-v-abc123] {
  color: red;
}

这样它就只匹配当前组件里的 .title

另一个组件的 DOM 可能是:

html 复制代码
<div class="title" data-v-def456>标题</div>

它没有 data-v-abc123,所以不会被影响。

所以 scoped 的核心不是控制 CSS 是否加载,而是控制 CSS 加载后能匹配哪些 DOM。

八、Vue scoped hash 是怎么实现的

.vue 文件不是浏览器原生文件,它会经过编译。

在 Vite + Vue 项目里,主要由:

txt 复制代码
@vitejs/plugin-vue
@vue/compiler-sfc

处理。

一个 Vue SFC:

vue 复制代码
<template>
  <div class="card">
    <h2 class="title">标题</h2>
  </div>
</template>

<style scoped>
.title {
  color: red;
}
</style>

编译过程大概是:

txt 复制代码
解析 .vue 文件
拆出 template / script / style
发现 style 上有 scoped
为组件生成 scopeId,比如 data-v-7ba5bd90
编译 CSS:.title -> .title[data-v-7ba5bd90]
编译 template/render:让组件渲染出的 DOM 带 data-v-7ba5bd90

概念上的编译结果:

css 复制代码
.title[data-v-7ba5bd90] {
  color: red;
}

DOM:

html 复制代码
<div class="card" data-v-7ba5bd90>
  <h2 class="title" data-v-7ba5bd90>标题</h2>
</div>

在 Vue 3 里,组件对象上会携带类似:

js 复制代码
__scopeId: 'data-v-7ba5bd90'

Vue runtime 渲染真实 DOM 时,会把这个 scopeId 设置到元素上。

注意,这个 data-v-xxx 不是 source map。它是样式作用域标识。

全局样式不会有这个 hash:

scss 复制代码
// src/assets/styles/index.scss
.text-primary {
  color: #333;
}

会编译成:

css 复制代码
.text-primary {
  color: #333;
}

因为它不是某个 Vue 文件的 <style scoped>

九、为什么路由懒加载仍然可能样式污染

我当时也疑惑:Vue 页面不是按需加载吗?为什么 A 页面的 CSS 会影响 B 页面?

因为:

txt 复制代码
按需加载控制的是资源什么时候进入页面
CSS 一旦进入页面,如果不是 scoped,就是全局规则
SPA 页面切换不会刷新浏览器
旧页面的全局 CSS 通常不会因为组件卸载就自动删除

假设 A 页面:

vue 复制代码
<style>
.title {
  color: red;
}
</style>

B 页面:

vue 复制代码
<template>
  <div class="title">B 标题</div>
</template>

如果用户先访问 A,再访问 B,A 的 CSS 可能已经进入页面:

css 复制代码
.title {
  color: red;
}

B 的 .title 就可能被影响。

更麻烦的是,这类问题可能和访问顺序有关:

txt 复制代码
直接刷新进入 B:正常
先进入 A 再进入 B:异常

这就是未 scoped 的页面级样式污染。

scoped 解决的是这个问题:

css 复制代码
.title[data-v-a111] {
  color: red;
}

即使 A 的 CSS 留着,它也匹配不到 B 的 DOM。

十、页面切换后 CSS 会不会越堆越多

某种程度上,会。

在 SPA 中,访问过的异步页面 CSS 通常会被加载进页面,并且不会因为路由离开就立刻卸载。

但这通常不是严重问题:

txt 复制代码
同一个 CSS chunk 通常只加载一次
CSS 体积一般比 JS 和图片小
浏览器会缓存资源
生产构建可能抽公共 CSS
页面数量有限

构建工具通常不积极卸载 CSS,原因包括:

txt 复制代码
CSS 可能被多个页面共享
组件可能 keep-alive
弹窗可能 teleport 到 body
删除 CSS 再加载可能导致闪烁
生产环境 CSS 可能被合并
判断某条 CSS 是否还被使用成本很高

所以工程上的常规策略不是"离开页面删除 CSS",而是:

txt 复制代码
局部样式 scoped
公共样式全局
组件库覆盖要有明确范围
减少低质量全局选择器

十一、父子组件中的 scoped 规则

父组件的 scoped 样式默认能影响:

txt 复制代码
父组件自己的 DOM
子组件的根节点

但不能深入影响:

txt 复制代码
子组件内部深层 DOM
组件库内部结构

举个例子。

父组件:

vue 复制代码
<template>
  <div class="parent">
    <Child />
  </div>
</template>

<style scoped>
.child-title {
  color: red;
}
</style>

子组件:

vue 复制代码
<template>
  <div class="child-root">
    <div class="child-title">子组件标题</div>
  </div>
</template>

父组件 CSS 会被编译成:

css 复制代码
.child-title[data-v-parent] {
  color: red;
}

但子组件内部 DOM 是:

html 复制代码
<div class="child-title" data-v-child>子组件标题</div>

没有 data-v-parent,所以父组件样式匹配不到。

但子组件根节点比较特殊。父组件给子组件加 class:

vue 复制代码
<Child class="child-from-parent" />

渲染出来的子组件根节点可能是:

html 复制代码
<div class="child-root child-from-parent" data-v-parent data-v-child>
  ...
</div>

它同时带父组件和子组件的 scopeId,所以父组件 scoped 样式可以控制子组件根节点布局。

这是合理的,因为父组件经常需要控制子组件外部布局:

txt 复制代码
margin
width
display
align-self

但父组件默认不应该随意修改子组件内部结构。

十二、:deep() 到底是什么

:deep() 的本质是:

txt 复制代码
在 scoped CSS 中,让 deep 内部的选择器不追加当前组件的 data-v hash

它不是 props,不是事件,也不是数据传递。它只是影响 CSS 选择器的编译结果。

普通 scoped:

scss 复制代码
.parent .child-title {
  color: red;
}

可能编译成:

css 复制代码
.parent[data-v-parent] .child-title[data-v-parent] {
  color: red;
}

要求 .child-title 也必须是父组件自己的 DOM。

使用 :deep()

scss 复制代码
.parent {
  :deep(.child-title) {
    color: red;
  }
}

会编译成类似:

css 复制代码
.parent[data-v-parent] .child-title {
  color: red;
}

注意:

txt 复制代码
.parent 有 data-v-parent
.child-title 没有 data-v-parent

所以它可以匹配子组件内部的:

html 复制代码
<div class="child-title" data-v-child>子组件标题</div>

这就是所谓"穿透"。

:deep() 常见场景

覆盖组件库内部 DOM:

vue 复制代码
<template>
  <div class="search-page">
    <el-input v-model="keyword" />
  </div>
</template>

<style scoped lang="scss">
.search-page {
  :deep(.el-input__wrapper) {
    border-radius: 8px;
  }
}
</style>
css 复制代码
推荐总是带一个父级范围:

```scss
.search-page {
  :deep(.qd-input__wrapper) {
    border-radius: 8px;
  }
}

少写这种裸穿透:

scss 复制代码
:deep(.title) {
  color: red;
}

因为它会影响当前组件下面所有深层 .title,包括多个子组件、组件库内部、插槽内容。

十三、Element Plus 的样式在哪里

组件库通常自带样式。

以 Element Plus 为例,它的样式通常在:

txt 复制代码
node_modules/element-plus/theme-chalk

源码 SCSS:

txt 复制代码
node_modules/element-plus/theme-chalk/src/index.scss
node_modules/element-plus/theme-chalk/src/button.scss
node_modules/element-plus/theme-chalk/src/input.scss
node_modules/element-plus/theme-chalk/src/table.scss

编译后的 CSS:

txt 复制代码
node_modules/element-plus/theme-chalk/el-button.css
node_modules/element-plus/theme-chalk/el-input.css
node_modules/element-plus/theme-chalk/el-table.css

项目里可能通过全局样式入口引入:

scss 复制代码
@use 'element-plus/theme-chalk/src/index.scss' as *;

或者在入口 TS 中引入:

ts 复制代码
import 'element-plus/dist/index.css'

组件库会提供大量类:

css 复制代码
.el-button {}
.el-button--primary {}
.el-input {}
.el-input__wrapper {}
.el-dialog {}

如果项目改过命名空间,可能不是 el,而是其他前缀,例如:

css 复制代码
.qd-button {}
.qd-button--primary {}
.qd-input {}
.qd-input__wrapper {}

十四、为什么自己的样式能覆盖组件库

因为最终浏览器只看 CSS。

它不关心这条规则来自:

txt 复制代码
Element Plus
业务组件库
assets/styles
某个 Vue 文件

浏览器只根据 CSS 层叠规则决定谁生效:

txt 复制代码
选择器优先级
加载顺序
!important

比如组件库先定义:

css 复制代码
.el-button {
  border-radius: 4px;
}

你后面写:

css 复制代码
.el-button {
  border-radius: 8px;
}

选择器优先级一样,后出现的规则覆盖先出现的规则。

如果组件库选择器更具体:

css 复制代码
.el-button.el-button--primary {
  background: blue;
}

而你只写:

css 复制代码
.el-button--primary {
  background: red;
}

可能覆盖不了,因为你的选择器优先级更低。

你需要写得更具体:

css 复制代码
.el-button.el-button--primary {
  background: red;
}

或者在页面范围下覆盖:

scss 复制代码
.order-page {
  :deep(.el-button.el-button--primary) {
    background: red;
  }
}

组件库覆盖一般有三种方式。

1. 覆盖 CSS 变量

现代组件库大量使用 CSS 变量:

css 复制代码
:root {
  --el-color-primary: #409eff;
}

.el-button--primary {
  background-color: var(--el-color-primary);
}

你可以改变量:

css 复制代码
:root {
  --el-color-primary: #1677ff;
}

这种方式最干净,适合主题级调整。

2. 全局覆盖组件库 class

适合统一规范:

scss 复制代码
.el-button {
  border-radius: 6px;
}

但要谨慎,因为它影响全项目。

3. 局部 deep 覆盖

适合某个页面特殊样式:

vue 复制代码
<style scoped lang="scss">
.order-page {
  :deep(.el-input__wrapper) {
    height: 36px;
  }
}
</style>

十五、常见样式不生效原因

1. CSS 没有被导入

比如你写了:

txt 复制代码
src/assets/styles/custom.scss

但没有任何地方 import 它:

ts 复制代码
import '@/assets/styles/custom.scss'

那它不会自动生效。

2. scoped 导致匹配不到子组件内部 DOM

错误:

vue 复制代码
<style scoped>
.el-input__wrapper {
  border-radius: 8px;
}
</style>

可能编译成:

css 复制代码
.el-input__wrapper[data-v-xxx] {
  border-radius: 8px;
}

组件库内部 DOM 没有 data-v-xxx,所以不生效。

正确:

scss 复制代码
:deep(.el-input__wrapper) {
  border-radius: 8px;
}

3. 选择器优先级不够

组件库:

css 复制代码
.el-form-item.is-error .el-input__wrapper {
  box-shadow: 0 0 0 1px red inset;
}

你写:

css 复制代码
.el-input__wrapper {
  box-shadow: none;
}

可能压不过。

4. 加载顺序靠前

如果你的样式先加载,组件库样式后加载,同优先级下会被组件库覆盖。

5. 被 CSS 变量间接控制

你以为要改:

css 复制代码
.el-button--primary {
  background: red;
}

但实际组件使用的是:

css 复制代码
background: var(--el-button-bg-color);

这时可能应该改变量。

6. 对方用了 !important

如果已有规则是:

css 复制代码
.some-class {
  color: blue !important;
}

普通规则覆盖不了。

不要滥用 !important,但遇到老项目或第三方库时要知道它的影响。

十六、CSS 优先级简单复习

CSS 决定谁生效,大概看:

txt 复制代码
!important
内联 style
ID 选择器
class / 属性 / 伪类
标签 / 伪元素
出现顺序

例如:

css 复制代码
.button {
  color: red;
}

.page .button {
  color: blue;
}

第二个更具体,所以 blue 生效。

同优先级时:

css 复制代码
.button {
  color: red;
}

.button {
  color: blue;
}

后面的 blue 生效。

Vue scoped 本质上会增加属性选择器:

css 复制代码
.title[data-v-xxx] {}

它比普通 .title 优先级略高,因为多了一个属性选择器。

这也是 scoped 样式有时比普通全局类更容易覆盖的原因之一。

十七、推荐的样式组织实践

1. 全局样式只放真正全局的东西

适合全局:

txt 复制代码
reset
主题变量
字体
原子类
公共布局
组件库统一覆盖

不适合全局:

txt 复制代码
.title
.content
.card
.item
.header

这些名字太通用,很容易污染。

2. 页面样式默认 scoped

推荐:

vue 复制代码
<style scoped lang="scss">
.invoice-page {
  &__header {}
  &__title {}
  &__content {}
}
</style>

3. 使用页面根类做范围限制

不要裸写:

scss 复制代码
:deep(.el-input__wrapper) {
  height: 40px;
}

更推荐:

scss 复制代码
.invoice-page {
  :deep(.el-input__wrapper) {
    height: 40px;
  }
}

4. 优先用变量改主题

主题色、文字色、边框色,优先考虑 CSS 变量:

css 复制代码
:root {
  --el-color-primary: #1677ff;
}

而不是到处覆盖内部 class。

5. 少用 !important

!important 很容易让后续维护变成军备竞赛:

css 复制代码
.a {
  color: red !important;
}

.b {
  color: blue !important;
}

能靠加载顺序、选择器、变量解决,就不要上来 !important

6. 覆盖组件库前先看真实 DOM

组件库的模板不是你写的标签本身。

比如:

vue 复制代码
<el-input />

真实 DOM 可能是:

html 复制代码
<div class="el-input">
  <div class="el-input__wrapper">
    <input class="el-input__inner" />
  </div>
</div>

十八、一句话串起全文

前端样式工程的核心不是"写在哪里都行",而是要知道样式最终如何进入浏览器、如何匹配 DOM、如何被编译工具改写、如何被组件库和框架共同影响。

你可以用这条链路理解大多数问题:

txt 复制代码
源码中的 .vue / .scss
  -> Vite / Vue compiler 编译
  -> CSS 被注入 head 或抽成 css 文件
  -> Vue DOM 渲染到 #app
  -> 浏览器根据 CSS 选择器匹配 DOM
  -> scoped 通过 data-v-hash 限制匹配范围
  -> :deep() 改变 scoped 选择器编译结果,让它穿透子组件内部
  -> 组件库样式通过 CSS 层叠规则被覆盖

掌握这条链路后,再遇到这些问题时就不会只靠猜:

txt 复制代码
为什么我的样式不生效?
为什么组件库样式被覆盖了?
为什么 A 页面影响了 B 页面?
为什么 scoped 里改不了 el-input?
为什么 :deep() 有时有效有时无效?
为什么 assets 里的资源构建后变了名字?

CSS 不是"写几行颜色和间距"那么简单。到了工程规模,它其实是在和构建工具、组件模型、浏览器层叠规则、设计系统一起工作。理解这些机制,才是真正能稳住复杂前端项目样式的关键。

相关推荐
扫一屋3 小时前
Element Plus 源码级深度定制:不 Fork、不改包,用 Vite 插件实现组件行为运行时改造
前端
BigTopOne4 小时前
KOOM 学习计划
前端
自进化Agent智能体4 小时前
Hermes 技能系统详解——安装、使用与浏览
前端
JieE2125 小时前
前端不等人:用 Mock 接口工程让 React 应用独立起飞
前端·react.js·面试
用户355856959025 小时前
HTTPS到底会不会拖慢网站速度?TLS握手原理与性能优化实战
前端
xywww1685 小时前
真实后台页实测:Opus 5 看图写前端的可用边界在哪
linux·服务器·前端·数据库·人工智能·gpt
小小尚@5 小时前
AE脚本-AE Actions v1.1.8 操作动作记录器
开发语言·前端·javascript·jupyter·postman
大家的林语冰6 小时前
👍 超越 ESLint,Oxc 优先采用 TypeScript 7,Rust 和 Go 梦幻联动!
前端·javascript·typescript
樊小肆7 小时前
# 你还在等DeepSeek官方 agent Harness‌? 来试试 DeepSeeker-Code吧
前端·人工智能·后端