vue2 项目升级 vue3 之 gogocode 代码转换规则覆盖情况(实践一)
vue2 项目技术支持到底还能有多久?
从官方发文来看,Vue 2.7 是当前、同时也是最后一个 Vue 2.x 的次级版本更新。Vue 2.7 会以其发布日期,即 2022 年 7 月 1 日开始计算,提供 18 个月的长期技术支持 (LTS:long-term support)。在此期间,Vue 2 将会提供必要的 bug 修复和安全修复,但不再提供新特性。
Vue 2 的终止支持时间是 2023 年 12 月 31 日。在此之后,Vue 2 在已有的分发渠道 (各类 CDN 和包管理器) 中仍然可用,但不再进行更新,包括对安全问题和浏览器兼容性问题的修复等。
vue2 项目升级为 vue3 已刻不容缓!
但是升级的这些非兼容性更新怎么办了,vue3毕竟改动很大,没法完全兼容vue2版本,但我们升级是必须的,重新开发吗?
有点难呀,够喝一大壶的了。
今天我们来一起实践阿里妈妈的 gogocode,为我们项目迁移带来了哪些惊喜!
下面我们一起来看看 gogocode 对升级代码转换规则覆盖情况
规则 | 转换支持 |
v-for 中的 ref 数组 | ✔ |
异步组件 | ✔ |
attribute 强制行为 | ✔ |
$attrs 包含 class & style | ✔ |
$children | ✖️ |
自定义指令 | ✔ |
自定义元素交互 | 无需转换 |
data 选项 | ✔ |
emits 选项 | ✔ |
事件api | ✔ |
过滤器 | ✔ |
片段 | ✔ |
函数式组件 | ✔ |
全局api | ✔ |
全局api treeshaking | ✔ |
内联模板 attribute | ✖️ |
keyattribute | ✔ |
按键修饰符 | ✔ |
移除 $listeners | ✔ |
挂载api变化 | ✔ |
propsData | 开发中 |
在prop的默认函数中访问this | 无需转换 |
渲染函数api | ✔ |
插槽统一 | ✔ |
suspense | 无需转换 |
过渡的class名更改 | ✔ |
transition作为root | 开发中 |
transition group 根元素 | ✔ |
移除 v-on.native 修饰符 | ✔ |
v-model | ✔ |
v-if 与 v-for 的优先级对比 | ✔ |
v-bind 合并行为 | ✔ |
VNode 生命周期事件 | 开发中 |
Watch on Arrays | ✔ |
vuex | ✔ |
vue-router | ✔ |
下面我们来看看实际情况:
v-for 中的 Ref 数组
在 Vue 2 中,在 v-for
里使用的 ref
attribute 会用 ref 数组填充相应的 $refs
property。当存在嵌套的 v-for
时,这种行为会变得不明确且效率低下。
<template>
<div class="books-container">
<ul>
<li v-for="book in books" ref="booksRef" :key="book.id">
<span>{{ book.id }}</span>
<span>{{ book.title }}</span>
<span>{{ book.author }}</span>
</li>
</ul>
<button @click="addBook">add book</button>
<button @click="getBookRefs">get book refs</button>
</div>
</template>
<script>
export default {
name: 'BookList',
data() {
return {
books: [],
itemRefs: [],
}
},
created() {
this.books = [
{
id: 1,
title: 'Jurassic Park',
author: 'Michael Crichton',
},
]
},
methods: {
addBook() {
this.books.push({
id: this.books.length + 1,
title: 'New Book',
author: 'New Author',
})
},
getBookRefs() {
console.log(this.$refs.booksRef)
}
},
}
</script>
转换前后代码对比:
异步组件
<template>
<div class="async-component-container">
<AsyncComponent />
</div>
</template>
<script>
export default {
name: 'CustomAsync',
components: {
AsyncComponent: () => import('./AsyncComponent.vue'),
}
}
</script>
转换前后代码没有变化,代码运行报错。
[Vue warn]: Invalid VNode type:undefined”(undefined)”
引入 “defineAsyncComponent” 实现异步引入。import { defineAsyncComponent } from "vue"
。问题解决了。
<template>
<div class="async-component-container">
<AsyncComponent />
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
name: 'CustomAsync',
components: {
AsyncComponent: defineAsyncComponent(() => import('./AsyncComponent.vue')),
},
}
</script>
attribute 强制行为
转换前后代码没有变化
注意:
vue2.x 有三个属性规则:布尔属性、枚举属性、其它属性。
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | – | draggable="false" |
:attr="undefined" | – | – |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | – | draggable="false" |
:attr="0" | foo="0" | draggable="true" |
attr="" | foo="" | draggable="true" |
attr="foo" | foo="foo" | draggable="true" |
attr | foo="" | draggable="true" |
Vue3.x 两种属性规则:枚举属性、其它属性归为一类。
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | – | -* |
:attr="undefined" | – | – |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | foo="false" * | draggable="false" |
:attr="0" | foo="0" | draggable="0" * |
attr="" | foo="" | draggable="" * |
attr="foo" | foo="foo" | draggable="foo" * |
attr | foo="" | draggable="" * |
<template>
<div class="attribute-container">
<h2>布尔属性</h2>
<p>
布尔类型:'如果是(undefined,null,或
false)移除该属性,但如果其他属性那则为对应本身'
</p>
<div class="des">
布尔属性有'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,
enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible'
</div>
<div :default="true">true</div>
<div :default="false">false</div>
<div :default="undefined">undefined</div>
<div :default="null">null</div>
<div :default="0">0</div>
<div :default="1">1</div>
<h2>枚举属性</h2>
<p>枚举类型:赋值为'undefined'则移除属性,其他无论你赋值成什么值</p>
<div class="des">枚举属性有'contenteditable,draggable,spellcheck'</div>
<div :draggable="true">true</div>
<div :draggable="false">false</div>
<div :draggable="undefined">undefined</div>
<div :draggable="null">null</div>
<div :draggable="0">0</div>
<div :draggable="1">1</div>
<h2>其它属性</h2>
<p>
普通类型:'如果是(undefined,null,或
false)移除该属性,其他则赋值是啥就是啥'
</p>
<div :attrA="true">true</div>
<div :attrA="false">false</div>
<div :attrA="undefined">undefined</div>
<div :attrA="null">null</div>
<div :attrA="0">0</div>
<div :attrA="1">1</div>
</div>
</template>
<script>
export default {
name: 'CustomAttribute',
}
</script>
$attrs包含class&style
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定
// Parent.vue
<script>
import Child from './Child.vue'
export default {
name: 'AttrsParent',
components: {
Child,
}
}
</script>
<template>
<div class="attrs-container">
<Child name="张三" age="30" sex="男" />
</div>
</template>
// Child.vue
<script>
import Son from './Son.vue'
export default {
name: 'CustomChild',
components: {
Son,
},
}
</script>
<template>
<div class="custom-child">
<Son v-bind="$attrs" />
</div>
</template>
// Son.vue
<script>
export default {
name: 'CustomSon'
}
</script>
<template>
<div class="custom-son">
<p>姓名:{{ $attrs.name }}</p>
<p>年龄:{{ $attrs.age }}</p>
<p>性别:{{ $attrs.sex }}</p>
</div>
</template>
转换前后代码没有变化
$children
当前实例的直接子组件。需要注意 $children
并不保证顺序,也不是响应式的。
// Parent.vue
<template>
<div class="parent-container">
<Child></Child>
子组件值:{{ childValue }}
</div>
</template>
<script>
import { $children } from '../../utils/gogocodeTransfer'
import Child from './Child.vue'
export default {
name: 'CustomParent',
components: {
Child,
},
data() {
return {
childValue: '',
}
},
mounted() {
this.childValue = $children(this)[0].count
console.log($children(this)[0].count)
},
}
</script>
// Child.vue
<template>
<div class="child">
{{ count }}
</div>
</template>
<script>
export default {
name: 'CustomChild',
data() {
return {
count: 1,
}
},
}
</script>
转换前后代码对比:
gogocode 帮忙写了个兼容的方法,真的不要太贴心了。
export function $children(instance) {
function $walk(vnode, children) {
if (vnode.component && vnode.component.proxy) {
children.push(vnode.component.proxy)
} else if (vnode.shapeFlag & (1 << 4)) {
const vnodes = vnode.children
for (let i = 0; i < vnodes.length; i++) {
$walk(vnodes[i], children)
}
}
}
const root = instance.$.subTree
const children = []
if (root) {
$walk(root, children)
}
return children
}
自定义指令
自定义指定作用的指令,需要先定义一个指令对象,该对象包含以下属性:
name
: 指令的名称,用于在模板中引用指令。bind
: 一个函数,用于将指令绑定到元素上。该函数接收三个参数:元素、属性名和属性值。inserted
: 一个函数,用于在元素插入到 DOM 中时调用。该函数接收两个参数:元素和父元素。update
: 一个函数,用于在元素的属性值发生变化时调用。该函数接收两个参数:元素和新的值。
<template>
<div class="v-focus-container">
<input v-focus />
</div>
</template>
<script>
export default {
name: 'VFocus',
directives: {
focus: {
inserted: function(el) {
el.focus()
},
},
},
}
</script>
转换前后代码对比:
自定义元素交互
非兼容:自定义元素白名单现在在模板编译期间执行,应该通过编译器选项而不是运行时配置来配置。 非兼容:特定 is prop 用法仅限于保留的 <component> 标记。
新增:有了新的 v-is 指令来支持 2.x 用例,其中在原生元素上使用了 v-is 来处理原生 HTML 解析限制。
<template>
<div class="is-component-container">
<component :is="component" />
<div is="custom-comp" />
</div>
</template>
<script>
import CustomComp from './CustomComp.vue'
export default {
name: 'IsComponent',
components: {
CustomComp,
},
data() {
return {
component: CustomComp,
}
},
}
</script>
转换前后代码没有变化
但使用 html 或 tag 的标签无法正常使用
// 错误的
<div is="custom-comp" />
// 正常的
<div v-is="custom-comp" />
Data 选项
vue2.x data声明形式有两种一种是对象形式,一种是函数形式
vue3.x 只有函数形式一种
const app = new Vue({
data: {
age:'2021'
}
})
对象形式 — 常用于 Vue 根实例,且常用于非工程化项目,这里就不过多介绍了。
emits选项
emits 触发当前实例上的事件。附加参数都会传给监听器回调,最常见的用处子传父
// emitsOptins.vue
<template>
<div class="emits-options-container">
<MyButton @callback="callback"></MyButton>
</div>
</template>
<script>
import MyButton from './MyButton.vue'
export default {
name: 'EmitsOptions',
components: {
MyButton,
},
methods: {
callback(data) {
console.log(data)
}
}
}
</script>
// MyButton.vue
<template>
<div class="my-button">
<button @click="clickFn">click</button>
</div>
</template>
<script>
export default {
name: 'MyButton',
methods: {
clickFn() {
this.$emit('callback', {
name: 'click',
value: 'hello',
})
}
}
}
</script>
转换前后代码没有变化
事件 API
$emit:触发当前实例上的事件。附加参数都会传给监听器回调。
$on:监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
$once: 监听当前实例上的自定义事件,只触发一次。
$off:移除自定义事件监听器。
// eventBus.js
import Vue from 'vue'
export default new Vue()
// eventApi.vue
<template>
<div class="event-api-container">
<A />
<B />
</div>
</template>
<script>
import A from './A.vue'
import B from './B.vue'
export default {
name: 'EventApi',
components: { A, B },
}
</script>
// A.vue
<template>
<div class="custom-a">
<button @click="clickFn">emit</button>
<button @click="clickFn2">emit2</button>
</div>
</template>
<script>
import EventBus from './eventBus'
export default {
name: 'CustomA',
methods: {
clickFn () {
EventBus.$emit('changeEvent', '参数1')
},
clickFn2 () {
EventBus.$emit('changeEvent2', '参数2')
}
},
}
</script>
// B.vue
<template>
<div class="custom-b">
custom b
</div>
</template>
<script>
import EventBus from './eventBus'
export default {
name: 'CustomB',
mounted() {
EventBus.$on('changeEvent', (msg) => {
console.log(msg)
})
EventBus.$once('changeEvent2', (msg) => {
console.log(msg)
})
},
destroyed() {
EventBus.$off('changeEvent')
EventBus.$off('changeEvent2')
}
}
</script>
转换前后代码对比:
过滤器
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化
局部注册
<template>
<div class="filters-container">
<p>人民币:{{ accountBalance | currencyRMD }}</p>
<p>美元:{{ accountBalance | currencyUSD }}</p>
</div>
</template>
<script>
export default {
name: 'CustomFilters',
data() {
return {
accountBalance: 100,
}
},
filters: {
currencyRMD(value) {
return `¥${value}`
}
}
}
</script>
转换前后代码对比:
全局注册
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 全局过滤器
Vue.filter('currencyUSD', function (value) {
return `$${value}`
})
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
转换前后代码对比:
更多请关注后续实践分享……