🛠️我把项目从Vue2升级到Vue3:一场技术债清零的逆袭之旅

193 阅读3分钟

目标:一份能直接照着敲的升级手册,覆盖单文件组件、路由、Vuex、UI 库、测试、构建、CI、灰度、回滚所有环节。
阅读方式:左侧目录导航,右侧命令行直接复制。
预计耗时:阅读 30 min,实操 2~4 周


0. 前置知识 & 一句话总结

你原来用的升级后一句话总结
Vue 2.6Vue 3.5先 2.7 再 3.5,减少 breaking
Webpack 4Vite 5开发构建 30 s → 1 s
Vuex 3Pinia 2官方推荐,TS 友好
vue-router 3vue-router 4模式声明方式全变
element-uielement-plus组件名不变,样式前缀变
Jest 27Vitest 2API 95 % 兼容,速度 ×3
Options API<script setup>新代码写新语法,旧代码可不动

1. 升级路线图(时间轴)

周次任务产出物可灰度?
W0(半天)备份 + 评估1. 打 tag vue2-final
2. 生成 migration-report.html
W1工具链迁移1. vite.config.ts 跑通
2. npm run dev 秒开
W2语法自动化1. gogocode 转换 70 %
2. 剩余 30 % 人工
W3依赖大版本1. Router / Vuex → Pinia
2. UI 库对齐
❌(需一次性)
W4测试 + 性能1. 单元测试全绿
2. Lighthouse ≥ 90
W5灰度上线1. 蓝绿发布
2. Sentry 零报警

2. W0:备份与评估(4 h)

2.1 一键备份

# 1. 代码
git tag vue2-final -m "last vue2 commit"
git push origin vue2-final

# 2. 镜像(示例)
docker build -t registry.xxx.com/myapp:vue2-final .
docker push registry.xxx.com/myapp:vue2-final

2.2 生成兼容性报告

npm i -D @vue/compat@3.5
# 临时把 main.js 改成 Vue3 入口
npx vue-compat-report > migration-report.html

浏览器打开报告,重点关注:

  • 红色 ❌ 数量 → 必须改
  • 黄色 ⚠️ 数量 → 运行期警告,可灰度

2.3 建立“降级页面”清单

页面依赖库是否支持 Vue3策略
/old/legacyvue2-editoriframe 嵌入,暂缓
/dashboardecharts 4有 5.x升级

3. W1:构建工具 Webpack → Vite(2 d)

3.1 安装

npm i -D vite @vitejs/plugin-vue2 sass
# 如用 jsx
npm i -D @vitejs/plugin-vue2-jsx

3.2 创建 vite.config.ts(可直接抄)

import { defineConfig } from 'vite'
import vue2 from '@vitejs/plugin-vue2'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue2()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '~': resolve(__dirname, 'node_modules')
    },
    extensions: ['.js', '.jsx', '.vue', '.json', '.ts', '.tsx']
  },
  server: {
    port: 9527,
    open: true
  },
  build: {
    target: 'chrome88',
    outDir: 'dist',
    sourcemap: true
  }
})

3.3 移动 index.html

Webpack 的 html 在 public/index.html,Vite 要求放项目根目录并显式引入入口:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

3.4 处理环境变量

Webpack 写法Vite 等价
process.env.VUE_APP_XXXimport.meta.env.VITE_XXX
如存量太多,可装垫片:
npm i -D vite-plugin-env-compatible

vite.config.ts 加:

import envCompatible from 'vite-plugin-env-compatible'
plugins: [vue2(), envCompatible()]

3.5 脚本替换

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
}

跑一下:

npm run dev

预期:冷启动 < 1 s,浏览器自动打开 localhost:9527


4. W2:语法迁移(Options API → Composition API)

4.1 自动转换(gogocode)

npm i -D gogocode-cli gogocode-plugin-vue
npx gogocode -s ./src -t gogocode-plugin-vue -o ./src-converted

转换率参考:

  • 全局 API(Vue.use / Vue.filter)→ 90 %
  • 生命周期 → 85 %
  • 过滤器 → 95 %
  • 事件总线 → 0 %(需手工)

4.2 手工必改清单

Vue2Vue3备注
this.$on/$off/$once移除用 mitt 或 tiny-emitter
v-model 语法默认 prop 改为 modelValue组件需加 emits: ['update:modelValue']
v-if + v-for优先级变化同节点共存会报警告
key 使用 index可能导致错位一律改为唯一 id
slot="xxx"v-slot:xxx兼容,但推荐新写法
destroyed 生命周期改名 unmountedgogocode 已转

4.3 事件总线替换(实例)

npm i mitt

新建 utils/bus.ts

import mitt from 'mitt'
export default mitt()

组件内:

import bus from '@/utils/bus'
bus.emit('login', user)
bus.on('login', (user) => {})

5. W3:依赖大版本升级

5.1 Vuex 3 → Pinia 2(官方推荐)

npm i pinia
npm un vuex

创建 stores/index.ts

import { createPinia } from 'pinia'
export default createPinia()

main.js 注册:

import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
createApp(App).use(pinia).mount('#app')

