【Vue3 + 中后台开发】:从基础响应式到工程化优化,掌握日常必做性能规范,避开渲染与维护高频坑!

📑 文章目录
- 一、前言:我们为什么要写这篇规范
- 二、基础扫盲:先搞清楚这些概念
- [2.1 computed 和 watch 怎么选?](#2.1 computed 和 watch 怎么选?)
- [2.2 v-if 和 v-show 怎么选?](#2.2 v-if 和 v-show 怎么选?)
- [2.3 ref 和 reactive 怎么选?](#2.3 ref 和 reactive 怎么选?)
- [2.4 :key 必须认真写](#2.4 :key 必须认真写)
- 三、日常必做优化清单
- [3.1 列表渲染:减少不必要的子组件](#3.1 列表渲染:减少不必要的子组件)
- [3.2 事件处理器:避免内联创建函数](#3.2 事件处理器:避免内联创建函数)
- [3.3 合理使用 v-once 和 v-memo](#3.3 合理使用 v-once 和 v-memo)
- [3.4 大组件拆分 + defineOptions](#3.4 大组件拆分 + defineOptions)
- [四、进阶实战:路由懒加载与 Keep-Alive](#四、进阶实战:路由懒加载与 Keep-Alive)
- [4.1 路由懒加载(必做)](#4.1 路由懒加载(必做))
- [4.2 Keep-Alive 缓存页面状态](#4.2 Keep-Alive 缓存页面状态)
- 五、可维护性与兜底规范
- [5.1 兜底:Loading / 空态 / 错误](#5.1 兜底:Loading / 空态 / 错误)
- [5.2 兜底:接口超时与重试](#5.2 兜底:接口超时与重试)
- [5.3 兜底:大列表用虚拟滚动](#5.3 兜底:大列表用虚拟滚动)
- [5.4 兜底:图片懒加载](#5.4 兜底:图片懒加载)
- 六、避坑总结
- 七、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
- [📝 可维护性与兜底规范](#📝 可维护性与兜底规范)
- [📚 系列总览](#📚 系列总览)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、前言:我们为什么要写这篇规范
工作几年后会发现,很多「性能优化」文章要么讲得特别底层,要么只给一两行代码。本文的目的很简单:让你在日常写 Vue3 时,知道该选什么、为什么选,以及容易踩的坑。
适合人群:
- 会写 JS,但
computed、watch、ref有点混 - 技术小白,想从零开始打好基础
- 有经验的前端,想系统梳理和校准自己的习惯
本文按「日常必做」→「进阶实战」→「可维护性与兜底」来写,力求可落地。
[⬆ 返回目录](#⬆ 返回目录)
二、基础扫盲:先搞清楚这些概念
2.1 computed 和 watch 怎么选?
简单记:
- 需要同步算出新值 → 用
computed - 需要做副作用 (发请求、打日志等)→ 用
watch
html
<script setup>
import { ref, computed, watch } from 'vue'
const keyword = ref('')
const list = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' },
])
// ✅ computed:纯计算,依赖变化自动重新算
const filteredList = computed(() => {
return list.value.filter(item =>
item.name.includes(keyword.value)
)
})
// ✅ watch:需要副作用时用
watch(keyword, (newVal) => {
console.log('搜索关键词变了:', newVal)
// 比如这里可以调接口、埋点等
})
</script>
<template>
<input v-model="keyword" placeholder="搜索" />
<ul>
<li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
</ul>
</template>
常见坑:
- 用
watch去算一个「展示用的派生值」→ 应换成computed,更符合 Vue 的响应式设计,也更好维护 - 在
computed里写fetch、localStorage等副作用 → 应放到watch或onMounted
[⬆ 返回目录](#⬆ 返回目录)
2.2 v-if 和 v-show 怎么选?
一句话:
条件切换频繁 用 v-show,初始就不常出现 用 v-if。
| 对比项 | v-if | v-show |
|---|---|---|
| 原理 | 条件为 false 时不渲染 | 始终渲染,用 CSS display 控制 |
| 切换成本 | 高(频繁创建/销毁) | 低 |
| 初始渲染 | 不渲染的节点不占内存 | 会渲染,占内存 |
html
<template>
<!-- ✅ 弹窗、Tab 等频繁切换 → v-show -->
<div v-show="isModalOpen" class="modal">弹窗内容</div>
<!-- ✅ 权限、一次性条件 → v-if -->
<AdminPanel v-if="user.role === 'admin'" />
</template>
常见坑: 大量列表项里用 v-if 做过滤 → 应先在数据层过滤(如用 computed),再 v-for 渲染,而不是在模板里 v-if。
[⬆ 返回目录](#⬆ 返回目录)
2.3 ref 和 reactive 怎么选?
建议:普通场景多用 ref 。
ref:适合基础类型、单个对象,解构不会丢响应式reactive:适合对象整体是「可变的配置」,解构会丢响应式
js
import { ref, reactive } from 'vue'
// ✅ ref:适合大部分场景
const count = ref(0)
const user = ref({ name: '张三', age: 18 })
// reactive:解构会丢失响应式!
const state = reactive({ count: 0 })
const { count } = state // ❌ count 不再响应式
[⬆ 返回目录](#⬆ 返回目录)
2.4 :key 必须认真写
v-for 的 key 直接影响虚拟 DOM 的 diff 和复用。
原则: 用稳定、唯一 的 id,不要用 index。
html
<!-- ❌ index 会随排序、增删变化,导致错乱或多余渲染 -->
<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>
<!-- ✅ 用唯一 id -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
常见坑: 接口没返回 id 时,可用 item.xxx + index 做临时 key,但上线前应改为真实 id。
[⬆ 返回目录](#⬆ 返回目录)
三、日常必做优化清单
3.1 列表渲染:减少不必要的子组件
html
<!-- ❌ 每次都创建新组件实例 -->
<div v-for="item in list" :key="item.id">
<HeavyChild :data="item" />
</div>
<!-- ✅ 简单展示用内联,避免多余组件 -->
<div v-for="item in list" :key="item.id">
<span>{{ item.name }}</span>
<span>{{ item.price }}</span>
</div>
规则: 只有在有独立逻辑、状态、插槽或需要被复用的时候,再拆成组件。
[⬆ 返回目录](#⬆ 返回目录)
3.2 事件处理器:避免内联创建函数
html
<!-- ❌ 每次渲染都创建新函数 -->
<button @click="() => doSomething(item.id)">点击</button>
<!-- ✅ 稳定引用 -->
<button @click="handleClick(item.id)">点击</button>
js
const handleClick = (id) => {
doSomething(id)
}
原因: 内联箭头函数会随渲染不断新建,增加子组件不必要的更新。
[⬆ 返回目录](#⬆ 返回目录)
3.3 合理使用 v-once 和 v-memo
html
<!-- 静态内容只渲染一次 -->
<p v-once>{{ staticTitle }}</p>
<!-- v-memo:依赖不变就不更新(Vue 3.2+)-->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.name]">
{{ item.name }} - {{ item.price }}
</div>
v-memo 适合:列表项结构简单,但数据量大的场景。
[⬆ 返回目录](#⬆ 返回目录)
3.4 大组件拆分 + defineOptions
大组件难以维护,也影响局部更新。建议按「区块」拆分,并用 defineOptions 明确 name:
html
<!-- UserProfile.vue -->
<script setup>
defineOptions({
name: 'UserProfile',
})
</script>
<template>
<div class="user-profile">
<UserAvatar />
<UserInfo />
<UserStats />
</div>
</template>
[⬆ 返回目录](#⬆ 返回目录)
四、进阶实战:路由懒加载与 Keep-Alive
4.1 路由懒加载(必做)
js
// ❌ 同步加载:首屏会加载所有路由
const routes = [
{ path: '/home', component: Home },
{ path: '/about', component: About },
]
// ✅ 懒加载:按需加载
const routes = [
{ path: '/home', component: () => import('@/views/Home.vue') },
{ path: '/about', component: () => import('@/views/About.vue') },
]
这样首屏只加载当前路由对应的 chunk,其它路由在访问时才加载。
[⬆ 返回目录](#⬆ 返回目录)
4.2 Keep-Alive 缓存页面状态
html
<!-- App.vue 或布局组件 -->
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['Home', 'List']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
include 要和组件的 name 一致。被缓存的页面再次进入时不会重新创建,适合列表滚动位置、表单填写等场景。
[⬆ 返回目录](#⬆ 返回目录)
五、可维护性与兜底规范
5.1 兜底:Loading / 空态 / 错误
html
<template>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="error" class="error">
{{ error }}
<button @click="retry">重试</button>
</div>
<div v-else-if="!list.length" class="empty">暂无数据</div>
<div v-else>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
const loading = ref(true)
const error = ref(null)
const list = ref([])
const fetchData = async () => {
loading.value = true
error.value = null
try {
const res = await api.getList()
list.value = res.data
} catch (e) {
error.value = e.message || '加载失败'
} finally {
loading.value = false
}
}
const retry = () => fetchData()
onMounted(fetchData)
</script>
这样用户在任何状态下都能得到明确反馈。
[⬆ 返回目录](#⬆ 返回目录)
5.2 兜底:接口超时与重试
js
const fetchWithTimeout = (url, options = {}, timeout = 5000) => {
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeout)
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(id))
}
生产环境建议配合业务封装统一请求库,做超时、重试、错误上报。
[⬆ 返回目录](#⬆ 返回目录)
5.3 兜底:大列表用虚拟滚动
当列表有几百、上千条时,只渲染可视区域:
bash
npm install vue-virtual-scroller
html
<template>
<RecycleScroller
:items="list"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
[⬆ 返回目录](#⬆ 返回目录)
5.4 兜底:图片懒加载
html
<img v-lazy="imageUrl" alt="" />
js
// main.js 或插件
app.directive('lazy', {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value
}
})
observer.observe(el)
},
})
[⬆ 返回目录](#⬆ 返回目录)
六、避坑总结
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 派生数据 | 用 watch 计算展示用数据 | 用 computed |
| 条件渲染 | 频繁切换用 v-if | 用 v-show |
| 列表 key | 用 index | 用唯一 id |
| 事件 | 内联箭头函数 | 抽成方法 |
| 路由 | 同步 import | 懒加载 import |
| 大列表 | 直接全部渲染 | 虚拟滚动或分页 |
| 接口 | 无 loading/error | 加 loading、error、重试 |
[⬆ 返回目录](#⬆ 返回目录)
七、小结
性能优化不需要追求极端,重点是:
- 概念清晰:computed / watch、v-if / v-show、ref / reactive 会用、会用对
- 日常习惯:正确 key、避免内联函数、合理拆组件
- 必做项:路由懒加载、图片懒加载、大列表虚拟滚动
- 可维护与兜底:Loading、空态、错误处理、超时重试
这些规范在日常开发中都能直接落地,不会有太玄学的内容。建议先按本文做一轮自检,再结合实际项目逐步迭代优化。
🔍 系列模块导航
📝 可维护性与兜底规范
一、《Vue 中后台项目可维护性规范:代码分层 + 逻辑拆分 + 数据兜底,3 年仍可维护|可维护性与兜底规范篇》
二、《Vue3 性能优化规范:日常必做优化(不玄学、可落地)|可维护性与兜底规范篇》
三、《Vue3 + 组合式 API 前端异常兜底规范:接口失败 / 无数据 / 页面异常统一处理|可维护性与兜底规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试
四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力
- 前端基础实战系列 : 《前端基础实战:JS/TS与Vue体系化扫盲(47 篇完整目录 + 避坑)》
- 前端规范实战系列 : 《JS/TS/Vue 前端规范实战:从写对到写优,搞定中后台规范落地,打造可维护代码(40 篇全目录)》
- 前端架构实战系列:聚焦工程化、性能优化、可维护架构、中后台体系设计(持续更新中)
- 前端大厂面试系列:覆盖高频考点、手写题、项目深挖、简历与面试技巧(规划中)
每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。
全套内容持续更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
如果你觉得这篇文章对你有帮助,欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~