Vue2升级Vue3实践

Vue2升级Vue3实践

一、升级前准备

在正式升级前,可以提前处理一些已兼容的小修改以及被移除的API。

deep 样式穿透

历史所有的 >>> || /deep/ || ::v-deep 样式穿透,更改为 :deep()

在 Vue 2.7 中,运行项目会提示 deep 相关问题,虽然项目还能正常启动,但会在控制台报警告信息。

// 历史写法
/deep/ .el-card__body {
  padding: 20px 20px 0;
}

// 新版写法
:deep(.el-card__body){
  padding: 20px 20px 0;
}

inline-template 属性

移除 inline-template 标识,在 Vue 3 中,inline-tempalte 属性将会被移除,不再支持该用法了,如果必须使用可用 <script> 或者默认 Slot 替代。

slot 插槽

Vue 3 中引入了一个新的指令 v-slot ,用来表示具名插槽和默认插槽(Vue2.6 中已支持)。如果在项目中仍然使用废弃的具名/作用域插槽语法,请先将其更新至最新的语法。

v-bind

在 Vue 2 中,如果一个元素同时定义了 v-bind="object" 和一个相同的单独的属性,那么这个单独的属性总是会覆盖 object 中的绑定。

在 Vue 3 中,v-bind=“object" 是顺序敏感的,声明绑定的顺序决定了它们如何合并,需确保 v-bind 先定义,再定义各个属性。

// Vue 2
<div id="aaa" v-bind="{ id: 'bbb' }"></div>
<div id="bbb"></div> // 渲染结果

// Vue 3
<div id="aaa" v-bind="{ id: 'bbb' }"></div>
<div id="bbb"></div> // 渲染结果
<div v-bind="{ id: 'bbb' }" id="aaa"></div>
<div id="aaa"></div> // 渲染结果

v-for

筛查所有 v-for 中使用 ref 的地方,将 ref 绑定为一个函数。

在 Vue 2 中,在 v-for 语句中使用 ref 属性时,会生成 refs 数组插入 $refs 属性中。

在 Vue 3 中,在 v-for 语句中使用 ref 属性时,将不再会自动在 $refs 中创建数组。而是将 ref 绑定到一个 function 中,在 function 中可以灵活处理 ref

keyCodes

在 Vue 2 中,使用数字 (即键码) 作为 v-on 的修饰符。

在 Vue 3 中,弃用了 keyCodes,可以更改为别名作为 v-on 的修饰符(Web标准中 KeyboardEvent.keyCode 已被废弃)。

<input v-on:keyup.13="submit" /> // 已弃用
<input v-on:keyup.enter="submit" />

Data 选项

在 Vue 2 中 ,声明 data 支持对象形式 || 函数形式。

在 Vue 3 中,对 data 的声明进行了标准化,只支持函数形式声明。

filters 过滤器

在 Vue 3 中,移除且不再支持 filters,如果之前项目中需要实现过滤功能,可以通过 computedmethods 实现。

如果需要使用全局过滤器,可以借助 globalProperties 来注册全局过滤器。

// lib/format.js
export default {
  formatCooperateStatus (status) {
    const map = {
      applied: '待支援',
      assigning: '指派中',
      partialRefuse: '重新指派',
      process: '协作中',
      refuse: '已退回',
      tested: '协作完成'
    }
    return map[status] || ''
  }
}

// main.js
// Vue 2
import Format from './lib/format.js'
Vue.prototype.$format = Format
// Vue 3
import Format from './lib/format.js'
const app = createApp(App)
app.config.globalProperties.$format = Format

watch 监听器

筛查所有 watch 监听,如果监听参数为数组,需要设置 deep: true

在 Vue 3 中,只有当数组被替换时,回调才会触发,如果想要数组在发生改变时被 Vue 识别到,则必须指定 deep 选项。

watch: {
  checkedMethod: {
    handler (curValue) {
      this.onChange(curValue)
    },
    deep: true,
    immediate: true
  }
}

$children

在 Vue 3 中,移除了 $children 实例,如果需要访问子组件实例可用 $refs 实现。

$destroy

在 Vue 3 中,移除了 $destroy 实例,不应该手动管理单个 Vue 组件的生命周期。

二、构建工具

Vue CLI 迁移为 Vite,Vue 3 推荐使用 Vite 作为构建工具,Vite 缩短了开发服务器的启动时间,它实现了按需编译,不再需要等待整个应用编译完成。

