声明
请注意,笔者在写本文时,已经完成了升级改造工作,故部分错误可能无法通过截图呈现,不过笔者会尽可能用语言描述清楚
一次失败的尝试
由于所要改造项目是公司运行六七年的老项目了,内容之大之重可以想象,纯靠人力的话,光是想想就头大
所谓遇事不决先百度,像这类迁移工具必定是有人已经做过了的,于是发现了gogocode
首先,使用npm安装该工具
ts
npm install gogocode-cli -g
接着进行语法转换
在项目根目录执行如下命令
ts
// src是要转换的源
gogocode -s ./src -t gogocode-plugin-vue -o ./src-out
然后静静等待它转换完成

下一步,对相关依赖做升级,gogocode也提供了相关指令
ts
gogocode -s package.json -t gogocode-plugin-vue -o package.json
这一步执行之后,使用yarn重新安装依赖
ts
yarn
这样工具帮我们处理的工作就完成了,我们先来跑一下lint看看,如下有3k+

此时,我已经有点打退堂鼓了
先不急,咱们先来看看它转出来的效果
首先,它对package.json转换后,vue的版本是3.0.0。我为什么要特意强调这个呢?
因为这说明了两个问题:
1.现在vue都3.3了,它却还在用3.0,这说明大概率它已经鲜有维护更新了
2.3.3的语法与3.0大概率会存在出入,也就是说,它的转换结果未必可信
现在,我们再来看具体的sfc文件的转换
- props
我们知道,vue3.3中props已经被扁平化了,是不需要通过特定的props属性进行传递的,但gogocode对这一部分并未处理

- attrs
attrs也有类似的问题

- style样式
工具对源码转换后,会导致style标签内的样式错乱,具体来说应该是它无法识别css代码中的注释导致的
- 格式
除了上述的语法转换问题外,还有其他的,此处不再一一阐述。其实真正让我决定放弃的是,它转换出来感觉有点乱,且对源码的侵入性太大
比如eventBus,修改后,无法和原来一样调用,需要先导入,再使用,且必须手动传入组件实例,伪代码如下
ts
import {$on,$emit,$off} from '../xx/utils'
$on(instance,key,callback)
这成功给笔者造成了将近一千个error,因为项目里用到了400多次

具体来说,每一个文件都会触发相对路径必须在绝对路径之后引入和变量未被使用这两个error
半自动化
现在,笔者打算自己手动修正。不过在开始前,先说原则,原则就是,能不修改原来代码的就尽量不修改,能保证原有调用格式的就尽量保证原来的使用方式
loaders
一些通用的调整,可以通过webpack的loader来自动化
- filters
vue3中已经剔除了filters选项,需要将其移动到methods中

不过,代码有点多,咱这文章毕竟也不是卖钱的小册,所以就只讲一下思路,这也是最重要的:
首先,通过正则提取出script标签的内容部分,然后将其ast化,在通过节点树找到export default部分并将其提取出来转换成对象
转对象的时候,由于部分代码可能是export default外部的,比如
ts
const yyy = 'spp'
export default {
data(){
return xxx:yyy
}
}
这部分直接去解析会报错,因此需要进行try...catch并在捕获到错误时递归
ts
function parse(){
try{
...
}catch(err){
if(err){
parse()
}
}
}
接着判断对象中是否存在filters属性,如果有,就对该属性做遍历,将其key提取到methods中,形式大概是这样
ts
export default {
methods:{
filtersKey:filtersValue
}
}
这个filtersValue对应的就是原来的过滤函数,要把它生成到script标签中,具体来说,是import语句和export之间
ts
function filtersValue(){
...
}
下一步,就是去template模板中查找使用过滤函数的地方进行替换。这利用正则可以很轻松的完成

- props
在vue2中可以这样声明props
ts
export default {
props:{
xxx:'some value'
}
}
在vue3中会报错。因此需要对此进行转换。具体来说,就是找到props对象,分别对它的key对应的值进行识别。如果是对象或函数则不处理,如果是基本数据类型,则转换成如下形式
ts
{
type:'原值所对应的数据类型',
default:'some value'
}
这里比较容易出错的点是关于字符串类型的获取,需要手动拼接上引号

完整代码如下

- bus通信
众所周知,vue3中已经剔除了eventBus,无法再通过new实例的方式来实现跨级通信
但第三方插件又无法保证this指向,一般需要手动传入
但前文我们说过,要尽可能保证原调用方式不变
因此,需要在loader中提取并添加

