样式系统最佳实践
介绍
本文档汇总了 RuoYi-Plus-UniApp 前端项目样式系统的最佳实践,涵盖 SCSS 架构、UnoCSS 原子化、主题系统、性能优化、代码规范等方面。这些实践基于项目实际架构和真实开发经验总结而来,旨在帮助开发者编写高质量、可维护、高性能的样式代码。
核心内容:
- 架构设计最佳实践 - SCSS 分层架构、模块化组织、命名规范
- 原子化 CSS 实践 - UnoCSS 使用规范、预设配置、自定义扩展
- 主题系统实践 - 暗黑模式适配、CSS 变量使用、动态切换
- 响应式设计实践 - 移动优先、断点策略、Flex/Grid 布局
- 性能优化实践 - CSS 性能优化、动画性能、打包优化
- 代码规范实践 - 命名约定、代码组织、团队协作
- 常见问题解决 - 实际开发中的典型问题和解决方案
适用场景:
本文档适用于以下开发场景:
- 新项目启动 - 参考架构设计和规范建立
- 代码重构 - 优化现有样式代码结构
- 主题开发 - 实现主题切换和暗黑模式
- 性能优化 - 解决样式性能问题
- 团队协作 - 统一样式开发规范
- 问题排查 - 解决常见样式问题
架构设计最佳实践
1. SCSS 分层架构
项目采用 ITCSS (Inverted Triangle CSS) 分层架构,按照特异性从低到高组织样式代码。
目录结构:
src/assets/styles/
├── abstracts/ # 抽象层(变量、混合器)
│ ├── _variables.scss # 全局变量
│ └── _mixins.scss # 混合器函数
├── themes/ # 主题层
│ ├── _light.scss # 亮色主题
│ └── _dark.scss # 暗黑主题
├── base/ # 基础层
│ ├── _reset.scss # 样式重置
│ └── _typography.scss # 排版样式
├── layout/ # 布局层
│ └── _layout.scss # 布局样式
├── components/ # 组件层
│ ├── _buttons.scss # 按钮组件
│ └── _animations.scss # 动画组件
└── vendors/ # 第三方覆盖
└── _element-plus.scss导入顺序(main.scss):
/* 1. 外部库 - 最先导入 */
@use 'animate.css';
@use 'element-plus/dist/index.css';
/* 2. 抽象层 - 提供变量和工具 */
@use './abstracts/variables' as *;
@use './abstracts/mixins' as *;
/* 3. 主题系统 - 定义主题变量 */
@use './themes/light';
@use './themes/dark';
/* 4. 基础样式 - 重置和基础样式 */
@use './base/reset';
@use './base/typography';
/* 5. 布局层 - 页面布局 */
@use './layout/layout';
/* 6. 组件样式 - 可复用组件 */
@use './components/buttons';
@use './components/animations';
/* 7. 第三方库覆盖 - 最后导入 */
@use './vendors/element-plus';
/* 8. 主题切换动画 */
@use './theme-animation';最佳实践要点:
- 严格按顺序导入 - 确保样式优先级正确
- 使用 @use 代替 @import - 避免命名冲突和重复导入
- 抽象层使用别名 -
as *允许直接使用变量和混合器 - 组件样式独立文件 - 便于维护和按需加载
- 第三方覆盖放最后 - 确保能覆盖第三方库样式
反例 - 错误的导入顺序:
// ❌ 错误示例
@use './components/buttons'; // 组件在变量之前
@use './abstracts/variables'; // 无法使用变量
@use './vendors/element-plus'; // 第三方在前
@use './base/reset'; // 重置在后,可能被覆盖2. 模块化组织原则
单一职责原则:
每个 SCSS 文件只负责一个功能模块。
// ✅ 正确 - _buttons.scss 只包含按钮相关样式
@mixin colorBtn($color) { /* ... */ }
.pan-btn { /* ... */ }
.custom-button { /* ... */ }
// ❌ 错误 - 混杂多种功能
@mixin colorBtn($color) { /* ... */ }
.data-table { /* ... */ } // 表格样式不应该在按钮文件中
.modal-dialog { /* ... */ } // 对话框样式不应该在按钮文件中命名空间隔离:
使用 @use 模块系统防止命名冲突。
// _colors.scss
$primary: #409eff;
$success: #67c23a;
// _sizes.scss
$primary: 16px; // 与 colors 模块的 primary 不冲突
// main.scss
@use './colors' as c;
@use './sizes' as s;
.button {
color: c.$primary; // #409eff
font-size: s.$primary; // 16px
}文件大小控制:
单个 SCSS 文件不超过 300 行,超过则拆分。
// ❌ 单个文件过大
// _components.scss (800行)
.button { /* 100行 */ }
.input { /* 150行 */ }
.select { /* 200行 */ }
.dialog { /* 350行 */ }
// ✅ 按组件拆分
// _button.scss (100行)
// _input.scss (150行)
// _select.scss (200行)
// _dialog.scss (350行)3. 变量命名规范
SCSS 变量命名:
使用 kebab-case,按用途分类。
// ✅ 正确的变量命名
// 颜色变量
$primary-color: #409eff;
$success-color: #67c23a;
$danger-color: #f56c6c;
// 尺寸变量
$base-sidebar-width: 240px;
$header-height: 50px;
// 断点变量
$breakpoint-sm: 768px;
$breakpoint-md: 992px;
$breakpoint-lg: 1200px;
// ❌ 错误的命名
$blue: #409eff; // 语义不明确
$sidebarWidth: 240px; // 使用了驼峰命名
$BP_SM: 768px; // 全大写不符合规范CSS 变量命名:
使用双连字符,体现层级关系。
:root {
// ✅ 正确的 CSS 变量命名
--color-primary: #409eff;
--color-text-primary: #303133;
--color-text-regular: #606266;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--duration-fast: 0.15s;
--duration-normal: 0.3s;
--duration-slow: 0.6s;
// ❌ 错误的命名
--primary: #409eff; // 缺少分类前缀
--text_color: #303133; // 使用下划线
--Spacing-MD: 16px; // 使用大写
}Element Plus 变量覆盖:
使用 --el- 前缀覆盖 Element Plus 变量。
:root {
// ✅ 覆盖 Element Plus 组件高度
--el-component-size: 32px !important;
// ✅ 覆盖 Element Plus 圆角
--el-border-radius-base: 6px !important;
--el-border-radius-small: 8px !important;
// ✅ 覆盖 Element Plus 颜色
--el-color-primary: #409eff !important;
}4. 混合器设计原则
参数化设计:
混合器应该接收参数,提供灵活性。
// ✅ 正确 - 参数化混合器
@mixin button-variant($bg-color, $text-color, $hover-scale: 1.05) {
background: $bg-color;
color: $text-color;
transition: all 0.3s ease;
&:hover {
transform: scale($hover-scale);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
// 使用
.btn-primary {
@include button-variant(#409eff, #fff);
}
.btn-success {
@include button-variant(#67c23a, #fff, 1.1);
}
// ❌ 错误 - 硬编码值
@mixin button-blue {
background: #409eff; // 只能用于蓝色按钮
color: #fff;
}默认参数:
为参数提供合理的默认值。
// ✅ 提供默认参数
@mixin card-style(
$padding: 16px,
$radius: 8px,
$shadow: 0 2px 8px rgba(0, 0, 0, 0.1)
) {
padding: $padding;
border-radius: $radius;
box-shadow: $shadow;
}
// 使用时可以只传部分参数
.card-small {
@include card-style($padding: 8px); // 其他参数使用默认值
}
.card-large {
@include card-style(24px, 12px); // 覆盖前两个参数
}避免过度抽象:
不要为了复用而过度抽象。
// ❌ 过度抽象 - 只用一次的样式
@mixin specific-header-style {
height: 60px;
background: linear-gradient(90deg, #409eff, #67c23a);
border-bottom: 2px solid #e4e7ed;
}
.page-header {
@include specific-header-style; // 只在一个地方使用
}
// ✅ 直接编写
.page-header {
height: 60px;
background: linear-gradient(90deg, #409eff, #67c23a);
border-bottom: 2px solid #e4e7ed;
}UnoCSS 原子化实践
1. 优先使用 UnoCSS
在 Vue 模板中,优先使用 UnoCSS 原子类而不是自定义样式。
优先级规则:
UnoCSS 原子类 > SCSS 工具类 > 自定义样式示例对比:
<!-- ✅ 最佳 - 使用 UnoCSS -->
<template>
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
<span class="text-lg font-semibold text-gray-800">标题</span>
<button class="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
操作
</button>
</div>
</template>
<!-- ⚠️ 次选 - 使用 SCSS 工具类 -->
<template>
<div class="card-container">
<span class="title">标题</span>
<button class="btn-primary">操作</button>
</div>
</template>
<!-- ❌ 避免 - 完全自定义 -->
<template>
<div class="card-container">
<span class="title">标题</span>
<button class="btn-primary">操作</button>
</div>
</template>2. UnoCSS 预设使用
项目配置了 Wind 预设和 Icons 预设。
Wind 预设常用类:
<template>
<!-- 布局 -->
<div class="flex flex-col items-center justify-center">
<!-- Flexbox 布局 -->
</div>
<div class="grid grid-cols-3 gap-4">
<!-- Grid 布局 -->
</div>
<!-- 间距 -->
<div class="p-4 m-2"> <!-- padding: 16px, margin: 8px -->
<div class="px-6 py-3"> <!-- padding: 24px 12px -->
</div>
</div>
<!-- 尺寸 -->
<div class="w-full h-screen"> <!-- width: 100%, height: 100vh -->
<div class="w-64 h-32"> <!-- width: 256px, height: 128px -->
</div>
</div>
<!-- 文字 -->
<p class="text-base font-normal text-gray-700">
普通文本
</p>
<h1 class="text-3xl font-bold text-blue-600">
标题文本
</h1>
<!-- 背景和边框 -->
<div class="bg-white border border-gray-200 rounded-lg">
卡片内容
</div>
<!-- 阴影 -->
<div class="shadow-sm hover:shadow-lg transition-shadow">
悬浮卡片
</div>
</template>Icons 预设使用:
<template>
<!-- 使用 Iconify 图标集 -->
<div class="i-carbon-user text-2xl" /> <!-- Carbon 图标 -->
<div class="i-mdi-home text-3xl text-blue-500" /> <!-- MDI 图标 -->
<div class="i-heroicons-solid-search" /> <!-- Heroicons 图标 -->
</template>3. 自定义配置扩展
主题色扩展:
项目扩展了 Element Plus 主题色。
// uno.config.ts
export default defineConfig({
theme: {
colors: {
primary: '#409eff',
success: '#67c23a',
warning: '#e6a23c',
danger: '#f56c6c',
info: '#909399',
}
}
})使用自定义主题色:
<template>
<!-- 文字颜色 -->
<span class="text-primary">主要文本</span>
<span class="text-success">成功文本</span>
<span class="text-danger">危险文本</span>
<!-- 背景颜色 -->
<div class="bg-primary text-white p-4">主要背景</div>
<div class="bg-success text-white p-4">成功背景</div>
<!-- 边框颜色 -->
<div class="border border-primary">主要边框</div>
</template>快捷方式(Shortcuts):
定义常用样式组合。
// uno.config.ts
export default defineConfig({
shortcuts: {
'btn': 'px-4 py-2 rounded cursor-pointer transition-colors',
'btn-primary': 'btn bg-primary text-white hover:bg-blue-600',
'btn-success': 'btn bg-success text-white hover:bg-green-600',
'card': 'p-4 bg-white rounded-lg shadow-sm',
'input-base': 'px-3 py-2 border border-gray-300 rounded focus:border-primary',
}
})使用快捷方式:
<template>
<!-- 按钮 -->
<button class="btn-primary">确认</button>
<button class="btn-success">提交</button>
<!-- 卡片 -->
<div class="card">
<h3 class="text-lg font-semibold mb-2">卡片标题</h3>
<p class="text-gray-600">卡片内容</p>
</div>
<!-- 输入框 -->
<input class="input-base w-full" placeholder="请输入内容">
</template>4. UnoCSS 性能优化
类名提取规则:
UnoCSS 只会提取实际使用的类名。
<!-- ✅ 静态类名 - 会被提取 -->
<div class="flex items-center p-4 bg-white">
静态内容
</div>
<!-- ✅ 动态类名 - 完整类名也会被提取 -->
<div :class="isActive ? 'bg-blue-500' : 'bg-gray-200'">
动态背景
</div>
<!-- ❌ 字符串拼接 - 不会被提取 -->
<div :class="'bg-' + color + '-500'">
<!-- 如果 color='blue',类名 'bg-blue-500' 不会生成 -->
</div>
<!-- ✅ 解决方案 - 使用完整类名 -->
<div :class="{
'bg-blue-500': color === 'blue',
'bg-red-500': color === 'red',
'bg-green-500': color === 'green'
}">
正确的动态类名
</div>Safelist 配置:
需要动态生成的类名添加到 safelist。
// uno.config.ts
export default defineConfig({
safelist: [
// 动态背景色
'bg-blue-500',
'bg-red-500',
'bg-green-500',
'bg-yellow-500',
// 动态间距
...Array.from({ length: 10 }, (_, i) => `p-${i}`),
...Array.from({ length: 10 }, (_, i) => `m-${i}`),
]
})按需加载:
UnoCSS 自动按需生成 CSS,无需手动配置。
<!-- 只使用了 3 个类,只生成这 3 个类的 CSS -->
<div class="flex items-center justify-center">
内容
</div>
<!-- 生成的 CSS (简化) -->
<style>
.flex { display: flex; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
</style>主题系统最佳实践
1. CSS 变量优先
使用 CSS 变量实现主题切换,而不是 SCSS 变量。
对比:
// ❌ 使用 SCSS 变量 - 无法运行时切换
$bg-color: #ffffff;
$text-color: #303133;
.container {
background: $bg-color;
color: $text-color;
}
// ✅ 使用 CSS 变量 - 支持运行时切换
:root {
--bg-color: #ffffff;
--text-color: #303133;
}
.dark {
--bg-color: #1a1a1a;
--text-color: #e5e5e5;
}
.container {
background: var(--bg-color);
color: var(--text-color);
}2. 语义化变量命名
使用语义化命名,而不是颜色字面量。
// ❌ 字面量命名
:root {
--white: #ffffff;
--black: #000000;
--gray-100: #f5f5f5;
}
.card {
background: var(--white);
color: var(--black);
}
// ✅ 语义化命名
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #303133;
--text-secondary: #606266;
}
.dark {
--bg-primary: #1a1a1a;
--bg-secondary: #2a2a2a;
--text-primary: #e5e5e5;
--text-secondary: #b0b0b0;
}
.card {
background: var(--bg-primary);
color: var(--text-primary);
}3. 分层变量系统
建立三层变量系统:基础色 → 语义色 → 组件色。
:root {
/* 第一层:基础色板 */
--color-blue-500: #409eff;
--color-green-500: #67c23a;
--color-red-500: #f56c6c;
--color-gray-50: #f5f7fa;
--color-gray-900: #303133;
/* 第二层:语义颜色 */
--color-primary: var(--color-blue-500);
--color-success: var(--color-green-500);
--color-danger: var(--color-red-500);
--bg-base: var(--color-gray-50);
--text-base: var(--color-gray-900);
/* 第三层:组件颜色 */
--button-bg: var(--color-primary);
--card-bg: var(--bg-base);
--input-border: var(--color-gray-300);
}
.dark {
/* 只需修改第二层变量 */
--bg-base: #1a1a1a;
--text-base: #e5e5e5;
/* 第三层自动继承 */
}4. 主题切换动画
使用 View Transition API 实现平滑切换。
基础实现:
// 检测浏览器支持
if (document.startViewTransition) {
document.startViewTransition(() => {
// 切换主题类名
document.documentElement.classList.toggle('dark')
})
} else {
// 不支持则直接切换
document.documentElement.classList.toggle('dark')
}圆形扩散动画:
// utils/themeAnimation.ts
export const toggleThemeWithAnimation = (event: MouseEvent, isDark: boolean) => {
const layout = useLayout()
// 获取点击位置
const x = event.clientX
const y = event.clientY
// 计算最大半径
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)
// 设置 CSS 变量
const root = document.documentElement
root.style.setProperty('--theme-x', `${x}px`)
root.style.setProperty('--theme-y', `${y}px`)
root.style.setProperty('--theme-r', `${endRadius}px`)
// 执行动画
if (document.startViewTransition) {
document.startViewTransition(() => {
layout.toggleDark(!isDark)
})
} else {
layout.toggleDark(!isDark)
}
}CSS 动画定义:
// theme-animation.scss
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-new(root) {
clip-path: circle(0 at var(--theme-x) var(--theme-y));
}
.dark::view-transition-old(root) {
clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y));
z-index: 1;
}
.dark::view-transition-new(root) {
clip-path: circle(0 at var(--theme-x) var(--theme-y));
}使用示例:
<template>
<el-switch
v-model="isDark"
@click="handleThemeSwitch"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useLayout } from '@/composables/useLayout'
import { toggleThemeWithAnimation } from '@/utils/themeAnimation'
const layout = useLayout()
const isDark = computed(() => layout.isDark)
const handleThemeSwitch = (event: MouseEvent) => {
toggleThemeWithAnimation(event, isDark.value)
}
</script>5. Element Plus 主题集成
覆盖 Element Plus 变量:
:root {
// 主色系
--el-color-primary: #409eff !important;
--el-color-success: #67c23a !important;
--el-color-warning: #e6a23c !important;
--el-color-danger: #f56c6c !important;
--el-color-info: #909399 !important;
// 组件尺寸
--el-component-size: 32px !important;
// 圆角
--el-border-radius-base: 6px !important;
--el-border-radius-small: 8px !important;
}暗黑模式适配:
.dark {
// Element Plus 会自动处理颜色深浅
// 只需覆盖特殊情况
--el-bg-color: #1a1a1a !important;
--el-text-color-primary: #e5e5e5 !important;
// 边框颜色
--el-border-color: rgba(255, 255, 255, 0.1) !important;
}响应式设计最佳实践
1. 移动优先策略
从小屏幕开始设计,逐步增强大屏幕体验。
// ✅ 移动优先
.container {
// 默认样式(移动端)
padding: 16px;
font-size: 14px;
// 平板及以上
@media (min-width: 768px) {
padding: 24px;
font-size: 16px;
}
// 桌面端
@media (min-width: 1200px) {
padding: 32px;
font-size: 18px;
}
}
// ❌ 桌面优先(不推荐)
.container {
// 默认样式(桌面端)
padding: 32px;
font-size: 18px;
// 平板
@media (max-width: 1199px) {
padding: 24px;
font-size: 16px;
}
// 移动端
@media (max-width: 767px) {
padding: 16px;
font-size: 14px;
}
}2. 断点使用规范
使用项目定义的标准断点。
标准断点:
// abstracts/_variables.scss
$sm: 768px; // 平板
$md: 992px; // 小屏桌面
$lg: 1200px; // 大屏桌面
$xl: 1920px; // 超大屏respond-to 混合器:
// 使用混合器
.sidebar {
width: 240px;
@include respond-to('md') {
width: 200px;
}
@include respond-to('sm') {
width: 100%;
}
}UnoCSS 响应式:
<template>
<!-- 移动端单列,平板2列,桌面3列 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>项目 1</div>
<div>项目 2</div>
<div>项目 3</div>
</div>
<!-- 移动端隐藏,桌面显示 -->
<aside class="hidden lg:block">
侧边栏内容
</aside>
<!-- 响应式间距 -->
<div class="p-4 md:p-6 lg:p-8">
内容区域
</div>
</template>3. Flexbox 和 Grid 布局
Flexbox 适用场景:
- 一维布局(行或列)
- 内容驱动的流式布局
- 对齐和分布控制
<template>
<!-- 水平居中对齐 -->
<div class="flex items-center justify-center h-screen">
<div>居中内容</div>
</div>
<!-- 两端对齐 -->
<div class="flex justify-between items-center p-4">
<span>左侧</span>
<span>右侧</span>
</div>
<!-- 垂直堆叠 -->
<div class="flex flex-col gap-4">
<div>项目 1</div>
<div>项目 2</div>
<div>项目 3</div>
</div>
</template>Grid 适用场景:
- 二维布局(行和列)
- 结构化的网格系统
- 复杂的对齐需求
<template>
<!-- 等宽列 -->
<div class="grid grid-cols-3 gap-4">
<div>列 1</div>
<div>列 2</div>
<div>列 3</div>
</div>
<!-- 响应式网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</div>
<!-- 不等宽列 -->
<div class="grid grid-cols-12 gap-4">
<div class="col-span-8">主内容(8列)</div>
<div class="col-span-4">侧边栏(4列)</div>
</div>
<!-- 复杂网格 -->
<div class="grid grid-cols-3 grid-rows-3 gap-4">
<div class="col-span-2 row-span-2">大区域</div>
<div>小区域 1</div>
<div>小区域 2</div>
<div class="col-span-3">底部横条</div>
</div>
</template>4. 容器查询(未来方案)
容器查询允许基于父容器尺寸应用样式。
// 容器查询 API(实验性)
.card-container {
container-type: inline-size;
container-name: card;
}
.card-content {
padding: 1rem;
@container card (min-width: 400px) {
padding: 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
}
@container card (min-width: 600px) {
padding: 3rem;
grid-template-columns: 1fr 1fr 1fr;
}
}性能优化最佳实践
1. CSS 选择器优化
避免深层嵌套:
// ❌ 过深嵌套(性能差)
.container {
.wrapper {
.content {
.item {
.title {
.text {
color: red; // 6层嵌套
}
}
}
}
}
}
// ✅ 扁平化(性能好)
.container {
// 一级样式
}
.container-content {
// 二级样式
}
.container-item-title {
// 使用 BEM 命名,避免嵌套
color: red;
}避免通配符选择器:
// ❌ 性能差
* {
margin: 0;
padding: 0;
}
.container * {
box-sizing: border-box;
}
// ✅ 针对性重置
html, body, div, p, h1, h2, h3 {
margin: 0;
padding: 0;
}
.container > * {
box-sizing: border-box; // 只影响直接子元素
}优先使用类选择器:
// ❌ 标签选择器(性能差)
div p span {
color: red;
}
// ❌ 属性选择器(性能差)
[data-type="button"] {
padding: 10px;
}
// ✅ 类选择器(性能好)
.text-content {
color: red;
}
.btn {
padding: 10px;
}2. 动画性能优化
使用 transform 代替 position:
// ❌ 使用 position(触发 reflow)
.move-item {
position: relative;
animation: movePosition 1s ease;
}
@keyframes movePosition {
from { left: 0; top: 0; }
to { left: 100px; top: 100px; }
}
// ✅ 使用 transform(仅触发 composite)
.move-item {
animation: moveTransform 1s ease;
}
@keyframes moveTransform {
from { transform: translate(0, 0); }
to { transform: translate(100px, 100px); }
}使用 will-change 提示:
// 提前告知浏览器将要变化的属性
.animated-element {
will-change: transform, opacity;
transition: transform 0.3s, opacity 0.3s;
&:hover {
transform: scale(1.1);
opacity: 0.8;
}
}
// ⚠️ 不要滥用 will-change
// ❌ 错误 - 对所有元素使用
* {
will-change: transform; // 会消耗大量内存
}
// ✅ 正确 - 只对动画元素使用
.dialog {
&.entering,
&.leaving {
will-change: transform, opacity;
}
&.entered {
will-change: auto; // 动画结束后移除
}
}GPU 加速:
// 触发 GPU 加速的属性
.gpu-accelerated {
// 3D transform
transform: translateZ(0);
transform: translate3d(0, 0, 0);
// 或者
will-change: transform;
// 或者
backface-visibility: hidden;
}3. 关键 CSS 内联
将首屏关键样式内联到 HTML。
<!-- index.html -->
<head>
<!-- 内联关键 CSS -->
<style>
/* 首屏必需的最小样式 */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.app-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 24px;
color: #409eff;
}
</style>
<!-- 异步加载完整样式 -->
<link rel="preload" href="/assets/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>4. CSS 代码分割
按路由分割 CSS 文件。
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vendor': ['vue', 'vue-router', 'pinia'],
},
// CSS 代码分割
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'assets/css/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
}
}
},
// CSS 代码分割
cssCodeSplit: true,
}
})5. 移除未使用的 CSS
使用 PurgeCSS 或 UnoCSS 自动移除未使用的样式。
// UnoCSS 自动按需生成
// uno.config.ts
export default defineConfig({
// UnoCSS 只生成使用的类
// 无需手动配置 PurgeCSS
})代码规范最佳实践
1. BEM 命名约定
使用 BEM (Block Element Modifier) 命名组件样式。
命名规则:
.block {} // 块
.block__element {} // 元素
.block--modifier {} // 修饰符
.block__element--modifier {} // 元素修饰符示例:
<template>
<!-- 卡片组件 -->
<div class="card card--primary">
<div class="card__header">
<h3 class="card__title">卡片标题</h3>
<span class="card__subtitle card__subtitle--secondary">副标题</span>
</div>
<div class="card__body">
<p class="card__text">卡片内容</p>
</div>
<div class="card__footer">
<button class="card__button card__button--primary">操作</button>
</div>
</div>
</template>2. 样式作用域
优先使用 scoped 样式,避免全局污染。
<!-- ✅ 使用 scoped -->
<!-- ⚠️ 全局样式 - 谨慎使用 -->
<style lang="scss">
.global-container {
padding: 20px; // 影响整个应用
}
</style>
<!-- ✅ 混合使用 -->
<style lang="scss">
/* 只在必要时添加全局样式 */
.global-utility {
/* 全局工具类 */
}
</style>深度选择器:
3. 注释规范
为复杂样式添加注释。
/**
* 卡片组件样式
* @description 提供统一的卡片样式,支持多种主题和尺寸
*/
.card {
// 基础布局
display: flex;
flex-direction: column;
// 间距系统
padding: var(--spacing-md); // 16px
margin-bottom: var(--spacing-lg); // 24px
// 视觉样式
background: var(--bg-primary);
border-radius: var(--radius-md); // 8px
// 阴影效果(仅在亮色主题显示)
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
/* 响应式适配 */
@include respond-to('sm') {
padding: var(--spacing-sm); // 移动端减少内边距
}
/* 暗黑模式适配 */
.dark & {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); // 增强阴影
}
}
/**
* 主题修饰符
*/
.card--primary {
border-left: 4px solid var(--el-color-primary); // 主题色左边框
}
// TODO: 添加更多主题变体
// FIXME: 修复 Safari 中的圆角问题4. 样式组织顺序
按照固定顺序组织 CSS 属性。
.element {
/* 1. 定位 */
position: absolute;
top: 0;
left: 0;
z-index: 10;
/* 2. 盒模型 */
display: flex;
width: 100%;
height: 50px;
padding: 16px;
margin: 20px;
border: 1px solid #ddd;
/* 3. 排版 */
font-family: sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 1.5;
text-align: center;
color: #333;
/* 4. 视觉效果 */
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
opacity: 1;
/* 5. 其他 */
cursor: pointer;
transition: all 0.3s ease;
transform: translateX(0);
}5. 避免魔法数字
使用变量代替硬编码的数字。
// ❌ 魔法数字
.sidebar {
width: 240px;
top: 50px;
z-index: 1001;
padding: 16px;
border-radius: 8px;
}
// ✅ 使用变量
:root {
--sidebar-width: 240px;
--header-height: 50px;
--z-sidebar: 1001;
--spacing-md: 16px;
--radius-md: 8px;
}
.sidebar {
width: var(--sidebar-width);
top: var(--header-height);
z-index: var(--z-sidebar);
padding: var(--spacing-md);
border-radius: var(--radius-md);
}团队协作最佳实践
1. 样式指南文档
维护团队样式指南文档。
内容包括:
- 颜色系统
- 间距系统
- 字体系统
- 组件样式库
- 命名规范
- 最佳实践
2. 代码审查要点
样式代码审查检查清单:
架构和组织:
- [ ] 样式文件是否按功能模块组织
- [ ] 是否避免了不必要的全局样式
- [ ] 是否使用了 scoped 样式
性能:
- [ ] 是否避免了深层选择器嵌套
- [ ] 动画是否使用了 transform 而不是 position
- [ ] 是否合理使用了 will-change
命名和规范:
- [ ] 类名是否遵循 BEM 命名
- [ ] CSS 变量命名是否语义化
- [ ] 是否使用了项目定义的变量和混合器
响应式:
- [ ] 是否遵循移动优先原则
- [ ] 是否使用了标准断点
- [ ] 是否测试了不同屏幕尺寸
主题适配:
- [ ] 是否适配了暗黑模式
- [ ] 是否使用了 CSS 变量而不是硬编码颜色
- [ ] 是否考虑了主题切换动画
3. 样式 Lint 配置
配置 Stylelint 强制代码规范。
// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard-scss',
'stylelint-config-recommended-vue',
],
rules: {
// 选择器嵌套最多3层
'max-nesting-depth': 3,
// 类名命名规范(BEM)
'selector-class-pattern': '^[a-z][a-z0-9]*(-[a-z0-9]+)*((__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?)?$',
// 颜色必须使用变量
'color-no-hex': true,
// 禁止重复选择器
'no-duplicate-selectors': true,
// 属性顺序
'order/properties-order': [
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'width',
'height',
// ... 更多属性
],
},
}4. 组件库开发规范
开发可复用组件时的样式规范。
<!-- 组件: Card.vue -->
<template>
<div :class="cardClass">
<div v-if="$slots.header" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div v-if="$slots.footer" class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface CardProps {
/** 卡片类型 */
type?: 'default' | 'primary' | 'success' | 'warning' | 'danger'
/** 是否显示阴影 */
shadow?: 'always' | 'hover' | 'never'
/** 是否可悬浮 */
hoverable?: boolean
}
const props = withDefaults(defineProps<CardProps>(), {
type: 'default',
shadow: 'always',
hoverable: false,
})
const cardClass = computed(() => [
'card',
`card--${props.type}`,
`card--shadow-${props.shadow}`,
{
'card--hoverable': props.hoverable,
},
])
</script>常见问题和解决方案
1. 样式优先级冲突
问题描述:
Element Plus 样式被覆盖失败。
原因分析:
- Element Plus 使用了较高的选择器特异性
- 样式导入顺序不正确
- 缺少
!important
解决方案:
// ❌ 优先级不够
.el-button {
background: red;
}
// ✅ 增加特异性
.custom-button.el-button {
background: red;
}
// ✅ 使用 deep 选择器
:deep(.el-button) {
background: red;
}
// ✅ 使用 !important (最后手段)
.el-button {
background: red !important;
}
// ✅ 最佳 - 覆盖 CSS 变量
:root {
--el-color-primary: red !important;
}2. scoped 样式不生效
问题描述:
scoped 样式无法影响子组件。
原因分析:
Vue scoped 样式只影响当前组件根元素和其子元素,不影响子组件内部。
解决方案:
<!-- 父组件 -->
<template>
<div class="parent">
<ChildComponent class="child" />
</div>
</template>3. CSS 变量在 SCSS 中使用
问题描述:
在 SCSS 函数中使用 CSS 变量报错。
原因分析:
SCSS 编译时无法解析运行时的 CSS 变量。
解决方案:
// ❌ SCSS 函数无法处理 CSS 变量
@function darken-color($color) {
@return darken($color, 10%);
}
.button {
background: darken-color(var(--color-primary)); // 报错
}
// ✅ 使用 CSS calc() 和 filter
.button {
background: var(--color-primary);
&:hover {
filter: brightness(0.9); // 使用 CSS filter
}
}
// ✅ 或者在 CSS 中使用 color-mix
.button {
background: var(--color-primary);
&:hover {
background: color-mix(in srgb, var(--color-primary) 90%, black);
}
}
// ✅ 或者定义衍生变量
:root {
--color-primary: #409eff;
--color-primary-dark: #3a8fe7; // 手动定义深色版本
}
.button {
background: var(--color-primary);
&:hover {
background: var(--color-primary-dark);
}
}4. 动画卡顿优化
问题描述:
过渡动画不流畅,出现卡顿。
原因分析:
- 动画触发了 reflow/repaint
- 动画元素过多
- 未使用 GPU 加速
解决方案:
// ❌ 触发 reflow
.slide-enter-active {
transition: width 0.3s, height 0.3s, left 0.3s, top 0.3s;
}
// ✅ 使用 transform(仅触发 composite)
.slide-enter-active {
transition: transform 0.3s, opacity 0.3s;
}
.slide-enter-from {
transform: translateX(-100%);
opacity: 0;
}
.slide-enter-to {
transform: translateX(0);
opacity: 1;
}
// ✅ 添加 will-change 提示
.slide-enter-active,
.slide-leave-active {
will-change: transform, opacity;
transition: transform 0.3s, opacity 0.3s;
}
// ✅ 动画结束后移除 will-change
.slide-enter-to,
.slide-leave-from {
will-change: auto;
}
// ✅ 启用 GPU 加速
.animated-element {
transform: translateZ(0); // 触发 3D 加速
}5. UnoCSS 类名不生效
问题描述:
动态生成的 UnoCSS 类名不起作用。
原因分析:
UnoCSS 无法识别字符串拼接的类名。
解决方案:
<template>
<!-- ❌ 字符串拼接 - 类名不会生成 -->
<div :class="'text-' + color + '-500'">
文本
</div>
<!-- ✅ 完整类名 -->
<div :class="{
'text-blue-500': color === 'blue',
'text-red-500': color === 'red',
'text-green-500': color === 'green'
}">
文本
</div>
<!-- ✅ 或者使用 safelist -->
<div :class="`text-${color}-500`">
文本
</div>
</template>
<script setup lang="ts">
// uno.config.ts 中添加 safelist
export default defineConfig({
safelist: [
'text-blue-500',
'text-red-500',
'text-green-500',
]
})
</script>6. 主题切换闪烁
问题描述:
切换主题时出现短暂的白屏或闪烁。
原因分析:
- 主题类名应用延迟
- 浏览器重新计算样式
- 未使用过渡动画
解决方案:
// ✅ 使用 View Transition API
const toggleTheme = () => {
if (!document.startViewTransition) {
// 不支持则直接切换
document.documentElement.classList.toggle('dark')
return
}
// 使用过渡动画
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
}
// ✅ 或者在页面加载前应用主题
// index.html
<script>
// 在页面渲染前应用主题,避免闪烁
const theme = localStorage.getItem('theme')
if (theme === 'dark') {
document.documentElement.classList.add('dark')
}
</script>
// ✅ 使用 CSS 过渡
:root {
--transition-theme: background-color 0.3s ease, color 0.3s ease;
}
* {
transition: var(--transition-theme);
}总结
核心原则
- 架构优先 - 建立清晰的样式架构和分层
- 原子化优先 - 优先使用 UnoCSS,减少自定义样式
- 变量驱动 - 使用 CSS 变量实现主题和动态样式
- 移动优先 - 从小屏幕开始,逐步增强
- 性能至上 - 优化选择器、动画和加载策略
- 规范统一 - 遵循 BEM 命名和团队规范
关键实践
- 使用 SCSS 分层架构组织样式代码
- 优先使用 UnoCSS 原子类而不是自定义样式
- 使用 CSS 变量实现主题系统和暗黑模式
- 遵循移动优先的响应式设计策略
- 使用 transform 和 will-change 优化动画性能
- 遵循 BEM 命名约定和团队编码规范
学习路径
- 入门阶段 - 了解 SCSS 基础和 UnoCSS 使用
- 进阶阶段 - 掌握主题系统、响应式设计和性能优化
- 精通阶段 - 深入理解架构设计、代码规范和团队协作
持续改进
- 定期审查和优化样式代码
- 关注 CSS 新特性和最佳实践
- 收集团队反馈,持续更新规范
- 建立样式组件库,提高复用性
通过遵循这些最佳实践,可以构建高质量、可维护、高性能的样式系统,提升开发效率和用户体验。
