从一个让人头疼的Vue2老项目说起
去年公司接了一个政务类SaaS平台,前端是两年前用Vue2 + Element UI搭的,Options API写出来的组件动不动就上千行,data、methods、computed、watch散落在不同区块,改一个功能要在文件里来回跳转十几回。新人接手代码的时候一脸懵,我花了整整两天才把某个权限模块的逻辑理清楚。那会儿我就下定决心——这个项目必须重构到Vue3。
说干就干,但重构从来不是换个CDN链接那么简单。从Options API到Composition API的思维转变、TypeScript的类型体操、跨端小程序的适配、构建工具从Webpack迁移到Vite,每一步都是坑。这篇文章就是我踩完坑之后的完整复盘,希望对你有帮助。
Composition API vs Options API:思维方式的根本转变
先说最核心的变化。Vue2的Options API按选项类型组织代码,data里放状态、methods里放方法、computed里放计算属性。看起来井井有条,但一旦组件逻辑变复杂,相关的代码就被拆到不同区块,维护起来像在玩拼图。
Composition API的核心思路是按逻辑关注点组织代码,而不是按选项类型。同一个功能的state、方法、计算属性可以放在一起,抽出成composable函数后甚至可以跨组件复用。看个对比:
// Vue2 Options API — 一个权限模块的代码散落在各处
export default {
data() {
return {
userList: [],
roleList: [],
currentUserRole: '',
permissionMap: {}
}
},
computed: {
filteredUsers() { /* ... */ },
hasPermission() { /* ... */ }
},
methods: {
fetchUsers() { /* ... */ },
fetchRoles() { /* ... */ },
checkPermission() { /* ... */ }
},
watch: {
currentUserRole() { /* ... */ }
}
}
// Vue3 Composition API — 同一个模块的逻辑聚在一起
function usePermission() {
const userList = ref([])
const roleList = ref([])
const currentUserRole = ref('')
const permissionMap = ref({})
const filteredUsers = computed(() =>
userList.value.filter(u => u.role === currentUserRole.value)
)
const hasPermission = (action) =>
permissionMap.value[currentUserRole.value]?.includes(action) ?? false
const fetchUsers = async () => {
userList.value = await api.getUsers()
}
const fetchRoles = async () => {
roleList.value = await api.getRoles()
}
watch(currentUserRole, () => fetchUsers())
return { userList, roleList, currentUserRole, filteredUsers, hasPermission, fetchUsers, fetchRoles }
}
// 组件里直接用,干净利落
export default defineComponent({
setup() {
const { hasPermission, fetchUsers } = usePermission()
onMounted(() => fetchUsers())
return { hasPermission }
}
})
你看到区别了吧——Options API里你要在data、methods、computed、watch之间反复跳转才能理解一个完整的权限逻辑,而Composition API把所有相关的代码都收进了usePermission这个函数里。组件本身变得极度简洁,逻辑关注点一目了然。我重构完权限模块后,那个组件从1200行砍到了300行,新增功能的时候再也不用翻来翻去了。
迁移策略:增量重构,不是一次性推翻重来
很多人一听重构就想全推翻重写,千万别这么干。我的做法是增量迁移:先装上@vue/compat兼容层,让Vue2代码在Vue3环境下也能跑;然后逐个模块用Composition API重写,每改完一个模块就跑一遍测试,确保没引入新bug。
// vite.config.ts — 开启兼容模式
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: { MODE: 2 } // Vue2兼容模式
}
}
})
],
resolve: {
alias: {
vue: '@vue/compat'
}
}
})
迁移顺序我建议从叶子组件开始——那些没有子依赖的展示型组件,改完风险最低。然后逐步往上层走,最后处理根组件和路由层。整个过程我用了三周,平均每天迁移4-5个组件,过程中没出过线上事故。
TypeScript集成:类型安全带来的不只是少写注释
Vue3对TypeScript的支持是原生级别的,不像Vue2时代要靠vue-class-component这种装饰器勉强凑合。配合Volar插件,模板里的类型推导、ref的自动解包、props的类型校验,体验非常顺滑。
// 定义严格的API响应类型
interface UserVO {
id: number
name: string
role: 'admin' | 'editor' | 'viewer'
permissions: string[]
createdAt: string
}
interface ApiResponse<T> {
code: number
data: T
message: string
}
// composable函数带完整类型推导
function useUserList() {
const users = ref<UserVO[]>([])
const loading = ref(false)
const fetchUsers = async (): Promise<void> => {
loading.value = true
try {
const res: ApiResponse<UserVO[]> = await request.get('/api/users')
if (res.code === 0) {
users.value = res.data
}
} finally {
loading.value = false
}
}
return { users, loading, fetchUsers }
}
说实话,刚加TypeScript的时候确实慢,到处标类型、跟any做斗争。但等核心类型定义沉淀下来之后,新功能开发反而更快了——IDE的自动补全太香了,接口字段改了立马报红,不用等到运行时才发现字段名拼错。我们团队统计过,上线后因为接口字段不匹配导致的bug下降了大概70%。
uni-app + Vue3:一套代码跑通H5和小程序
项目需求里有一项是微信小程序端也得有,当时团队人手不够,不可能再拉一组人写原生小程序。我选了uni-app + Vue3的方案,Web端和小程序端共用核心逻辑层,只在视图层做差异化适配。
// shared/composables/useCart.ts — 购物车逻辑,Web和小程序共用
function useCart() {
const cartItems = ref<CartItem[]>([])
const totalPrice = computed(() =>
cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const addItem = (item: CartItem) => {
const existing = cartItems.value.find(i => i.skuId === item.skuId)
if (existing) {
existing.quantity += item.quantity
} else {
cartItems.value.push(item)
}
// 跨端存储适配
uni.setStorageSync('cart', JSON.stringify(cartItems.value))
}
const loadCart = () => {
const stored = uni.getStorageSync('cart')
if (stored) {
cartItems.value = JSON.parse(stored)
}
}
return { cartItems, totalPrice, addItem, loadCart }
}
这里有个关键决策:composable函数全放shared目录,Web和小程序页面各自引用。视图层通过条件编译处理平台差异:
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">
微信授权登录
</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button @click="onWebLogin">
账号密码登录
</button>
<!-- #endif -->
最终我们做到了85%的代码复用率,Web端和微信小程序端共享同一套业务逻辑,只在UI渲染和平台特有API处做区分。如果后续要加支付宝小程序或抖音小程序,改动量也非常小。
Web性能优化:首屏从3秒到800毫秒
老项目首屏加载时间3.2秒,Lighthouse评分42分,这数据在政务系统里简直是灾难。重构过程中我从四个维度做了优化:
第一,路由级代码分割。Vue2项目用的是Webpack的require.ensure,配置繁琐而且tree-shaking效果差。Vue3 + Vite下直接用动态import,Vite自动做分割:
// router/index.ts
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue') // 自动代码分割
},
{
path: '/user-management',
component: () => import('@/views/UserManagement.vue')
},
{
path: '/permission',
component: () => import('@/views/Permission.vue')
}
]
第二,虚拟列表替换全量渲染。权限管理页面有上万个用户数据,Vue2版本直接v-for全量渲染,DOM节点炸裂。重构后用了vue-virtual-scroller,DOM节点始终控制在可视区域范围内:
<RecycleScroller
:items="userList"
:item-size="56"
key-field="id"
v-slot="{ item }"
>
<UserRow :user="item" />
</RecycleScroller>
第三,图片和静态资源优化。Vite构建时配置图片压缩和hash命名,配合CDN缓存策略,重复访问几乎零网络请求:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-ui': ['element-plus'],
'vendor-utils': ['lodash-es', 'dayjs']
}
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
第四,请求预取与骨架屏。在路由跳转之前预取下一页数据,同时用骨架屏消除白屏感:
// 全局路由守卫里预取数据
router.beforeEach(async (to) => {
if (to.meta.prefetch) {
const fetchFn = to.meta.prefetch as () => Promise<void>
await fetchFn()
}
})
优化完之后,首屏加载时间降到了820ms,Lighthouse评分升到89分。业务方反馈”感觉像换了个系统”,这个评价让我挺有成就感的。
组件库设计:基于Element Plus的二次封装
政务系统有大量表单和表格,Element Plus本身很好用但直接裸用会导致每个页面的样式和逻辑都不统一。我的做法是在Element Plus之上封装业务组件库,统一处理校验规则、请求状态、权限控制等横切关注点。
// components/BizTable/index.ts
function useBizTable<T>(fetchApi: () => Promise<ApiResponse<{ list: T[]; total: number }>>) {
const data = ref<T[]>([])
const total = ref(0)
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 20 })
const refresh = async () => {
loading.value = true
try {
const res = await fetchApi()
if (res.code === 0) {
data.value = res.data.list
total.value = res.data.total
}
} finally {
loading.value = false
}
}
watch(pagination, refresh, { deep: true })
onMounted(refresh)
return { data, total, loading, pagination, refresh }
}
这样每个业务页面只需要传入API函数,表格的数据加载、分页、loading状态全自动处理。以前写一个带分页的表格页面要200行,封装后50行搞定。关键是行为一致——所有表格的空数据提示、加载状态、分页交互完全统一,不用再为”这里loading没加””那里分页不对”这种破事开bug单了。
Vite构建优化:开发体验的质变
最后说说构建工具。从Webpack切到Vite是整个重构里体验提升最明显的一步。Webpack冷启动要40多秒,改一行代码热更新等3秒;Vite冷启动2秒,热更新几乎是即时的。开发幸福感直线上升。
但Vite的生产构建也有一些需要注意的地方。我踩过的坑包括:动态import的glob模式写法不对导致代码分割失效、CommonJS依赖没做好预构建导致dev模式报错、还有terser配置不当时构建时间反而变长。这里贴一份比较完善的Vite配置片段:
// vite.config.ts 完整优化配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
// Element Plus按需导入,不用全量引入
Components({
resolvers: [ElementPlusResolver()]
}),
// 构建体积分析
visualizer({ open: false, gzipSize: true })
],
build: {
target: 'es2020',
cssCodeSplit: true,
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('element-plus')) return 'vendor-element'
if (id.includes('echarts')) return 'vendor-echarts'
return 'vendor'
}
}
}
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
其中Element Plus按需导入这个改动效果很显著——全量引入时element-plus的CSS和JS加起来有800多KB,按需导入后只打包用到的组件,体积降到150KB左右。
写在最后
整个Vue3重构项目历时两个月,从最初的方案调研到最后的灰度上线,中间踩了无数的坑。但回头看效果:代码量减少40%,构建速度提升20倍,首屏加载降到820ms,TypeScript让线上bug率降了七成,uni-app方案让我们用一套逻辑覆盖了Web和微信小程序两端。
如果你也在犹豫要不要把老项目迁到Vue3,我的建议是——别等了。用增量迁移的方式,风险完全可控,而收益是实实在在的。Composition API不是语法糖,它改变的是你组织代码的方式;TypeScript不是负担,它给你的是长期维护的安全感;Vite不是噱头,它实实在在把开发体验拉到了另一个档次。
对了,重构过程中我整理了一份完整的迁移checklist和踩坑记录,如果有需要可以留言,我后续会整理发布出来。前端这条路,咱们一起走。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-wo-ru-he-ba-yi-ge-lao-xiang-mu/