vue2 项目升级 vue3 之 gogocode 代码转换规则覆盖情况(实践一)

作者: tww844475003 分类: 前端开发 发布时间: 2023-09-03 02:34

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"
attrfoo=""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"*
attrfoo=""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')

转换前后代码对比:

更多请关注后续实践分享……

前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注