PS: Vite 需要 Node.js 版本 >= 12.0.0

迁移过程

1. 安装
npm i vite -g
2. 构建新的vite项目
npm create vite@latest
cd xxx
npm install
npm run dev

构建的初始项目结构如下:

在这里插入图片描述

用原始项目的 src 文件替换新构建项目的 src 目录,然后再继续接下来的重构操作。

3. 迁移package文件及配置项

根据项目需求迁移 package.json文件 ,并移除 Vue CLI 相关依赖项。

初始文件如下,Vite 默认构建的项目为 Vue 3 项目。

在这里插入图片描述

修改 vite.config.js 文件,vue.config.js --> vite.config.js

初始文件配置如下

在这里插入图片描述

修改文件配置,例:

import { resolve } from 'path'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {  // 配置路径别名
      '@': resolve('src')
    },
  },
  server: {
    open: true,
    host: XXX,
    port: XXX,
    https: false,
    proxy: {
      '/api': {
        target: XXX,
        secure: false,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
    '/casefiles': {
      target: XXX,
      secure: false,
      changeOrigin: true
    }
})
4. 更新环境变量

process.env --> import.meta.env

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。

5. 文件引入

@vue/cli 中支持无扩展名的 vue文件导入,加不加 .vue 后缀都会被正确识别。

Vite 的设计中,import xxx from "./xxx.vue" 才能正确导入,必须确保单个文件组件的所有导入都以扩展名结尾。

6. 修改CommonJS语法

Vite 使用 ES Modules 作为模块化方案,因此不支持使用 require 方式来导入模块。

三、HTML 文件与入口文件

1. index.html

./public/index.html --> ./index.html

Vite 项目的 HTML文件 是放在项目根目录下的,同时在 Vite 中,JavaScript 应用程序不再是自动注入的,main.js 文件需要手动引入。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

2. main.js

创建并挂载根实例

创建实例,new Vue --> createApp()

挂载方式,$mount() --> mount()

添加全局属性和方法,Vue.prototype --> app.config.globalProperties

// Vue 2
import Vue from 'vue'
import 'element-ui/lib/theme-chalk/index.css'
import ElementUI from 'element-ui'
import App from './App'
import router from './router'
import Vuex from 'vuex'
import store from './store'
import '@/assets/styles/common.scss'
import '@/assets/styles/main.scss'
import '@/assets/fonts/iconfont.css'

import './lib/filters.js'
import Format from './lib/format.js'
Vue.prototype.$format = Format

Vue.config.productionTip = false

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

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

// Vue 3
import { createApp } from 'vue'
import App from './App.vue'

import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import locale from 'element-plus/lib/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import router from './router'
import store from './store'

import '@/assets/styles/common.scss'
import '@/assets/styles/main.scss'
import '@/assets/fonts/iconfont.css'

import Format from './lib/format.js'

const app = createApp(App)
app.config.globalProperties.$format = Format

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.use(ElementPlus, { locale } ).use(router).use(store)
app.mount('#app')

四、语法和API

以下只是简单例举了几个常用的变化,关于语法和API的详细变更可以参见Vue官方文档:Vue2迁移

v-model

排查没有修饰符的 v-model,分别将 propevent 命名更改为 modelValueupdate:modelValue

v-bind.sync 修饰符和组件的 model 选项已移除,更改为 v-model:value

<template>

移除没有指令的 <template>,Vue 3 对组件的写法做了调整,支持多个根节点。

在 Vue 2 中,<template> 标签不能添加 key 属性,而是在其子元素上添加 key

在 Vue 3 中,key 应该被设置在 <template> 标签上。

// Vue 2
<template v-for="item in list">
  <span :key="item.value">...</span>
</template>

// Vue 3
<template v-for="item in list" :key="item.id">
  <span>...</span>
</template>

key

在 Vue 2 中,建议在 v-if / v-else / v-else-if 上添加 key

在 Vue 3 中,Vue 会自动生成唯一 key (依旧接受历史写法,但是不再被推荐使用)。

// Vue 2
<div v-if="isAdmin" key="aaa"> aaa </div>
<div v-else key="bbb"> bbb </div>

// Vue 3
<div v-if="isAdmin"> aaa </div>
<div v-else> bbb </div>

v-if && v-for

v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

在 Vue 3 中,如果在一个元素上同时使用 v-ifv-forv-if 的优先级高于 v-for (不推荐这样使用)。

functional

在 Vue 3 中,{ functional: true } 选项已从通过函数创建的组件中移除,移除配置中 { functional: true } 和模板中 <template functional>functional

defineAsyncComponent 辅助函数

在 Vue 3 中异步组件通过 defineAsyncComponent 方法创建。

// Vue 2
const asyncComponent = () => import('./async-component.vue')

// Vue 3
const asyncComponent = defineAsyncComponent(() => import('./async-component.vue'))

defineAsyncComponentcomponent 选项更名为 loader ,同时 loader 函数移除了 resolvereject 参数,必须手动返回 Promise

// Vue 2
const asyncComponent = (resolve, reject) => {
  ...
}

// Vue 3
const asyncComponent = defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      ...
    })
)

