从 Vue 2 到 Vue 3,我在大型项目中的进阶之路:Vue.js 生态系统深度探索与实战
开篇:为什么我要写这篇文章?

大家好,我是前端组的一个老码农,目前在一家中型互联网公司负责后台系统的开发维护工作。说到 Vue.js,其实我早在两三年前就开始用它了,那时候还是 Vue 2 的天下。现在我们公司在重构一个核心后台管理系统时,决定全面升级到 Vue 3,并引入 Vite + Composition API + Pinia 这一套生态组合。
这趟旅程并不轻松,过程中踩了很多坑,也积累了不少经验。今天就想通过这篇文章,和大家分享一下我在实际项目中使用 Vue.js 及其生态的一些实战经验和心得。希望对正在转型 Vue 3 或者准备深入掌握 Vue 生态体系的你有所帮助。
问题描述:旧项目的瓶颈与重构动机

我们原来的系统是基于 Vue 2 + Element UI 搭建的一套后台管理平台,功能模块多、组件复用率高。随着业务增长,这个项目逐渐暴露出以下痛点:
- 代码结构臃肿:由于历史原因,大量的业务逻辑混杂在
methods和生命周期钩子中,可读性差,后期难以维护。 - 状态管理混乱:Vuex 虽然解决了全局状态共享的问题,但在大型项目中,拆分
modules后依然显得笨重,调试困难。 - 性能下降明显:页面打开缓慢,尤其是数据量大的表格页面,卡顿严重,严重影响用户体验。
- 兼容性和可拓展性不足:随着团队人数增加,新同学加入后上手慢,缺乏统一的技术规范;同时,老代码难以扩展新的业务需求。
面对这些挑战,我们决定做一次彻底的重构,目标是:
- 升级至 Vue 3
- 使用 Composition API 重构核心模块
- 引入 Pinia 替代 Vuex
- 使用 Vite 加速构建流程
- 提升整体性能与可维护性
解决方案:技术选型与架构设计
在确定技术栈的时候,我们主要考虑了以下几点:
技术选型
| 工具 | 版本 | 原因 |
|---|---|---|
| Vue | 3.4.x | Composition API 更清晰,TypeScript 支持更好 |
| Vite | 4.3.x | 极快的冷启动速度,支持按需加载 |
| Pinia | 2.x | 更简洁的状态管理,替代 Vuex 成为主流 |
| TypeScript | 5.x | 类型安全 + 团队协作更高效 |
| Vue Router | 4.x | Vue 3 官方配套 |
| Element Plus | 2.x | Vue 3 兼容版,UI 组件丰富 |

