解放雙手-vue語法自動轉typescript

代碼的複用是一件很常見的事情,若是是公共代碼的複用那還好說,直接作成一個內部私有庫,想用的話安裝一下 npm包就好了,可是業務代碼的複用就很差作成包了,通常都是複製粘貼前端

我通常寫代碼的時候,若是以爲某段業務代碼之前見過其餘人寫過,那麼考慮到業務優先性,只要別人的代碼不是寫得太爛,我通常會優先抄別人的代碼,免得本身再寫一遍 而後我就遇到了一個問題,公司目前前端項目大部分都是 vue,早期沒有 ts這個說法,後來新項目才逐漸引入 ts,因此新項目用的是 vue-ts,而通常想抄的老代碼都是沒有引入 ts的,當然,這兩者是能夠兼容存在的,但對於有着輕微代碼潔癖的我來講,仍是不想看到同一個項目代碼裏摻雜着 ts和非 ts兩種寫法的,因此只要有時間,我都會盡可能手動把老代碼轉化爲 ts規範的vue

難度卻是沒多少,只不過每一份都要手動轉一遍,轉得多了我突然陷入沉思,我好像 repeat myself了啊,不太能忍,因而決定寫一個自動將 vue-js轉成 vue-ts的工具node

這個工具的代碼已經被我放到 github 上了,而且爲了方便使用,我已經將其作成了一個 npm 包,感興趣的能夠親自試一下react

@babel

涉及到 js語法轉換的東西,第一時間想到的就是 babel了,babel早就提供了豐富完善的 js語法的解析與反解析工具git

@babel/parser

@babel/parser 是負責解析 js語法的工具,能夠理解爲將 js語法轉化爲 ast,方便開發者進行自定義處理,經過 plugins來支持多種 js語法,例如 es6es7tsflowjsx甚至是一些實驗室的語法(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/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 解析後的代碼字符串上進行的

props

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對應的值,多是 typetype 分爲單類型(例如 Number),以及類型數組(例如 [Number, String]);多是一個對象,這個對象下的屬性最少爲 0個,最多爲 4個,若是這個對象存在一個屬性名爲 type的屬性,則這個屬性的值也須要判斷單類型和類型數組,其餘屬性直接取原值便可

不管 props對象的屬性值是對象仍是 type,都須要處理 type,因此一個專門處理 type的方法 handlerType

如此一來,若是是 type,則 handlerType直接處理好;若是是對象,則遍歷這個對象的屬性,發現屬性是 type,則調用 handlerType進行處理,不然直接原樣做爲 @Prop的參數便可

data

vue官網對於 data的介紹在 data

data的類型能夠是 ObjectFunction,即如下幾種寫法都合法:

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
    }
  }
}
複製代碼

dataFunction 類型時,在 return以前,還能夠運行一段代碼,這段代碼的運行結果可能影響到 data的值

這種寫法並很多見,因此不可忽視,但如何處理 return以前的代碼? 個人作法是將 return以前的代碼放到 created生命週期函數中,而且在 created中的這些代碼以後,再對每一個 data從新賦一遍值 好比,對於上面的代碼來講,轉換成 ts,能夠這麼作:

export default class YourComponent extends Vue {
  a: any = null
  created () {
    const originA = 1
    this.a = originA
  }
}
複製代碼

因此,這就又涉及到 datacreated的數據修改了,這裏能夠考慮強制先處理 data,可是我看了下,其實這裏寫兩段邏輯也並不複雜,因此我就不嚴格規定處理的順序了

model

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的要複雜一些,因此這裏我就直接按照順序處理了,免得給本身找麻煩

computed

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的修飾器,由於 ES6get/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上的箭頭函數時,給你一個提示

watch

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下的每一個屬性都是一個須要進行 watchvue響應值,這些屬性的值能夠是字符串、函數、對象和數組,共四種類型

其中,字符串類型就是至關於調用當前 vue實例裏的方法,函數類型就是調用這個函數,比較簡單; 對於對象類型,其具備三個屬性:handlerdeepimmediate,三個屬性都是可選,其中 handler的值是函數或字符串,其餘兩個屬性的值都是 boolean類型; 對於數組類型,其每個數組項,其實都至關因而字符串類型、函數類型和對象類型的聚合,因此實際上只要處理這三種類型便可,數組類型則直接遍歷數組項,每一個數組項的類型確定在這三個類型以內,按照類型調用相應的處理方法便可。

這是主體部分,除此以外,還須要考慮 handler函數的形式,如下幾種函數的寫法都是合法的:

export default {
  watch: {
    a: function {},
    b () {},
    c: () => {},
    d: async function {},
    e: async () => {}
  }
}
複製代碼

不只在 watch裏面,其餘一些 vue實例屬性,好比 createdcomputed等,只要是可能出現函數的地方,都須要考慮到這些寫法 固然,除此以外,還有 Generator函數,但我這裏不考慮,有更好的 async/await可用,爲何非要用 Generator

methods

vue實例的方法,都做爲 methods這個對象的屬性存在,每一個方法都是一個函數,因此只須要將原 methods下的全部方法取出,轉換爲 class的方法便可,沒什麼工做量 不過須要注意的是,函數的寫法有不少,還能夠支持 async/await,這些寫法都須要考慮到

lifeCycle

vue的生命週期鉤子函數有不少,還有一些第三方的鉤子函數,例如 vue-router

const vueLifeCycle = ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed', 'errorCaptured', 'beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']
複製代碼

這些鉤子函數其實就是函數,跟 methods的處理方法同樣

component

這個比較簡單,轉化一下而後拼接

export default {
  components: {
    a: A,
    B
  },
}
複製代碼

上述轉換爲 ts對應以下:

@Component({
  components: {
    a: A,
    B
  }
})
export default class TransVue2TS extends Vue {}
複製代碼

因此就是把原 components的屬性所有映射一遍便可

mixins

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的全部數組項

provide && inject

當我考慮如何處理這兩個的時候,看了下 vue官網,官網上對於這兩個是這麼說的:

provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。

而且在這段話上,還專門用紅色感嘆號標識了一下,說白了就是不建議你在業務代碼中,由於這不利於數據的追蹤,徹底可使用成熟的 vueBus或者 vuex代替,通常也不會用到這個東西的,我寫這個轉換程序也是爲了轉換業務代碼,因此我沒有對這兩個作處理,若是發現代碼中存在這兩個屬性,會提示你本身手動處理

emit && ref

這兩個都只是一種相似語法糖的東西,能夠不作處理

文件處理

上述是針對一份 .vue文件的詳細處理的邏輯,想要真正的接入實際文件乃至文件夾的處理,天然少不了文件的讀取和更新操做,這就涉及到 node的文件處理內容了,不過並不複雜,就很少說了

npm 包

代碼寫完以後,爲了簡化使用流程,我將其打包成了一個 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語法,難點在於你要找到兩者之間全部語法的映射關係,並知道如何進行處理,因此實際上大部分都是體力活

只要你明白了這其中的套路,其實換個什麼 vuewepy,或者 react轉微信小程序,其實都是同樣,都是翻譯器,都是體力活,只不過有些很輕鬆,也就是搬幾塊磚的事情,而有些體力活比較辛苦還須要動腦子罷了

相關文章
相關標籤/搜索