聲明
請注意,筆者在寫本文時,已經(jīng)完成了升級改造工作,故部分錯誤可能無法通過截圖呈現(xiàn),不過筆者會盡可能用語言描述清楚
一次失敗的嘗試
由于所要改造項目是公司運(yùn)行六七年的老項目了,內(nèi)容之大之重可以想象,純靠人力的話,光是想想就頭大
所謂遇事不決先百度,像這類遷移工具必定是有人已經(jīng)做過了的,于是發(fā)現(xiàn)了gogocode
首先,使用npm安裝該工具
npm install gogocode-cli -g
接著進(jìn)行語法轉(zhuǎn)換
在項目根目錄執(zhí)行如下命令
// src是要轉(zhuǎn)換的源
gogocode -s ./src -t gogocode-plugin-vue -o ./src-out
然后靜靜等待它轉(zhuǎn)換完成

下一步,對相關(guān)依賴做升級,gogocode也提供了相關(guān)指令
gogocode -s package.json -t gogocode-plugin-vue -o package.json
這一步執(zhí)行之后,使用yarn重新安裝依賴
yarn
這樣工具幫我們處理的工作就完成了,我們先來跑一下lint看看,如下有3k+

此時,我已經(jīng)有點打退堂鼓了
先不急,咱們先來看看它轉(zhuǎn)出來的效果
首先,它對package.json轉(zhuǎn)換后,vue的版本是3.0.0。我為什么要特意強(qiáng)調(diào)這個呢?
因為這說明了兩個問題:
1.現(xiàn)在vue都3.3了,它卻還在用3.0,這說明大概率它已經(jīng)鮮有維護(hù)更新了
2.3.3的語法與3.0大概率會存在出入,也就是說,它的轉(zhuǎn)換結(jié)果未必可信
現(xiàn)在,我們再來看具體的sfc文件的轉(zhuǎn)換
- props
我們知道,vue3.3中props已經(jīng)被扁平化了,是不需要通過特定的props屬性進(jìn)行傳遞的,但gogocode對這一部分并未處理

- attrs
attrs也有類似的問題

- style樣式
工具對源碼轉(zhuǎn)換后,會導(dǎo)致style標(biāo)簽內(nèi)的樣式錯亂,具體來說應(yīng)該是它無法識別css代碼中的注釋導(dǎo)致的
- 格式
除了上述的語法轉(zhuǎn)換問題外,還有其他的,此處不再一一闡述。其實真正讓我決定放棄的是,它轉(zhuǎn)換出來感覺有點亂,且對源碼的侵入性太大
比如eventBus,修改后,無法和原來一樣調(diào)用,需要先導(dǎo)入,再使用,且必須手動傳入組件實例,偽代碼如下
import {$on,$emit,$off} from '../xx/utils'
$on(instance,key,callback)
這成功給筆者造成了將近一千個error,因為項目里用到了400多次

具體來說,每一個文件都會觸發(fā)相對路徑必須在絕對路徑之后引入和變量未被使用這兩個error
半自動化
現(xiàn)在,筆者打算自己手動修正。不過在開始前,先說原則,原則就是,能不修改原來代碼的就盡量不修改,能保證原有調(diào)用格式的就盡量保證原來的使用方式
loaders
一些通用的調(diào)整,可以通過webpack的loader來自動化
- filters
vue3中已經(jīng)剔除了filters選項,需要將其移動到methods中

不過,代碼有點多,咱這文章畢竟也不是賣錢的小冊,所以就只講一下思路,這也是最重要的:
首先,通過正則提取出script標(biāo)簽的內(nèi)容部分,然后將其ast化,在通過節(jié)點樹找到export default部分并將其提取出來轉(zhuǎn)換成對象
轉(zhuǎn)對象的時候,由于部分代碼可能是export default外部的,比如
const yyy = 'spp'
export default {
data(){
return xxx:yyy
}
}
這部分直接去解析會報錯,因此需要進(jìn)行try...catch并在捕獲到錯誤時遞歸
function parse(){
try{
...
}catch(err){
if(err){
parse()
}
}
}
接著判斷對象中是否存在filters屬性,如果有,就對該屬性做遍歷,將其key提取到methods中,形式大概是這樣
export default {
methods:{
filtersKey:filtersValue
}
}
這個filtersValue對應(yīng)的就是原來的過濾函數(shù),要把它生成到script標(biāo)簽中,具體來說,是import語句和export之間
function filtersValue(){
...
}
下一步,就是去template模板中查找使用過濾函數(shù)的地方進(jìn)行替換。這利用正則可以很輕松的完成