这里的注意点在于loader的执行时机,必须要保证它在源代码被转换之前。否则添加的this与回调函数内的this大概率不是一个。如下,是经过转换后的代码,在回调中使用的this其实并不是实际传入的this
ts
var this1 = this
this.$bus.$on(key,this,()=>{
this1.xxx = 'spp'
})
fixed
还有一些是可以在全局去做兼容的,它们的目的是与改造前的代码行为尽可能保持一致
- bus通信
在前文loader中虽然解决了eventBus的this指向问题,但还没有找到可替代的包
幸运的是,笔者在之前实现的web-localstorage-plus中实现了该功能
首先,使用web-localstorage-plus提供的接口来代替bus功能

接着将其挂载到app.config.globalProperties上,这是vue3中提供的类似Vue.prototype挂载方式

又不幸的是,web-localstorage-plus目前其实并不支持接收this参数,且回调函数也不支持传递多个参数
因此,还需要对这两个接口进行重写。如下,接受参数二并将其挂载到第三个函数参数上,然后在触发on注册的监听函数时手动使用call修改其this指向,并将参数使用展开运算符传递以支持多个参数传递

- vue-router
在笔者的业务中,对单点登录进行了校验,并在无权限时跳转到指定的页面,伪代码如下
ts
// 获取权限
this.$router.onReady(...)
// 校验权限
this.$router.beforeEach(...)
这两行代码有两个问题
一个是vue-router4.x中已经废弃了onReady,需要改成isReady代替

另一个问题是,当页面刷新或执行window.open时,其表现与vue-router3.x不一致
在vue-router3.x中刷新并不会触发beforeEach钩子,但vue-router4.x中会触发
这就导致,会触发权限的重新获取,而此时beforeEach钩子执行时还拿不到权限数据导致跳转到noAuth页面
因此,需要对beforeEach钩子进行重写。如下,我们在页面刷新或window.open被执行时向本地设置缓存,并在beforeEach钩子中判断是否存在缓存标记,存在则什么都不做,否则正常跳转路由

- $children
vue3中已经剔除了$children接口,需要自己手动实现一份查找逻辑。如下,只需要按指定的格式进行递归即可

- $filters
对于注册的全局过滤器,现在统一调整到app.config.globalProperties上

- view-ui-plus
之前默认注册到全局的loading现在已经没有了,需要根据业务需求做调整

其他
剩下的,就是需要手动调整的其他语法了
- vue-router
1-h函数
在vue4.x版本中应该已经不支持render函数了,由于笔者公司项目有且仅有这一处对组件的使用,故修改成了默认方式

实际上,更准确的写法应该是这样

2-注册方式
4.x中已经不需要使用new关键字了,取而代之的是createRouter接口。另外,mode属性也被history替代了
ts
import { createRouter, createWebHashHistory } from 'vue-router';
export default createRouter({
history: createWebHashHistory(),
routes:[...]
})
- vuex
1-注册方式
与vue-router类似,通过createStore替换。
ts
import { createStore } from 'vuex';
export default createStore({...})
2-日志引入
它的日志插件的导入方式要改成下边这样
ts
import { createLogger } from 'vuex';
- view-ui-plus
对于js模块中使用的消息提示需要手动按需导入
ts
import { Message } from 'view-ui-plus'
Message.error('...')
- h函数
h函数的变动大致有四个
1-props与attrs
现在不需要在显示的指定这两个属性了,可以直接平铺传递
ts
h('div',{
//props:{
// xxx:'spp'
//}
xxx:'spp'
})
2-事件绑定
现在也已经剔除了on属性,而需要改成onEventType的形式
ts
h('div',{
//on:{
// click:()=>{}
//}
onClick(){}
})
3-slots
插槽需要改成函数形式
ts
h('div',{
//slot:'slotName'
slotName(){}
})
4-组件渲染
现在使用h函数渲染组件有两种方式,笔者采用的方式如下
首先将要导入的组件挂载到全局
ts
import { LoadingBar } from 'view-ui-plus';
app.component('LoadingBar', LoadingBar);
然后借助vue3提供的resolveComponent来执行导入
ts
import { resolveComponent } from 'vue';
h(resolveComponent('LoadingBar'))
- $set
基于proxy的v3已经不需要$set或$delete了,现在直接进行修改即可
- slot
v3中的插件必须使用template,且写法有变更,如下,现在可以合写成一个了
ts
<template
//slot="createTime"
//slot-scope="scope"
v-slot="scope"
>
...
</template>
- 三方组件库
这种每个人的不一样,笔者就不贴了,只说下笔者的处理方式
1-找vue3版本
有些组件库是支持vue3版本的,这部分直接yarn即可
2-修改源码
如果没有v3版本,就利用patch-pkg自己修改源码做兼容
3-copy源码
对于比较复杂的库,笔者是找到其源代码copy了一份,然后在项目中引入并修改
- vue-loader
项目启动后,页面元素之间的间隙会失效,配置如下
