总体设计
任何框架底层核心都是围绕运行时和编译时设计开发,因此针对 vue 2 项目升级 vue 3,也应该从这两方面入手。
vue 运行时包含调度系统、响应式系统、API 层等,vue 3 相较于 vue 2 做了底层架构上的调整优化,比如在响应式机制上,使用 Proxy 替代之前的 Object.defineproperty,好在大部分修改都是渐进式的,大部分废弃的 API 是 deprecated 而不是 removed,只有极少数比如 filter、destoryed 等属于 removed,这部分需要手动调整,但是可以通过 @vue/compat 来实现 polyfill,来渐进式修改。
除了 Vue 自身外,Vue 还提供插件来增强框架能力,比如 Vuex、Vue-router,或者一些组件库等,在之前都需要通过 Vue.use 来注册,插件内部可能存在只依赖 vue 2 的逻辑语法,因此也需要同步升级到 vue 3 支持的版本。
在编译时方面,主要涉及两部分,一是 Vue 自身的编译器,比如 vue 的 SFC 编译成 js 文件,这就是 Vue 自身的编译器完成的,二是打包工具,Vue 3 之前主流是 vue-cli,当下主流的选择是 Vite,当然前者是必选项,后者是可选项。
总的来说,针对 Vue 2 升级 Vue 3 这一过程,总体设计为围绕运行时和编译时进行升级,在代码层面采用渐进式升级。
升级步骤
[官方升级手册](Migration Build | Vue 3 Migration Guide)已经写得很详细了,核心在于:
- @vue/compiler-sfc 替换 vue-template-compiler 包
- vue 包从 v2 升级到 v3
- 安装@vue/compat
- 升级插件到支持 Vue 3 的版本,比如 Vue-Router、Vuex 等
{
"dependencies": {
- "vue": "^2.6.12",
+ "vue": "^3.1.0",
+ "@vue/compat": "^3.1.0"
...
},
"devDependencies": {
- "vue-template-compiler": "^2.6.12"
+ "@vue/compiler-sfc": "^3.1.0"
}
}之后通过构建工具,将对 vue 的导入改为 @vue/compat,这可以通过别名来实现,以 vue-cli 为例
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.resolve.alias.set('vue', '@vue/compat')
config.resolve.alias.set('vue3', 'vue') // 允许指向真正的vue3
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}@vue/compat 提供了一个兼容层,相当于在 vue3 环境下实现了 vue2 的 polyfill,它使得在 vue3 环境下,也能使用 vue2 中的语法。
此外,还需要注意的是这三个包版本需要保持一致,官方推荐 v3.1.0,但是据笔者实践,3.1.0 这个版本 bug 非常多,建议统一升级到 v3.2.47 这个版本。
如果使用的是 vue-cli,还需要执行 vue upgrade 升级 vue-cli 版本,或者手动将 package. json 中这几个包升级版本:
{
"@vue/cli-plugin-babel": "^5.0.9",
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-plugin-router": "^5.0.9",
"@vue/cli-service": "^5.0.9",
}经过上面操作后,项目应该能够正常跑起来,终端中应该会有很多警告,但是不影响项目启动,接下来只要参考 Vue 3 具体变更逐步修改即可,具体变更内容可以参考[官方文档](Breaking Changes | Vue 3 Migration Guide),重点关注 Removed APIs 这部分。
说明和建议
Vue版本推荐
不建议使用官方文档使用的 v3.1.0 版本,建议使用 v 3.2.47,相较前者更加稳定,bug 更少。vue 和@vue/compat 等包强烈推荐使用统一版本。
@vue/compat
配置
@vue/compat 为从 Vue 2 升级到 Vue 3 过程提供兼容层,兼容 Vue 2 中被废弃的 API,可以通过MODE配置执行行为,支持以下值:
- 1:静默模式 - 兼容但不警告
- 2:警告模式 - 兼容但显示警告(推荐迁移期使用)
- 3:严格模式 - 不兼容,直接报错(用于后期验证)
此外也可以指定单独特性是否启动兼容,这可以和 MODE搭配使用,比如说:
import { configureCompat } from '@vue/compat'
configureCompat({
MODE: 3,
GLOBAL_MOUNT: true // 兼容 new Vue语法
})在这个配置下,默认使用 Vue3 特性,但是启动兼容 new Vue 语法(GLOBAL_MOUNT)。
此外@vue/compat 还支持组件级配置,比如:
export default {
compatConfig: {
MODE: 3, // 默认Vue3
FEATURE_ID_A: true, // 某个特性
},
// ...
}限制
@vue/compat也有一些限制,具体如下:
- 如果老代码有使用到 vue 2 内部未公开的 API,比如 vnode,则可能出问题
- 不支持 IE 11,因为 Vue 3 已经放弃对 IE 的支持
- 不建议用于 SSR 的升级,SSR 建议使用 Nuxt
npm 包
vue 官方包基本都设置了 peerDependencies 来约束版本,因此你安装时可能遇到过这种报错:
npm error Found: vue-router@4.0.0
npm error node_modules/vue-router
npm error vue-router@"4.6.4" from the root project
npm error
npm error Could not resolve dependency:
npm error vue-router@"4.6.4" from the root project
npm error
npm error Conflicting peer dependency: vue@3.5.26
npm error node_modules/vue
npm error peer vue@"^3.5.0" from vue-router@4.6.4
npm error node_modules/vue-router
npm error vue-router@"4.6.4" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
这是因为我本地安装的 vue 版本较低,而现在安装的 vue-router 版本过高导致的,版本不对可能会导致冲突。
不建议使用 npm i --force 等方式绕过,建议按照说明调整你安装的版本,避免导致版本冲突,比如尝试 npm i -S vue-router@4.2 进行降级或者对 vue 进行升级。
Vue. prototype
在Vue 2,Vue 是一个全局共享的类,由于 JS类的原型链机制,我们可以向这个类的原型对象上挂载各种方法,比如:
Vue.prototype.$http = axios
// 使用
this.$http.post这是 vue2 中非常常见的做法,甚至 vue-router、vuex 这些插件过去也是这样做的,但是在 vue 3 中,new Vue 被改为 createApp,不再暴露 Vue 类,这有利于实现状态隔离,但是对于老语法修改起来工作量比较大,有一个技巧可以快速实现渐进式修改。
// Vue2
// Vue.prototype.$axios = request
// Vue.prototype.$utils = utils
// Vue.prototype.$toast = Toast
// Vue3 临时处理
globalThis.$utils = utils
globalThis.$toast = Toast
globalThis.$axios = request
globalThis.$router = router
globalThis.$route = $router.currentRoute思路是先将所有之前挂载原型上的方法,挂载到全局对象上(window),然后全局代码替换(vscode 等编辑器都支持),比如 this.$axios 替换为 $axios,这样能够保证以非常小的修改量让代码先跑起来,然后借助 eslint 的来实现警告,在后续逐渐将全局变量改为局部导入。
Vue-router
Vue-Router 相较于老版本的调整具体可以见官方说明,下面重点讲几个容易被忽视的点。
布尔值不会被自动解析
在老版本,’? a=false’会被自动解析成布尔值,但是新版本不会再自动处理,因此判断逻辑需要调整。
!this.$route.query.a // 不建议
this.$route.query.a === 'true' // 推荐集成 Vite(可选)
步骤
首先安装 vite , 如果只是想在 vite 中使用 vue 2,那么可以安装 vite-plugin-vue2 来支持 vue 2 的 SFC 的解析,但是我们这是将 vue 2 升级到 vue 3,因此需要安装 @vitejs/plugin-vue。
npm i -D vite @vitejs/plugin-vue
之后在根路径下新建 vue.config.js 文件,并将 webpack/vue-cli 配置迁移, 可以参考下面配置。
import { defineConfig } from 'vite'
import path from 'node:path'
import vue from '@vitejs/plugin-vue'
const __dirname = import.meta.dirname
export default defineConfig({
base: '/h5/', // 访问路径前缀
define: {
'process.env': {},
},
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2, // 兼容vue2语法,警告级别
},
},
},
}),
],
server: {
port: 8086,
proxy: {
// 代理,按照实际的配置,和vue-cli配置差不多
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
alias: {
'@': path.join(__dirname, './src'),
'@api': path.join(__dirname, './src/api'),
'@assets': path.join(__dirname, './src/assets'),
'@components': path.join(__dirname, './src/components'),
'@store': path.join(__dirname, './src/store'),
'@utils': path.join(__dirname, './src/utils'),
// vue: '@vue/compat', // 兼容vue2,但是当你vue和插件都升级到vue3版本时,建议删除别名,否则可能会干扰vue3的正常运行
},
},
})
接下来需要处理入口 html,和 webpack 不同的是,vite 的入口 html 在项目根目录,此外还需要注意下面几点:
- 替换 webpack 插值模板语法。
- 用’/‘替换’/public/‘路径
- 添加
<script type="module" src="/src/你的入口文件.js"></script>引入入口 js 文件
参考示例
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width,user-scalable=no,initial-scale=1" name="viewport">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
替换成 Vite 支持的格式
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width,user-scalable=no,initial-scale=1" name="viewport">
<link rel="icon" href="/favicon.ico">
<title>title</title>
</head>
<body>
<noscript>
<strong>We're sorry but doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="module" src="./src/main.js"></script>
</body>
</html>
接下来需要全局处理下面几点
- 由于 vite 和 webpack 对公共资源路径处理规则不同,因此需要将所有的’/public/xx’改成’/xx’
- 将
process改成import
例如:
- export default process.env.VUE_ENV === 'server' // 删除
+ export default import.meta.env.SSR // 新增最后,修改 package. json 的 script 脚本命令。
"dev": "vite",
"build": "vite build"启动测试无误后,删除 vue-cli 相关的包。
问题记录
在配置完毕启动后,出现异常报错:
Uncaught ReferenceError: exports is not defined at vue-router.esm-bundler.js:2306:23从报错中分析:exports 很明显是 Commonjs 的导出语句,由于 Vite 底层基于 ESM 实现预构建,可能是某些配置与其产生冲突,因此按照下面步骤操作:
package.json配置"type": "module"tsconfig. json的target配置项配置成 es 格式(如esnext)- 删除
node_module/. cache中的webpack 缓存 然而重启后仍然报错,怀疑是包本身的问题,升级版本
npm i -S vue-router@4.2.0
升级后,问题解决。
建议(可选)
vite 本身依赖 ESM ,因此建议项目 package.json 配置 "type": "module", 同时将 tsconfig. json 的 target 配置项配置成 es 格式(如 esnext)。