解放双手-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转微信小程序,其实都是同样,都是翻译器,都是体力活,只不过有些很轻松,也就是搬几块砖的事情,而有些体力活比较辛苦还须要动脑子罢了

相关文章
相关标签/搜索