把原 store/modules/user.js 改成:

import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({ name: '' }),
  actions: {
    setName(n) { this.name = n }
  }
})

组件使用:

import { useUserStore } from '@/stores/user'
const user = useUserStore()
user.setName('tom')

5.2 vue-router 3 → 4

npm i vue-router@4

router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [/* 原数组 */]
})
export default router

Breaking

  • mode: 'history' 改为 createWebHistory()
  • base 参数移入 createWebHistory('/base/')

5.3 UI 库

安装
element-uielement-plusnpm i element-plus
ant-design-vue4.xnpm i ant-design-vue@4
vant2 → 4npm i vant@4

全局引入(element-plus 示例):

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
createApp(App).use(ElementPlus)

样式前缀变化
el- 保留,但 CSS 变量名 --el-*--ep-* 共存,主题覆盖需检查。


6. W4:测试与性能

6.1 单元测试 Jest → Vitest

npm i -D vitest @vue/test-utils@2 jsdom

vitest.config.ts

import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue2'
export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom'
  }
})

把原 *.spec.jsimport { mount } from '@vue/test-utils' 保留即可,95 % 用例直接绿

6.2 端到端

Cypress 13 已支持 Vue3 组件测试:

npm i -D cypress @cypress/vue

cypress/component/Button.cy.ts

import Button from './Button.vue'
import { mount } from '@cypress/vue'
it('renders', () => {
  mount(Button, { props: { label: 'ok' } })
  cy.contains('ok')
})

6.3 性能基线

npm i -D rollup-plugin-visualizer lighthouse-ci

vite.config.ts

import { visualizer } from 'rollup-plugin-visualizer'
plugins: [vue(), visualizer({ filename: 'dist/stats.html' })]

CI 中增加:

- name: Lighthouse
  run: |
    npm run build
    npx lhci autorun --config=lighthouserc.json

阈值示例(lighthouserc.json):

{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", {"minScore": 0.9}]
      }
    }
  }
}

7. W5:灰度与回滚

7.1 双镜像 Dockerfile

# 阶段 1:编译
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

# 阶段 2:运行
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

构建两条流水线:

docker build -t registry.xxx.com/myapp:vue2-${CI_COMMIT_SHA} .
docker build -t registry.xxx.com/myapp:vue3-${CI_COMMIT_SHA} .

7.2 K8s 蓝绿发布

apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
    version: vue3   # 切换时改这里
  ports:
  - port: 80

回滚只需 kubectl patch service myapp -p '{"spec":{"selector":{"version":"vue2"}}}'

7.3 监控

  • Sentry 8 支持 Vue3 源码 Map,上传命令:
sentry-cli releases files $VERSION upload-sourcemaps dist --url-prefix '~/'
  • Grafana 面板增加 vue_compat_warning_total 指标,>0 立即报警。

8. 超细节踩坑 50 条(2025 年整理)

序号症状原因修复
1控制台 defineEmits is not defined未安装 unplugin-vue-define-optionsnpm i -D unplugin-vue-define-options
2v-model 报错 modelValue is required组件没写 emits: ['update:modelValue']加上
3样式 ~@/assets 找不到Vite 不支持 ~去掉 ~
4Sass /deep/ 警告已废弃替换为 ::v-deep
5require.context('./svg', false, /\.svg$/) 失败Vite 无 webpack APIimport.meta.glob('/src/svg/*.svg', { eager: true })
6globalProperties 在 TS 下无提示未扩充 ComponentCustomProperties见下文
7JSX 组件 h 未定义未自动注入import { h } from 'vue'
8Pinia 页面刷新丢状态未持久化pinia-plugin-persistedstate
9Cypress 找不到组件没装 @cypress/vue
10Lighthouse 性能掉分忘了开 gzipvite-plugin-compression

篇幅所限,完整 50 条放 GitHub 仓库,文末扫码自取。


9. TypeScript 支持(可选但强烈建议)

9.1 添加 TS

npm i -D typescript vue-tsc
npx vue-tsc --init

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    "jsx": "preserve",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["vite/client", "vitest/globals"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

9.2 让 globalProperties 有类型

shims-vue.d.ts

import { ComponentCustomProperties } from 'vue'
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $http: typeof axios
  }
}

10. 一键模板 & 工具地址

名称地址说明
vue3-vite-ts-templategithub.com/xiaogao007/…本文完整模板
gogocode-plugin-vuegithub.com/xiaogao007/…自动转换
vue-compat-reportgithub.com/xiaogao007/…兼容性报告
lighthouse-cigithub.com/xiaogao007/…性能门禁

11. 结语 & 彩蛋

升级完成后,记得:

  1. Dockerfilenode:14 换成 node:20,构建时间再降 20 %。
  2. npm pkg delete devDependencies 清理废弃包,体积 ↓ 20 MB。
  3. npm run dev 的截图甩到群里,享受 1 s 冷启动的快感

如果本文帮到了你,去 GitHub 点个小星星 ⭐,升级过程中有任何疑问,欢迎提 Issue,24 h 内回复。
Happy Vue3!