.native 修饰符

在 Vue 3 中,移除了 v-on: event.native 修饰符。(PS: Vue 3 中新增 emits 选项,所有未在组件 emits 选项中定义的事件作为原生事件添加到子组件的根元素中,除非子组件选项中设置了 inheritAttrs: false

// Vue 2
<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

// Vue 3
<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
// MyComponent.vue
<script>
  export default {
    emits: ['close']
  }
</script>

$scopedSlots

在 Vue 3 中,移除了 $scopedSlots ,统一了 $scopedSlots$slots ,所有插槽都通过 $slots 作为函数暴露。

$listeners

在 Vue 3 中,已经弃用 $listeners 对象,将事件监听器并入 $attrs ,作为 $attrs 的一部分。

$attrs

在 Vue 3 中,$attrs 将包含传递给组件的所有属性,包括 classstyle 属性 。

$set() 和 $delete()

在 Vue 2 中,修改某一些数据,视图是不能及时重新渲染的,因此提供了一些变异的方法,比如 $set$delete

在 Vue 3 中,移除了 $set$delete,基于代理的变化检测已经不再需要它们了。

<el-input v-model="form.no" placeholder="请输入编号"></el-input>

// Vue 2
export default {
  data() {
    return{
      form:{
        no:'11'
      }
    }
  }
  mounted () {
    this.form.no = 'CaseNo' // 视图不变
    this.$set(this.form, 'no', 'CaseNo') // 视图更新
  }
}

// Vue3
export default {
  data() {
    return{
      form:{
        no:'11'
      }
    }
  }
  mounted() {
    this.form.no = 'CaseNo' // 视图更新
  }
}

Events API

在 Vue 3 中,移除了 $on$off$once 这三个事件相关的API,不再支持事件发射器接口,可以使用外部库来实现事件总线,例如 mitt

// @/lib/bus.js
import mitt from 'mitt'
const bus = new mitt()
export default bus

// 使用
import Bus from '@/lib/bus'
export default {
  mounted () {
    Bus.off('loadMore')
    Bus.on('loadMore', this.initActivity)
  }
}

Mixin

在 Vue 2 中,data 的合并是深拷贝形式。

在 Vue 3 中,当组件的 datamixinextendsdata 进行合并时,只进行浅拷贝。

watch 监听器

在 Vue 3 中,watch 不再支持点分隔字符串路径,可以将监听的参数更改为 computed

Composition API

Options API --> Composition API

在 Vue 2 中,使用的是 Options API ,代码逻辑比较分散,可读性差,可维护性差。

在 Vue 3 中,使用的是 Composition API ,代码逻辑分明,可维护性更高。

因为涉及到所有页面和组件,修改起来变更过大,同时目前 Vue 3 也是兼容了 Options 写法,所以这部分代码结构可以先不用更改。之后新增的页面和组件可以按照 Vue 3 新增的 composition API 结构来写,其余内容可以后期逐步修改。

详情请参考 Vue 3 官方文档

五、生命周期

vue2vue3 (Options)vue3 (Composition)
beforeCreatebeforeCreatesetup()内部
createdcreatedsetup()内部
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted

六、路由

Vue 2 使用的是 router3.x 的API,换成 Vue3 需要用 router4.x 的API。

Vue Router 从 v3 到 v4 在迁移的过程中可能改动地方比较多,这里只是简单例举了几个常见的变化,详情可以参考 官方文档

1. 安装

npm install vue-router@4

2. new Router 变成 createRouter

new Router() --> createRouter 函数

3. 废除了mode选项配置

history --> createWebHistory

hash --> createWebHashHistory

abstract --> createMemoryHistory

// v3
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
  mode: 'history',
  routes
})
export default router

