代碼的複用是一件很常見的事情,若是是公共代碼的複用那還好說,直接作成一個內部私有庫,想用的話安裝一下 npm
包就好了,可是業務代碼的複用就很差作成包了,通常都是複製粘貼前端
我通常寫代碼的時候,若是以爲某段業務代碼之前見過其餘人寫過,那麼考慮到業務優先性,只要別人的代碼不是寫得太爛,我通常會優先抄別人的代碼,免得本身再寫一遍 而後我就遇到了一個問題,公司目前前端項目大部分都是 vue
,早期沒有 ts
這個說法,後來新項目才逐漸引入 ts
,因此新項目用的是 vue-ts
,而通常想抄的老代碼都是沒有引入 ts
的,當然,這兩者是能夠兼容存在的,但對於有着輕微代碼潔癖的我來講,仍是不想看到同一個項目代碼裏摻雜着 ts
和非 ts
兩種寫法的,因此只要有時間,我都會盡可能手動把老代碼轉化爲 ts
規範的vue
難度卻是沒多少,只不過每一份都要手動轉一遍,轉得多了我突然陷入沉思,我好像 repeat myself
了啊,不太能忍,因而決定寫一個自動將 vue-js
轉成 vue-ts
的工具node
這個工具的代碼已經被我放到 github 上了,而且爲了方便使用,我已經將其作成了一個 npm 包,感興趣的能夠親自試一下react
涉及到 js
語法轉換的東西,第一時間想到的就是 babel
了,babel
早就提供了豐富完善的 js
語法的解析與反解析工具git
@babel/parser 是負責解析 js
語法的工具,能夠理解爲將 js
語法轉化爲 ast
,方便開發者進行自定義處理,經過 plugins
來支持多種 js
語法,例如 es6
、es7
、ts
、flow
、jsx
甚至是一些實驗室的語法(experimental language proposals
)等es6
例如:github
const code = 'const a = 1'
const ast = require("@babel/parser").parse(code)
複製代碼
轉換後的 ast
就是一個對象,數據結構描述的就是 const a = 1
這個表達式vue-router
對這個 ast
進行遍歷,就能夠得到全部當前解析的 js
語法的信息,天然也能對其進行修改vuex
有解析就有反解析,@babel/generator用於將 @babel/parser解析出的 ast
轉化回字符串形式的 js
代碼vue-cli
const code = 'const a = 1;'
const ast = require("@babel/parser").parse(code)
const codeStr = require('@babel/generator').default(ast).code
code === codeStr // => true
複製代碼
通常 @babel/parser、@babel/generator 和 @babel/traverse會一塊兒出現使用,前兩個前面已經介紹過了,至於 @babel/traverse,其主要做用就是對 @babel/parser生成的 ast
進行遍歷,提供了一些方法,免得開發者本身去作各類判斷
不過我這裏寫的這個程序,由於不須要太過細緻的解析,因此沒用 @babel/traverse這個東西,我按照本身的意願對 ast
進行遍歷操做
除此以外,babel還提供了一些其餘的工具庫啦幫助庫啦,通常都不太用獲得,想要詳細瞭解的能夠本身去看文檔
本文下面所說的操做,基本上都是在 @babel/parser 轉換後的 ast
,以及 @babel/generator 解析後的代碼字符串上進行的
vue
官網對於 props
的介紹在 props
所以 props
的如下幾種寫法都是符合規範的:
export default {
props: ['size', 'myMessage'],
props: {
a: Number,
b: [Number, String],
c: 'defaultValue',
d: {
type: [Number, String]
}
e: {
type: Number,
default: 0,
required: true,
validator: function (value) {
return value >= 0
}
}
}
}
複製代碼
上述轉換爲 ts
對應以下:
export default class YourComponent extends Vue {
@Prop() readonly size: any | undefined
@Prop() readonly myMessage: any | undefined
@Prop({ type: Number }) readonly a: number | undefined
@Prop([Number, String]) readonly b: number | string | undefined
@Prop() readonly c!: any
@Prop({ type: [Number, String] }) readonly d: number | string | undefined
@Prop({ type: Number, default: 0, required: true, validator: function (value) {
return value >= 0
} }) readonly e!: number
}
複製代碼
ok
,那就好辦了,首先 props
值的類型只有 Array<string>
和 對象 這兩種類型
Array<string>
類型很好辦,就一個轉換模板:
@Prop() readonly propsName: any | undefined
複製代碼
只須要遍歷 Array<string>
類型的 props
,而後,把 propsName
替換成真正的值便可
對象類型的轉化模板在數組類型的模板上,多加了一些字符串,主要就是 @Prop
的參數:
@Prop({ type: typeV, default: defaultV, required: requiredV, validator: validatorV }) readonly propsName: typeV
複製代碼
props
這個大對象的每一個屬性,都是一個 propsName
,這個是肯定的,而後 propsName
對應的值,多是 type
,type
分爲單類型(例如 Number
),以及類型數組(例如 [Number, String]
);多是一個對象,這個對象下的屬性最少爲 0
個,最多爲 4
個,若是這個對象存在一個屬性名爲 type
的屬性,則這個屬性的值也須要判斷單類型和類型數組,其餘屬性直接取原值便可
不管 props
對象的屬性值是對象仍是 type
,都須要處理 type
,因此一個專門處理 type
的方法 handlerType
如此一來,若是是 type
,則 handlerType
直接處理好;若是是對象,則遍歷這個對象的屬性,發現屬性是 type
,則調用 handlerType
進行處理,不然直接原樣做爲 @Prop
的參數便可
vue
官網對於 data
的介紹在 data
data
的類型能夠是 Object
或 Function
,即如下幾種寫法都合法:
export default {
data: {
a: 1
},
data () {
return {
a: 1
}
},
data: function () {
return {
a: 1
}
}
}
複製代碼
上述轉換爲 ts
對應以下:
export default class YourComponent extends Vue {
a: number = 1
}
複製代碼
因此這裏就很明瞭了,就是取 data
返回值對象的每一個屬性,做爲 class
的屬性,好像轉換一下就好了
可是,data
其實還能夠這麼寫:
export default {
data () {
const originA = 1
return {
a: originA
}
}
}
複製代碼
當 data
是 Function
類型時,在 return
以前,還能夠運行一段代碼,這段代碼的運行結果可能影響到 data
的值
這種寫法並很多見,因此不可忽視,但如何處理 return
以前的代碼? 個人作法是將 return
以前的代碼放到 created
生命週期函數中,而且在 created
中的這些代碼以後,再對每一個 data
從新賦一遍值 好比,對於上面的代碼來講,轉換成 ts
,能夠這麼作:
export default class YourComponent extends Vue {
a: any = null
created () {
const originA = 1
this.a = originA
}
}
複製代碼
因此,這就又涉及到 data
對 created
的數據修改了,這裏能夠考慮強制先處理 data
,可是我看了下,其實這裏寫兩段邏輯也並不複雜,因此我就不嚴格規定處理的順序了
vue
官網對於 model
的介紹在 model
model
中引用了 props
中的值,因此 model
的使用實際上是須要 props
配合的
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: {
type: Boolean
}
}
}
複製代碼
上述轉換爲 ts
對應以下:
export default class YourComponent extends Vue {
@Model('change', { type: Boolean }) readonly checked!: boolean
}
複製代碼
可見,@Model
是具有聲明 props
的功能的,在 @Model
中聲明瞭的 props
,就不必在 @Prop
中再聲明一遍了,因此我這裏安排了一下處理順序,先處理 model
,再處理 props
,而且在處理 props
的時候,將 model
中已經聲明瞭的 props
篩選掉
固然,你也能夠不專門先處理 model
再處理 props
,只要在處理 model
的時候判斷一下,是否在此以前已經處理過 props
了,根據結果來作相應的處理流程,但這樣未免有些麻煩,須要根據 props
的處理與否來寫兩段邏輯,這兩段邏輯比上面 data
影響 created
的要複雜一些,因此這裏我就直接按照順序處理了,免得給本身找麻煩
vue
官網對於 model
的介紹在 computed
如下幾種 computed
的寫法都是正確的
export default {
computed: {
a () { return true },
b: function () { return true },
d: {
get () { return true },
set: function (v) { console.log(v) }
}
}
}
複製代碼
vue-property-decorator
並無提供專門的用於 computed
的修飾器,由於 ES6
的 get/set
語法自己就能夠替代 computed
上述轉換爲 ts
對應以下:
export default class YourComponent extends Vue {
get a () { return true }
get b () { return true },
get d (){ return true },
set d (v) { console.log(v) }
}
複製代碼
除此以外,computed
其實還支持箭頭函數的寫法:
export default {
computed: {
e: () => { return true }
}
}
複製代碼
可是 class
語法的 get/set
不支持箭頭函數,因此很差轉換,另外由於箭頭函數會改變 this
的指向,而 computed
計算的就是當前 vue
實例上的屬性,因此通常也不推薦在 computed
中使用箭頭函數,當然你能夠在箭頭函數的第一個參數上得到當前 vue
實例,但這就未免有點畫蛇添足的嫌疑了,因此我這裏略過對箭頭函數的處理,只會在遇到 computed
上的箭頭函數時,給你一個提示
vue
官網對於 watch
的介紹在 watch
如下都是合法的 watch
寫法:
export default {
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 該回調會在任何被偵聽的對象的 property 改變時被調用,不論其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 該回調將會在偵聽開始以後被當即調用
d: {
handler: 'someMethod',
immediate: true
},
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
immediate: true
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
}
複製代碼
上述轉換爲 ts
對應以下:
export default class YourComponent extends Vue {
@Watch('a')
onAChanged(val: any, oldVal: any) {}
@Watch('b')
onBChanged (val: any, oldVal: any) {
this.someMethod(val, oldVal)
}
@Watch('c', { deep: true })
onCChanged (val: any, oldVal: any) {}
@Watch('d', { deep: true })
onDChanged (val: any, oldVal: any) {}
@Watch('e')
onE1Changed (val: any, oldVal: any) {}
@Watch('e')
onE2Changed (val: any, oldVal: any) {}
@Watch('e', { immediate: true })
onE3Changed (val: any, oldVal: any) {}
@Watch('e.f')
onEFChanged (val: any, oldVal: any) {}
}
複製代碼
寫法仍是不少的,因此判斷分支確定少不了
watch
下的每一個屬性都是一個須要進行 watch
的 vue
響應值,這些屬性的值能夠是字符串、函數、對象和數組,共四種類型
其中,字符串類型就是至關於調用當前 vue
實例裏的方法,函數類型就是調用這個函數,比較簡單; 對於對象類型,其具備三個屬性:handler
、deep
、immediate
,三個屬性都是可選,其中 handler
的值是函數或字符串,其餘兩個屬性的值都是 boolean
類型; 對於數組類型,其每個數組項,其實都至關因而字符串類型、函數類型和對象類型的聚合,因此實際上只要處理這三種類型便可,數組類型則直接遍歷數組項,每一個數組項的類型確定在這三個類型以內,按照類型調用相應的處理方法便可。
這是主體部分,除此以外,還須要考慮 handler
函數的形式,如下幾種函數的寫法都是合法的:
export default {
watch: {
a: function {},
b () {},
c: () => {},
d: async function {},
e: async () => {}
}
}
複製代碼
不只在 watch
裏面,其餘一些 vue
實例屬性,好比 created
、computed
等,只要是可能出現函數的地方,都須要考慮到這些寫法 固然,除此以外,還有 Generator
函數,但我這裏不考慮,有更好的 async/await
可用,爲何非要用 Generator
vue
實例的方法,都做爲 methods
這個對象的屬性存在,每一個方法都是一個函數,因此只須要將原 methods
下的全部方法取出,轉換爲 class
的方法便可,沒什麼工做量 不過須要注意的是,函數的寫法有不少,還能夠支持 async/await
,這些寫法都須要考慮到
vue
的生命週期鉤子函數有不少,還有一些第三方的鉤子函數,例如 vue-router
:
const vueLifeCycle = ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed', 'errorCaptured', 'beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']
複製代碼
這些鉤子函數其實就是函數,跟 methods
的處理方法同樣
這個比較簡單,轉化一下而後拼接
export default {
components: {
a: A,
B
},
}
複製代碼
上述轉換爲 ts
對應以下:
@Component({
components: {
a: A,
B
}
})
export default class TransVue2TS extends Vue {}
複製代碼
因此就是把原 components
的屬性所有映射一遍便可
vue
官網對於 mixins
的介紹在 mixins
其值類型爲 Array<Object>
export default {
mixins: [A, B]
}
複製代碼
上述轉換爲 ts
對應以下:
export default class TransVue2TS extends Mixins(A, B) {}
複製代碼
本來 extends Vue
改爲 extends Mixins
,而且 Mixins
的參數就是原 mixins
的全部數組項
當我考慮如何處理這兩個的時候,看了下 vue
官網,官網上對於這兩個是這麼說的:
provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。
而且在這段話上,還專門用紅色感嘆號標識了一下,說白了就是不建議你在業務代碼中,由於這不利於數據的追蹤,徹底可使用成熟的 vueBus
或者 vuex
代替,通常也不會用到這個東西的,我寫這個轉換程序也是爲了轉換業務代碼,因此我沒有對這兩個作處理,若是發現代碼中存在這兩個屬性,會提示你本身手動處理
這兩個都只是一種相似語法糖的東西,能夠不作處理
上述是針對一份 .vue
文件的詳細處理的邏輯,想要真正的接入實際文件乃至文件夾的處理,天然少不了文件的讀取和更新操做,這就涉及到 node
的文件處理內容了,不過並不複雜,就很少說了
代碼寫完以後,爲了簡化使用流程,我將其打包成了一個 npm包上傳到 npm上去了,想要使用的話,只須要下載這個包,而後在命令行中輸入指令便可
npm i transvue2ts -g
複製代碼
安裝完以後,默認是跟 vue-cli
同樣,會把此庫的路徑寫到系統的 path
中,直接打開命令行工具便可使用,同時支持單文件和文件目錄耳朵轉化 transvue2ts
是庫的指令,第二個參數是須要處理的文件(夾)的 完整全路徑 例如: 處理 E:\project\testA\src\test.vue
文件:
transvue2ts E:\project\testA\src\test.vue
=>
輸出路徑:E:\project\testA\src\testTs.vue
複製代碼
處理 E:\project\testA\src
文件夾下的全部 .vue
文件:
transvue2ts E:\project\testA\src
=>
輸出路徑:E:\project\testA\srcTs
複製代碼
對於單文件來講,其必須是 .vue
結尾,轉化後的文件將輸出到同級目錄下,文件名爲原文件名 + Ts
,例如 index.vue
=> indexTs.vue
; 對於文件目錄來講,程序將會對此文件目錄進行遞歸遍歷,找出這個文件夾下全部的 .vue
文件進行轉化,轉化後的文件將按照原先的目錄結構所有平移到同級目錄下的一個新文件夾中,例如 /src
=> /srcTs
這個轉化程序看起來很麻煩的樣子,歸納一下,其實就三步:
vue-js
語法及其多變的寫法js-ts
語法之間的轉化映射關係本質上這個程序就是一個翻譯器,將 vue-js
語法翻譯成 vue-ts
語法,難點在於你要找到兩者之間全部語法的映射關係,並知道如何進行處理,因此實際上大部分都是體力活
只要你明白了這其中的套路,其實換個什麼 vue
轉 wepy
,或者 react
轉微信小程序,其實都是同樣,都是翻譯器,都是體力活,只不過有些很輕鬆,也就是搬幾塊磚的事情,而有些體力活比較辛苦還須要動腦子罷了