- props
在vue2中可以這樣聲明props
export default {
props:{
xxx:'some value'
}
}
在vue3中會報錯。因此需要對此進(jìn)行轉(zhuǎn)換。具體來說,就是找到props對象,分別對它的key對應(yīng)的值進(jìn)行識別。如果是對象或函數(shù)則不處理,如果是基本數(shù)據(jù)類型,則轉(zhuǎn)換成如下形式
{
type:'原值所對應(yīng)的數(shù)據(jù)類型',
default:'some value'
}
這里比較容易出錯的點是關(guān)于字符串類型的獲取,需要手動拼接上引號

完整代碼如下

- bus通信
眾所周知,vue3中已經(jīng)剔除了eventBus,無法再通過new實例的方式來實現(xiàn)跨級通信
但第三方插件又無法保證this指向,一般需要手動傳入
但前文我們說過,要盡可能保證原調(diào)用方式不變
因此,需要在loader中提取并添加

這里的注意點在于loader的執(zhí)行時機(jī),必須要保證它在源代碼被轉(zhuǎn)換之前。否則添加的this與回調(diào)函數(shù)內(nèi)的this大概率不是一個。如下,是經(jīng)過轉(zhuǎn)換后的代碼,在回調(diào)中使用的this其實并不是實際傳入的this
var this1 = this
this.$bus.$on(key,this,()=>{
this1.xxx = 'spp'
})
fixed
還有一些是可以在全局去做兼容的,它們的目的是與改造前的代碼行為盡可能保持一致
- bus通信
在前文loader中雖然解決了eventBus的this指向問題,但還沒有找到可替代的包
幸運(yùn)的是,筆者在之前實現(xiàn)的web-localstorage-plus中實現(xiàn)了該功能
首先,使用web-localstorage-plus提供的接口來代替bus功能

接著將其掛載到app.config.globalProperties上,這是vue3中提供的類似Vue.prototype掛載方式

又不幸的是,web-localstorage-plus目前其實并不支持接收this參數(shù),且回調(diào)函數(shù)也不支持傳遞多個參數(shù)
因此,還需要對這兩個接口進(jìn)行重寫。如下,接受參數(shù)二并將其掛載到第三個函數(shù)參數(shù)上,然后在觸發(fā)on注冊的監(jiān)聽函數(shù)時手動使用call修改其this指向,并將參數(shù)使用展開運(yùn)算符傳遞以支持多個參數(shù)傳遞

- vue-router
在筆者的業(yè)務(wù)中,對單點登錄進(jìn)行了校驗,并在無權(quán)限時跳轉(zhuǎn)到指定的頁面,偽代碼如下
// 獲取權(quán)限
this.$router.onReady(...)
// 校驗權(quán)限
this.$router.beforeEach(...)
這兩行代碼有兩個問題
一個是vue-router4.x中已經(jīng)廢棄了onReady,需要改成isReady代替

另一個問題是,當(dāng)頁面刷新或執(zhí)行window.open時,其表現(xiàn)與vue-router3.x不一致
在vue-router3.x中刷新并不會觸發(fā)beforeEach鉤子,但vue-router4.x中會觸發(fā)
這就導(dǎo)致,會觸發(fā)權(quán)限的重新獲取,而此時beforeEach鉤子執(zhí)行時還拿不到權(quán)限數(shù)據(jù)導(dǎo)致跳轉(zhuǎn)到noAuth頁面
因此,需要對beforeEach鉤子進(jìn)行重寫。如下,我們在頁面刷新或window.open被執(zhí)行時向本地設(shè)置緩存,并在beforeEach鉤子中判斷是否存在緩存標(biāo)記,存在則什么都不做,否則正常跳轉(zhuǎn)路由