// v4
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes
})
export default router

4. 取消(*)通配符路由

// v3
{
  path: '*',
  name: '404',
  component: NotFound,
  meta: {
    title: '404'
  }
}

// v4
{
  path: '/:pathMatch(.*)*',
  name: '404',
  component: NotFound,
  meta: {
    title: '404'
  }
}

5. 新增useRoute、useRouter

路由信息, this.$route --> useRoute()

操作路由, this.$router --> useRouter()

// Options写法
this.$router.push({ path: this.$route.path, query: query })

// Composition写法
import { useRoute, useRouter } from 'vue-router'
export default defineComponent ({
  setup(props, ctx) {
    const route = useRoute()
    const router = useRouter()
    Router.push({ path: Route.path, query: query })
  }
})

七、状态管理

1. 安装

npm install vuex@next

2. 创建store对象并导出store

new Vuex.Store() --> createStore()

// store/index.js
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {  },
  mutations: {  },
  actions: {  }
})

import {createStore} from 'vuex'
export default createStore({
  state: {  },
  mutations: {  },
  actions: {  }
})

// main.js
...
import store from './store'
...
app.use(store)

3. 获取 store 实例对象

this.$store --> useStore()

// Options写法
const user = this.$store.user

// Composition写法
import { useStore } from 'vuex'
export default defineComponent ({
  setup(props, ctx) {
    const Store = useStore()
    const user = Store.state.user
  }
})

八、项目依赖

将项目中所使用到的 UI框架 和 第三方插件 切换成对应的 Vue 3 版本,相应用法可能也需要变更。

Element UI 升级为 Element Plus

PS: Element UI 升级为 Element Plus 后,可能会产生很多破坏性的更改,界面样式需要重新调整。

以下简单例举了几个常见的变化,详情可以参考 官方文档

Icon 图标
// Element UI
<i class="el-icon-plus"></i>

// Element Plus
<el-icon>
  <Plus />
</el-icon>
Dialog 对话框

:visible.sync --> v-model

// Element UI
<el-dialog title="提示" :visible.sync="dialogVisible" :before-close="handleClose">
  ...
</el-dialog>

// Element Plus
<el-dialog v-model="dialogVisible" title="提示" :before-close="handleClose">
  ...
</el-dialog>
Pagination 分页

:current-page.sync --> v-model:currentPage

// Element UI
<el-pagination layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" :total="total" :page-size="limit" :current-page.sync="filter.page" @current-change="changePage">
<el-pagination>

// Element Plus
<el-pagination layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" :total="total" :page-size="filter.limit" v-model:currentPage="filter.page" @current-change="changePage">
<el-pagination>
Button 按钮

sizemedium / small / mini --> large / default / small

texttype="text" --> text: boolean

// Element UI
<el-button type="text" disabled>文字按钮</el-button>

// Element Plus
<el-button text disabled>文字按钮</el-button>
Message 消息提示
// Element UI
this.$message.success('添加成功')

// Element Plus
// 引入
import { ElMessage } from "element-plus"
// 使用
ElMessage.success('添加成功') // 使用
Message Box 弹框
// Element UI
this.$confirm('是否确认删除该文件?', '提示', {
  confirmButtonText: '确定',
  cancelButtonText: '取消',
  type: 'warning'
}).then(async () => {
  ...
})

// Element Plus
// 引入
import { ElMessageBox } from "element-plus"
// 使用
ElMessageBox.confirm('是否确认删除该文件?', '提示', {
  confirmButtonText: '确定',
  cancelButtonText: '取消',
  type: 'warning'
}).then(async () => {
  ...
})

九、VS Code 扩展

Vetur --> Volar

十、其他

项目代码编译无误成功启动后,需点击 所有界面 进行测试,在启动的时候,Vite 并不会打包源码,而是在浏览器请求路由时才会进行打包,而且也仅仅打包当前路由的源码,故当某个子页面出错时可能项目启动、运行时都正常。

同时也需要检查代码中的业务是否正确,升级过后可能会对原有代码逻辑产生影响,例如表单校验等功能,业务逻辑需要逐一比对和测试。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值