架构设计思路
我们在架构设计上采用了以下策略:
- 按模块划分结构:将整个系统划分为几个大功能区,每个区域为一个独立模块(如:订单中心、权限中心等),模块间尽量解耦。
- Composition API 重构业务逻辑:将原来写在
methods中的逻辑全部拆分成composables函数,提高复用率。 - Pinia 状态管理优化:将原先的 Vuex modules 改写为多个 Pinia store,配合接口抽象化统一调用方式。
- 性能优化手段:
- 所有表格组件按需加载
- 对复杂计算进行缓存处理(比如使用 computed)
- 接口请求去重(防重复请求)
代码实践:关键实现片段分享
一、Pinia Store 示例 —— 封装用户信息获取模块
以前在 Vuex 中要定义一个 user module 大概是这样:
// vuex/modules/user.js
export default {
state: { ... },
mutations: { ... },
actions: { ... }
}
而在 Pinia 中我们可以这样实现:
// stores/userStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import api from '@/api'
export const useUserStore = defineStore('user', () => {
const userInfo = ref(null)
async function fetchUserInfo() {
try {
const res = await api.getUserInfo()
userInfo.value = res.data
} catch (e) {
console.error(e)
}
}
return {
userInfo,
fetchUserInfo
}
})
然后在组件里直接调用即可:
import { useUserStore } from '@/stores/userStore'
export default defineComponent({
setup() {
const userStore = useUserStore()
onMounted(async () => {
await userStore.fetchUserInfo()
})
return { userStore }
}
})
是不是清爽多了?不需要再写 mapState、mapActions,也不需要区分 action 和 mutation,一切都变得自然得多。
二、Composition API 分离业务逻辑 —— 表格搜索封装
我们有一个订单管理页,里面包含复杂的筛选条件。之前的写法是在组件内部维护大量 data 和 methods,很难复用。现在改成 composition API 后,我们可以这样:
// composables/useOrderSearch.ts
import { ref, computed } from 'vue'
export function useOrderSearch() {
const keyword = ref('')
const filterStatus = ref(-1)
const filters = computed(() => {
return {
keyword: keyword.value,
status: filterStatus.value
}
})
async function searchOrders(filters) {
// 实际调用接口
return await api.getOrderList(filters)
}
return {
keyword,
filterStatus,
filters,
searchOrders
}
}
在组件中使用:
import { useOrderSearch } from '@/composables/useOrderSearch'
export default defineComponent({
setup() {
const { keyword, filterStatus, filters, searchOrders } = useOrderSearch()
async function handleSearch() {
const orders = await searchOrders(filters.value)
updateTableData(orders)
}
return {
keyword,
filterStatus,
handleSearch
}
}
})
这种“函数式组合”的写法让代码更清晰,也更容易测试和复用。
三、Vite 配置优化构建速度
我们之前用 Webpack,启动时间动辄 20s+,Vite 真的是救星。下面是我们的 vite.config.ts 片段:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [
vue(),
tsconfigPaths(), // 支持 TS Path 映射
],
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
chunkSizeWarningLimit: 1000 // 控制打包体积告警
},
optimizeDeps: {
include: ['element-plus'] // 提前预构建第三方依赖
}
})
我们还用了 Vite 的 Auto Import 插件,减少手动 import。
踩坑经验:那些让我头秃的日子
✅ 1. Pinia 在 SSR 中遇到的问题
项目初期我们尝试接入 Vue 3 + Vite + SSR 渲染(主要是为了 SEO 优化部分营销页),结果发现 Pinia 的初始化在服务端运行会抛异常。
经过排查发现是因为服务器没有 window 对象,而某些 pinia 的 plugin 会访问 window。最终解决方案是将一些浏览器相关的逻辑抽出来,在服务端忽略执行。
💡 解决方案:
// main.ts(客户端)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
// entry-server.ts(服务端)
import { createSSRApp } from 'vue'
const app = createSSRApp(App)
app.use(createPinia()) // 不注册某些插件
return { app }
✅ 2. TypeScript 中 Composable 泛型类型推导失败
我们在封装一个通用表格 hook 时,想传入一个泛型参数表示 item 类型:
function useTable<T>() {
const list = ref<T[]>([])
...
}
但发现在使用的地方无法正确推导出类型,后来发现需要显式地传递类型才能被识别:
// 正确使用
const { list } = useTable<OrderItem>()
// 错误 ❌
const { list } = useTable() // T 默认为 unknown
这点需要注意,Vue 3 的 Composition API 和 TS 结合使用时,很多地方需要显式声明类型。
✅ 3. Element Plus 表格动态列渲染问题
在做一个列拖拽排序的表格配置器时,发现使用 v-if 控制列显示,会导致列宽错乱甚至空白不渲染。
后来发现原因是虚拟滚动机制导致 DOM 并未真正更新。于是改成了使用 v-show 替代 v-if,并在列切换时手动触发 resize:
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:show-overflow-tooltip="true"
v-show="col.show"
/>
并在 change 事件中调用:
nextTick(() => {
tableRef.handleResize() // 手动触发重新渲染
})
效果总结:重构后的收益
重构完成后,我们做了几项性能对比测试:
| 指标 | Vue 2 + Webpack | Vue 3 + Vite |
|---|---|---|
| 构建时间 | 18s ~ 22s | 2.8s ~ 4s |
| 首屏渲染时间 | 3.2s | 1.9s |
| 包大小(压缩后) | 3.1MB | 2.2MB |
| 代码行数 | 15W+ | 12W+ |
重构后的效果非常显著,不仅提升了首屏加载速度,而且代码的结构更加清晰,团队协作效率也提高了。最关键的是,项目变得更加容易扩展,新产品需求可以在两周内快速上线。
经验分享:给前端同行们的建议
如果你也在考虑从 Vue 2 升级到 Vue 3,或者正准备搭建一个新的项目,这里有几个建议希望对你有用:
1. 📌 优先考虑 Composition API 的写法
Composition API 最大的优势不是写法本身,而是让你能更好地组织逻辑代码。比起 Options API,更能体现“关注点分离”,非常适合团队合作和长期维护。
2. ⚙️ 状态管理推荐用 Pinia,而非 Vuex
Vuex 在 Vue 3 中虽然还能用,但已经不再官方主推。Pinia 更轻量、语法更简单,而且可以配合 DevTools 很方便地调试。推荐作为首选状态管理工具。
3. 🔍 性能优化别只看包体积,也要关注“感知性能”
不要一味追求 bundle size,用户体验更重要。比如你可以先渲染骨架屏,再懒加载内容;使用 Suspense 组件提前占位等技巧,让用户感觉更流畅。
4. 🧪 写单元测试,尽早写,越早越好
我们在重构初期没怎么写测试,后来发现一旦涉及公共库改动,影响范围太大,只能靠手动回归测试。后来补上了 Jest + Vue Test Utils 的测试覆盖率,才真正安心。
5. 🔍 浏览器兼容性和调试小贴士
- 使用 caniuse 查看是否支持 ESModule 功能
- 给生产环境加个 Polyfill,比如 core-js 或 unplugin-vue-components 的自动注入
- 推荐安装 Vue Devtools v6(Chrome 插件),支持 Composition API 跟踪和响应式追踪
- 使用 Vue SFC Playground 快速验证模板或逻辑
写在最后:关于技术和成长的一些思考
回顾这段升级 Vue 3 的旅程,我觉得最深的感受就是——技术演进从来都不是一蹴而就的,它需要我们不断学习和拥抱变化。
曾经我也很抗拒新技术,觉得学完就淘汰了怎么办?但现在我更愿意把它当作一种成长的过程。就像这次重构,虽然花了不少时间,但带来的不仅是性能提升,更是思维方式和工程能力上的跃迁。
如果你也遇到了类似的项目重构难题,不妨试试 Vue 3 的这套生态系统。它可能不会立刻解决所有问题,但它一定是一条值得走的路。
当然,欢迎你在评论区分享自己的经历或困惑,我们一起讨论,共同进步!
文末福利:
如果你想了解完整的 Vue 3 + Vite + TypeScript + Pinia 的模板结构,可以关注我的 GitHub 仓库,我会陆续把我们项目中的最佳实践整理出来,欢迎 Star & Fork 😊
Happy Coding!

评论 0