代码的复用是一件很常见的事情,若是是公共代码的复用那还好说,直接作成一个内部私有库,想用的话安装一下 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
转微信小程序,其实都是同样,都是翻译器,都是体力活,只不过有些很轻松,也就是搬几块砖的事情,而有些体力活比较辛苦还须要动脑子罢了