Vue3组合式API实战:我如何把一个老项目重构到Vue3并实现跨端小程序开发

从一个让人头疼的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/

(0)
小编小编
上一篇 17小时前
下一篇 17小时前

相关推荐