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 中同步修改。此外,sasssass-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.jsthree.meshline 需要加入预构建依赖列表:

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

阶段四:工程化完善

4.1 自动导入配置

引入 unplugin-auto-importunplugin-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-vuevue 版本存在依赖差。

解决 :确保 @vue/compiler-sfcvue 的版本号匹配(同一主版本下的 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:devwebpack:build 脚本,万一 Vite 构建出问题,可以随时切回 Webpack 发布。

  4. 关注第三方依赖的 Vite 兼容性 。像 @jiaminghi/data-viewthree.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 技术栈升级过程的一次系统性复盘,总结了我们在迁移中的关键决策、遇到的问题与解决思路。如果存在纰漏和不合理之处,欢迎在评论区交流指出。

相关推荐
前端拷贝猿1 小时前
扫码领券功能需求分析
前端
前端拷贝猿1 小时前
设备活动弹窗功能需求分析
前端
飞天狗1111 小时前
零基础JavaWeb入门——第五课第一小节:九大内置对象 · 第1个:request(请求对象)
java·开发语言·前端·后端·servlet
a15108416931 小时前
记一次大模型探索
java·服务器·前端
石山代码1 小时前
变量与解构
开发语言·前端·javascript
888CC++2 小时前
箭头函数(ES6)
前端·javascript·es6
qq_419854052 小时前
css filter
前端·javascript·css
Agatha方艺璇2 小时前
VUE复习笔记
前端·vue.js
大家的林语冰2 小时前
npm 不忍了,正式上线“阶段式发布“的新功能,进一步对抗频繁的供应链攻击!
前端·javascript·node.js