- $children
vue3中已經(jīng)剔除了$children接口,需要自己手動實現(xiàn)一份查找邏輯。如下,只需要按指定的格式進(jìn)行遞歸即可

- $filters
對于注冊的全局過濾器,現(xiàn)在統(tǒng)一調(diào)整到app.config.globalProperties上

- view-ui-plus
之前默認(rèn)注冊到全局的loading現(xiàn)在已經(jīng)沒有了,需要根據(jù)業(yè)務(wù)需求做調(diào)整

其他
剩下的,就是需要手動調(diào)整的其他語法了
- vue-router
1-h函數(shù)
在vue4.x版本中應(yīng)該已經(jīng)不支持render函數(shù)了,由于筆者公司項目有且僅有這一處對組件的使用,故修改成了默認(rèn)方式

實際上,更準(zhǔn)確的寫法應(yīng)該是這樣

2-注冊方式
4.x中已經(jīng)不需要使用new關(guān)鍵字了,取而代之的是createRouter接口。另外,mode屬性也被history替代了
import { createRouter, createWebHashHistory } from 'vue-router';
export default createRouter({
history: createWebHashHistory(),
routes:[...]
})
- vuex
1-注冊方式
與vue-router類似,通過createStore替換。
import { createStore } from 'vuex';
export default createStore({...})
2-日志引入
它的日志插件的導(dǎo)入方式要改成下邊這樣
import { createLogger } from 'vuex';
- view-ui-plus
對于js模塊中使用的消息提示需要手動按需導(dǎo)入
import { Message } from 'view-ui-plus'
Message.error('...')
- h函數(shù)
h函數(shù)的變動大致有四個
1-props與attrs
現(xiàn)在不需要在顯示的指定這兩個屬性了,可以直接平鋪傳遞
h('div',{
//props:{
// xxx:'spp'
//}
xxx:'spp'
})
2-事件綁定
現(xiàn)在也已經(jīng)剔除了on屬性,而需要改成onEventType的形式
h('div',{
//on:{
// click:()=>{}
//}
onClick(){}
})
3-slots
插槽需要改成函數(shù)形式
h('div',{
//slot:'slotName'
slotName(){}
})
4-組件渲染
現(xiàn)在使用h函數(shù)渲染組件有兩種方式,筆者采用的方式如下
首先將要導(dǎo)入的組件掛載到全局
import { LoadingBar } from 'view-ui-plus';
app.component('LoadingBar', LoadingBar);
然后借助vue3提供的resolveComponent來執(zhí)行導(dǎo)入
import { resolveComponent } from 'vue';
h(resolveComponent('LoadingBar'))
- $set
基于proxy的v3已經(jīng)不需要$set或$delete了,現(xiàn)在直接進(jìn)行修改即可
- slot
v3中的插件必須使用template,且寫法有變更,如下,現(xiàn)在可以合寫成一個了
<template
//slot="createTime"
//slot-scope="scope"
v-slot="scope"
>
...
</template>
- 三方組件庫
這種每個人的不一樣,筆者就不貼了,只說下筆者的處理方式
1-找vue3版本
有些組件庫是支持vue3版本的,這部分直接yarn即可
2-修改源碼
如果沒有v3版本,就利用patch-pkg自己修改源碼做兼容
3-copy源碼
對于比較復(fù)雜的庫,筆者是找到其源代碼copy了一份,然后在項目中引入并修改
- vue-loader
項目啟動后,頁面元素之間的間隙會失效,配置如下

測試問題修復(fù)
- vue-router傳參
4.x已經(jīng)不支持傳遞對象形式了

因為拿到的是字符串,并且這個字符串還沒法通過JSON.parse進(jìn)行解析

但是又沒法一個一個調(diào)整,畢竟有一百多處都在使用

所以還是老規(guī)矩,從源碼修改。這只需要分兩步
第一步,重寫push

第二步,重寫query屬性
