裸辞半年后,我用 Vue3 重构了老项目
去年十一月,我从某大厂“战略性撤退”——说白了就是裸辞了。Gap 半年里,一边在上海租房续命(房租贵得我怀疑人生),一边折腾各种新技术:Svelte、Qwik、甚至玩了一阵 WebAssembly。但现实很骨感:面试官一问“你最近在做什么项目?”,我支支吾吾只能拿 GitHub 上几个玩具 demo 出来撑场面。
于是上个月重新找工作前,我咬牙决定:用 Vue.js 生态从零到一搞个像样的实战项目。不为别的,就为了简历上能写一句“独立完成全栈 SPA 应用开发”。结果这一折腾,踩的坑比我过去两年在大厂踩的都多。
老项目的债,终究要还
事情起因是帮前同事维护一个内部管理系统。那是个典型的 Vue2 + Options API 老古董,代码耦合得像泡面——面条缠在一起,想捞一根出来都难。最离谱的是,某个列表页加载 500 条数据直接卡死,产品经理上周五晚上十点发消息:“能不能优化下?明天演示给老板看。”
我打开 DevTools 一看,好家伙,v-for 里嵌套三层 v-if,每个组件都在 mounted 里发三个请求……当时真的想砸电脑。但冷静下来一想:这不正是练手 Vue3 Composition API 的绝佳机会吗?
从 create-vue 到 Pinia:现代 Vue 开发流水线
这次我决定彻底现代化。先跑官方脚手架:
npm create vue@3
一路回车选了 TypeScript + Pinia + Vue Router + Vitest。说实话,Vite 的热更新速度让我感动哭了——以前在公司 Webpack 构建动不动 30 秒,现在改一行代码瞬间刷新,幸福感拉满。
状态管理:告别 Vuex 的仪式感
以前在大厂写 Vuex,光是 mapState、mapActions 就能写半屏。现在 Pinia 直接导出 store 实例,组合式函数里直接解构使用:
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const userInfo = ref<User | null>(null)
const fetchUser = async () => {
const res = await api.getUser()
userInfo.value = res.data
}
return { userInfo, fetchUser }
})
在组件里:
<script setup>
import { useUserStore } from '@/stores/user'
const { userInfo, fetchUser } = useUserStore()
</script>
没有 modules 嵌套地狱,没有命名空间冲突,连 TypeScript 类型推导都丝滑。难怪公司新项目都偷偷切 Pinia 了——虽然 PM 还在用“状态管理框架”这种过时术语。
组件通信:Provide/Inject 的优雅时刻
有个需求:全局主题切换。以前的做法是在根组件放个 theme 状态,层层 props 透传下去。现在用 Provide/Inject 配合 Composables:
// composables/useTheme.ts
import { provide, inject, ref } from 'vue'
const ThemeSymbol = Symbol()
export function provideTheme() {
const theme = ref<'light' | 'dark'>('light')
provide(ThemeSymbol, theme)
return { theme }
}
export function useTheme() {
const theme = inject(ThemeSymbol)
if (!theme) throw new Error('useTheme() must be called after provideTheme()')
return theme
}
App.vue 里调用 provideTheme(),任何子孙组件直接 const theme = useTheme() 拿到响应式引用。再也不用担心中间组件“吃掉”props 了!
性能优化:从卡成 PPT 到丝般顺滑
回到最初那个 500 条数据的列表页。重构后做了三件事:
- 虚拟滚动:用
vue-virtual-scroller替代原生v-for - 懒加载图片:
<img v-lazy="item.avatar"> - 计算属性缓存:把格式化时间等操作移到 computed
关键代码:
<template>
<RecycleScroller
class="scroller"
:items="filteredItems"
:item-size="60"
key-field="id"
>
<template #default="{ item }">
<UserItem :user="item" />
</template>
</RecycleScroller>
</template>
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref([]) // 500+ 条数据
const searchKeyword = ref('')
// 计算属性自动缓存!
const filteredItems = computed(() =>
items.value.filter(item =>
item.name.includes(searchKeyword.value)
)
)
</script>
效果立竿见影:内存占用从 400MB 降到 80MB,滚动帧率稳定 60fps。测试同学惊呼:“这还是同一个页面?”
调试技巧:Vue DevTools 6 的隐藏彩蛋
这次开发中发现 Vue DevTools 6 有个神功能:组件依赖图谱。点击任意组件,能看到它依赖哪些 props、响应式变量,以及被哪些组件引用。
有次遇到一个诡异 bug:搜索框输入后列表没更新。通过依赖图谱发现,filteredItems 计算属性居然没追踪到 searchKeyword —— 原来我把 searchKeyword 定义成了普通变量而不是 ref!这种问题以前只能 console.log 到天荒地老。
GitHub 上那些救我命的资源
作为技术博客重度用户,我整理了一份 Vue3 生态必备资源清单(已同步到 GitHub):
| 类别 | 推荐资源 | 为什么香 |
|---|---|---|
| UI 库 | Naive UI | TypeScript 完美支持,暗黑模式开箱即用 |
| 工具库 | VueUse | 70+ 个 Composition 函数,省下 80% 轮子代码 |
| 脚手架 | Vitesse | Anthony Fu 大神的 starter,集成最佳实践 |
| 学习 | Vue Mastery 免费课 | 手把手教 Composition API |
特别安利 VueUse 的 useDebounceFn——解决搜索防抖只需一行:
const debouncedSearch = useDebounceFn(() => {
// 执行搜索
}, 300)
再也不用手写 debounce 工具函数了!
代码人生的顿悟时刻
重构完这个项目,我突然理解了 Vue 团队的设计哲学:用最小的认知成本解决最大范围的问题。Composition API 不是炫技,而是让逻辑复用变得像写函数一样自然;Pinia 不是替代 Vuex,而是消除模板代码的仪式感。
上周面试时,面试官问我:“为什么选择 Vue 而不是 React?” 我笑着回答:“因为当我深夜加班改 Bug 时,Vue 的报错信息会告诉我第几行第几个字符错了——而 React 只会说 ‘Something went wrong’。”
当然,这只是玩笑。真正的原因是:Vue 让我更专注于业务逻辑,而不是框架本身。在大厂时我们总追求“高大上”的技术栈,却忘了前端的核心使命——交付用户体验。这次裸辞后的实战让我重新找回了 coding 的初心。
如果你也在维护祖传 Vue2 项目,或者正准备入坑 Vue3,不妨试试这些方案。我的 GitHub 仓库 已开源完整代码,欢迎 star & issue(求轻喷,毕竟 Gap 期间写的代码可能有点野)。
最后送大家一句我在工位贴了三年的话:“代码即人生,删掉冗余,留下优雅。”
P.S. 租房合同快到期了,如果这篇博客帮你拿到了 offer,记得请我喝杯瑞幸(上海静安寺附近) 😏

评论 0