Vue2 升级 Vue3 项目实战

Vue2 升级 Vue3 项目实战:从 Webpack 到 Vite 的全链路迁移指南

前言

上半年,我所在的团队对一个中大型 IoT 设备管理平台进行了技术栈升级。这个项目最初基于 Vue 2 + Vue CLI(Webpack)构建,随着业务规模增长,开发体验和构建性能的瓶颈日益突出:冷启动时间长、HMR 速度慢、生产构建内存溢出......于是我们启动了向 Vue 3 + Vite 的全链路迁移。

整个升级覆盖了路由、状态管理、全局 API、组件库、构建工具、CSS 预处理器等多个维度。本文将完整复盘整个升级过程,记录总结。


升级路线图

我们将整个升级拆分为四个阶段,按依赖关系依次推进:

复制代码
阶段一       → 组件级重构:Options API → Composition API
阶段二       → 构建配置优化:node-sass → Dart Sass
阶段三       → 构建工具迁移:Webpack → Vite
阶段四       → 工程化完善:自动导入、类型声明

下面逐一展开。


阶段一:组件级 Vue 3 化重构

1.1 为什么先做组件重构?

在迁移构建工具之前,我们先花了几个月时间把几乎所有页面从 Options API 重写为 Composition API。这样做的好处是:

  • 降低耦合:逻辑按功能拆分,不再散落在 data/methods/computed 中
  • 问题诊断维度隔离:如果先切构建工具,代码中还混着 Vue 2 时代的模式(Vue.prototype、Options API 等),构建异常时难以区分是工具配置问题还是代码兼容性问题。先在代码层面完成 Vue 3 化,Vite 迁移阶段只涉及构建配置变更,排查路径清晰可控
  • 渐进式交付:每个页面独立重构、独立测试、独立上线,风险可控

1.2 入口文件改造

Vue 3 不再使用 new Vue(),取而代之的是 createApp 的链式 API:

升级前(Vue 2 + Options API 风格):

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import ElementUI from 'element-ui'

Vue.use(ElementUI)
Vue.use(VueRouter)
Vue.use(Vuex)

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

升级后(Vue 3 + createApp):

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

// 全局注册 Element Plus
app.use(ElementPlus)

// 全局注册图标组件
import * as Icons from '@element-plus/icons-vue'
Object.keys(Icons).forEach((key) => {
  app.component(key, Icons[key])
})

// 状态管理从 Vuex 迁移到 Pinia
const pinia = createPinia()
app.use(pinia)

// 事件总线从 Vue.prototype.$bus 迁移到 mitt
import mitt from 'mitt'
app.config.globalProperties.$Bus = mitt()

// 全局挂载 axios 替代 Vue.prototype.$http
import axios from 'axios'
app.config.globalProperties.$axios = axios

app.use(router)
app.mount('#app')

1.3 路由迁移

升级前 :使用 VueRouter 构造函数 + new

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'hash',
  routes: [...]
})

升级后 :使用 createRouter 工厂函数

javascript 复制代码
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [...]
})

// 路由守卫中使用 Pinia Store
import { useAppStore } from '@/store/app'

router.beforeEach((to, from, next) => {
  const store = useAppStore()
  // 权限校验逻辑...
})

1.4 组件重构示例

以一个设备列表页面为例,展示从 Options API 到 Composition API 的转变:

升级前:

vue 复制代码
<script>
export default {
  data() {
    return {
      tableData: [],
      loading: false,
      queryParams: {
        page: 1,
        pageSize: 20,
        keyword: ''
      }
    }
  },
  computed: {
    filteredData() {
      return this.tableData.filter(item =>
        item.name.includes(this.queryParams.keyword)
      )
    }
  },
  mounted() {
    this.fetchData()
  },
  methods: {
    async fetchData() {
      this.loading = true
      try {
        const res = await getDeviceList(this.queryParams)
        this.tableData = res.data
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

升级后:

vue 复制代码
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { getDeviceList } from '@/api/device'

const tableData = ref([])
const loading = ref(false)

const queryParams = reactive({
  page: 1,
  pageSize: 20,
  keyword: ''
})

const filteredData = computed(() =>
  tableData.value.filter(item =>
    item.name.includes(queryParams.keyword)
  )
)

async function fetchData() {
  loading.value = true
  try {
    const res = await getDeviceList(queryParams)
    tableData.value = res.data
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchData()
})
</script>

阶段二:构建配置优化

2.1 node-sass → Dart Sass

node-sass 已于 2020 年被官方宣布弃用,且对 Node.js 版本有严格依赖。我们将项目迁移到 sass(Dart Sass 的纯 JavaScript 实现)。

diff 复制代码
// package.json
 {
   "devDependencies": {
 -   "node-sass": "^7.0.0",
 -   "sass-loader": "^7.3.1",
 +   "sass": "^1.33.0",
 +   "sass-loader": "^10.2.0",
   }
 }

注意 sass-loader 从 7.x 升级到 10.x 时,prependData 选项已更名为 additionalData,需要在 vue.config.js 中同步修改。此外,sass 和 sass-loader 均为构建时依赖,统一放在 devDependencies 中。

2.2 Babel 插件补充

为了兼容 Vue Router v4 中 .mjs 文件的可选链语法(?.),增加了两个 Babel 插件:

javascript 复制代码
// vue.config.js - chainWebpack 配置
config.module
  .rule('mjs')
  .test(/\.mjs$/)
  .include.add(/node_modules/)
  .end()
  .type('javascript/auto')
  .use('babel-loader')
  .loader('babel-loader')
  .options({
    presets: ['@vue/cli-plugin-babel/preset'],
    plugins: [
      '@babel/plugin-proposal-optional-chaining',
      '@babel/plugin-proposal-nullish-coalescing-operator',
    ],
  })

阶段三:Webpack → Vite 构建工具迁移

这是整个升级中最核心也是最复杂的一步,涉及整个项目的文件变更。

3.1 package.json 变更

diff 复制代码
{
  "scripts": {
-   "dev": "vue-cli-service serve",
-   "build": "vue-cli-service build",
+   "dev": "vite",
+   "build": "NODE_OPTIONS='--max-old-space-size=2048' vite build",
+   "preview": "vite preview",
+   "webpack:dev": "vue-cli-service serve",
+   "webpack:build": "vue-cli-service build"
  },
  "devDependencies": {
+   "@vitejs/plugin-vue": "^4.6.2",
+   "@vue/compiler-sfc": "^3.5.28",
+   "vite": "^4.5.14",
+   "vite-plugin-compression": "^0.5.1",
+   "vite-plugin-require-transform": "^1.0.21",
+   "rollup-plugin-visualizer": "^5.12.0",
  }
}

说明几个关键点:

  • 保留 webpack 脚本作为回退方案,降低迁移风险
  • 构建命令增加 --max-old-space-size=2048:由于项目依赖较多(Three.js、ECharts 等),生产构建时需要更大内存
  • vite-plugin-require-transform :用于兼容老代码中残留的 require() 语法

3.2 vite.config.js 完整配置

将 vue.config.js(CommonJS 格式)迁移为 vite.config.js:

javascript 复制代码
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import viteCompression from 'vite-plugin-compression'
import { visualizer } from 'rollup-plugin-visualizer'
import requireTransform from 'vite-plugin-require-transform'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())

  return {
    base: mode === 'production' ? './' : '/',

    plugins: [
      vue(),
      // 兼容老代码中的 require() 语法
      requireTransform({
        fileRegex: /src\/.*\.(js|vue)$/,
      }),
      // 生产环境 Gzip 压缩
      mode === 'production' &&
      viteCompression({
        verbose: true,
        disable: false,
        threshold: 10240,
        algorithm: 'gzip',
        ext: '.gz',
      }),
      // 生产环境打包分析
      mode === 'production' &&
      visualizer({
        open: true,
        gzipSize: true,
        brotliSize: true,
        filename: 'dist/report.html',
      }),
    ],

    resolve: {
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
      alias: {
        '@': path.resolve(__dirname, 'src'),
        // 修复 Element Plus 语言包路径
        'element-plus/lib/locale/lang/zh-cn':
          'element-plus/es/locale/lang/zh-cn.mjs',
        'element-plus/lib/locale/lang/en':
          'element-plus/es/locale/lang/en.mjs',
      },
    },

    server: {
      host: '0.0.0.0',
      port: 7070,
      open: true,
      hmr: true,
      proxy: {
        '/api': {
          target: 'http://127.0.0.1:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

    build: {
      outDir: 'dist',
      assetsDir: 'static',
      sourcemap: false,
      commonjsOptions: {
        transformMixedEsModules: true,
      },
    },

    define: {
      __VUE_I18N_FULL_INSTALL__: true,
      __VUE_I18N_LEGACY_API__: true,
      __INTLIFY_PROD_DEVTOOLS__: false,
    },

    css: {
      preprocessorOptions: {
        scss: {
          silenceDeprecations: ['legacy-js-api'],
        },
      },
    },
  }
})

3.3 Webpack 配置对照表

功能 Webpack (vue.config.js) Vite (vite.config.js)
路径别名 configureWebpack.resolve.alias resolve.alias
开发服务器 devServer server
代理 devServer.proxy server.proxy
Gzip 压缩 compression-webpack-plugin vite-plugin-compression
打包分析 webpack-bundle-analyzer rollup-plugin-visualizer
环境变量 process.env.NODE_ENV loadEnv(mode, ...)
CSS 预处理 css.loaderOptions.sass css.preprocessorOptions.scss
输出目录 outputDir build.outDir
公共路径 publicPath base

3.4 require → ESM import 改造

Vite 基于原生 ESM,不支持 Webpack 的 require.context。路由自动注册是最典型的改造场景。

Webpack 版本:

javascript 复制代码
const requireComponent = require.context('../views', true, /\.vue$/)
const names = requireComponent.keys()

names.forEach(name => {
  const componentConfig = requireComponent(name)
  // 注册路由...
})

Vite 版本:

javascript 复制代码
const modules = import.meta.glob('../views/**/*.vue', { eager: true })

for (const path in modules) {
  const componentConfig = modules[path]
  // 注册路由...
}

3.5 静态资源处理

Vite 中 index.html 必须放在项目根目录,不再是 public/ 目录下:

arduino 复制代码
- public/index.html     →  index.html(根目录)

同时,index.html 中需要显式引入入口 JS:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>IoT 设备管理平台</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

3.6 第三方库兼容处理

部分老旧的第三方库在 Vite 中需要特殊处理:

DataV 大屏组件 :原 @jiaminghi/data-view 不兼容 Vite,替换为社区维护的 @kjgl77/datav-vue3:

diff 复制代码
- import dataV from '@jiaminghi/data-view'
+ import DataV from '@kjgl77/datav-vue3'

Three.js :three.meshline 需要加入预构建依赖列表:

javascript 复制代码
optimizeDeps: {
  include: ['immer', 'qrcodejs2-fixes', 'three.meshline'],
}

阶段四:工程化完善

4.1 自动导入配置

引入 unplugin-auto-import 和 unplugin-vue-components 实现 API 和组件的自动导入,彻底告别手动 import:

javascript 复制代码
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// 在 vite.config.js 的 plugins 中添加
AutoImport({
  imports: [
    'vue',
    'vue-router',
    'pinia',
    'vue-i18n',
    { '@/lang': ['t'] }  // 国际化函数自动导入
  ],
  resolvers: [ElementPlusResolver()],
  dts: 'src/auto-imports.d.ts',  // 生成类型声明
  vueTemplate: true,              // 模板中也支持自动导入
}),

Components({
  dirs: ['src/components'],
  extensions: ['vue'],
  resolvers: [ElementPlusResolver()],
  dts: 'src/components.d.ts',
}),

配置后,组件中无需再手动导入 Vue API:

vue 复制代码
<script setup>
// 不再需要这些 import
// import { ref, computed, onMounted } from 'vue'
// import { ElMessage } from 'element-plus'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function handleClick() {
  ElMessage.success('操作成功')  // 自动导入
}

onMounted(() => {
  // 自动导入
})
</script>

4.2 Vue I18n 配置

由于项目中使用了 Vue I18n v9 的 Legacy API 模式,需要在 Vite 中设置特性标志:

javascript 复制代码
define: {
  __VUE_I18N_FULL_INSTALL__: true,
  __VUE_I18N_LEGACY_API__: true,
  __INTLIFY_PROD_DEVTOOLS__: false,
}

遇到的坑与解决方案

坑 1:生产构建 ESLint 校验警告

现象 :Vite 构建时报 'xxx' is declared but its value is never read 警告。

原因 :使用了 unplugin-auto-import 后,部分 Vue API 在 .vue 文件中被自动导入,但 ESLint 无法识别,认为变量未使用。

解决:

  1. 生成 auto-imports.d.ts 类型声明文件供 TypeScript/ESLint 识别
  2. 在 ESLint 配置中扩展 auto-imports 的 globals

坑 2:@vue/compiler-sfc 版本不匹配

现象:Vite 构建时出现编译错误。

原因 :@vitejs/plugin-vue 和 vue 版本存在依赖差。

解决 :确保 @vue/compiler-sfc 与 vue 的版本号匹配(同一主版本下的 minor 版本必须一致):

json 复制代码
{
  "devDependencies": {
    "vue": "^3.2.47",
    "@vue/compiler-sfc": "^3.2.47",
    "@vitejs/plugin-vue": "^4.6.2",
    "vite": "^4.5.14"
  }
}

注意:从 Vue 3.3 开始,@vue/compiler-sfc 已内置于 vue 主包中,无需单独安装。如果项目使用的 Vue 为 3.3+,可直接移除该依赖项。

坑 3:require 语法残留

现象 :部分老代码中存在 require() 调用,Vite 无法直接处理。

解决 :使用 vite-plugin-require-transform 插件作为过渡方案,同时逐步将 require 改为 import:

javascript 复制代码
import requireTransform from 'vite-plugin-require-transform'

// vite.config.js
plugins: [
  requireTransform({
    fileRegex: /src\/.*\.(js|vue)$/,
  }),
]

升级效果对比

指标 升级前 (Webpack) 升级后 (Vite) 提升
冷启动时间 ~45s ~3s 15x
HMR 热更新 ~2-5s <100ms 20-50x
生产构建时间 ~180s ~45s 4x
构建内存占用 OOM 风险(需手动限制) 正常(8GB 上限) 稳定
开发体验 修改代码后等待 即时生效 显著提升

总结与建议

总结了升级过程中的经验教训,几点经验值得分享:

  1. 分期推进,而非大爆炸式重构。四个阶段各有明确的交付物和验证标准,每个阶段都可以独立上线,降低了风险。

  2. 先做组件级重构,再做构建工具迁移。把 Options API → Composition API 的改造放在第一阶段,一方面让团队熟悉 Vue 3 生态,另一方面 Composition API 的纯 ESM 导入风格天然适配 Vite,后续迁移更顺畅。

  3. 保留回退路径 。在 package.json 中保留了 webpack:dev 和 webpack:build 脚本,万一 Vite 构建出问题,可以随时切回 Webpack 发布。

  4. 关注第三方依赖的 Vite 兼容性 。像 @jiaminghi/data-view、three.meshline 这类老旧库可能存在兼容问题,需要提前调研替代方案或配置 optimizeDeps。

  5. ESLint / TypeScript 配置要及时跟进 。引入 unplugin-auto-import 后,ESLint 会报变量未使用的警告,需要生成类型声明文件并更新 ESLint 配置。

  6. 利用 Vite 的插件生态 。vite-plugin-compression(Gzip)、rollup-plugin-visualizer(打包分析)、unplugin-vue-components(组件自动导入)都是开箱即用的,不需要像 Webpack 那样手写复杂的 chain 配置。

本文主要是对 Vue 技术栈升级过程的一次系统性复盘,总结了我们在迁移中的关键决策、遇到的问题与解决思路。如果存在纰漏和不合理之处,欢迎在评论区交流指出。

相关推荐
派小心.2 分钟前
DAU和MAU怎么计算?先确定活跃事件、用户键和统计窗口
大数据·服务器·前端·人工智能·数据分析
代码猎人2 分钟前
前端怎么配合后端完成bug修复
前端
定栓6 分钟前
Webpack迁到Vite第七节(四):全家桶移除 — 保留项、脚本与数据收尾
前端·webpack·node.js
代码猎人14 分钟前
前端怎么保障项目稳定上线与运行
前端
数据掘金15 分钟前
小程序埋点方案的埋点文档怎么自动生成?
前端
deli00715 分钟前
cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂
前端
代码猎人19 分钟前
前端怎么优化页面性能
前端
代码猎人21 分钟前
前端怎么配合后端完成数据对接
前端
lexiangqicheng27 分钟前
Docker的沙箱机制是怎样的?
前端·javascript
燕落南枝1 小时前
HTML 表格标签与 AI 合并单元格实战指南
前端·人工智能·html