时间:2021-07-04 09:25:27 | 栏目:vue | 点击:次
代码的复用是一件很常见的事情,如果是公共代码的复用那还好说,直接做成一个内部私有库,想用的话安装一下 npm包就行了,但是业务代码的复用就不好做成包了,一般都是复制粘贴
我一般写代码的时候,如果觉得某段业务代码以前见过其他人写过,那么考虑到业务优先性,只要别人的代码不是写得太烂,我一般会优先抄别人的代码,省得自己再写一遍
然后我就遇到了一个问题,公司目前前端项目大部分都是 vue,早期没有 ts这个说法,后来新项目才逐渐引入 ts,所以新项目用的是 vue-ts,而一般想抄的老代码都是没有引入 ts的,固然,这二者是可以兼容存在的,但对于有着轻微代码洁癖的我来说,还是不想看到同一个项目代码里掺杂着 ts和非 ts两种写法的,所以只要有时间,我都会尽量手动把老代码转化为 ts规范的
难度倒是没多少,只不过每一份都要手动转一遍,转得多了我忽然陷入沉思,我好像 repeat myself了啊,不太能忍,于是决定写一个自动将 vue-js转成 vue-ts的工具
这个工具的代码已经被我放到 github 上了,并且为了方便使用,我已经将其做成了一个 npm 包,感兴趣的可以亲自试一下
@babel
涉及到 js语法转换的东西,第一时间想到的就是 babel了,babel早就提供了丰富完善的 js语法的解析与反解析工具
@babel/parser
@babel/parser 是负责解析 js语法的工具,可以理解为将 js语法转化为 ast,方便开发者进行自定义处理,通过 plugins来支持多种 js语法,例如 es6、es7、ts、flow、jsx甚至是一些实验室的语法(experimental language proposals)等
例如:
const code = 'const a = 1' const ast = require("@babel/parser").parse(code)
转换后的 ast就是一个对象,数据结构描述的就是 const a = 1这个表达式
对这个 ast进行遍历,就可以获得所有当前解析的 js语法的信息,自然也能对其进行修改
@babel/generator
有解析就有反解析, @babel/generator用于将@babel/parser解析出的 ast转化回字符串形式的 js代码
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对应的值,可能是 type,type 分为单类型(例如 Number),以及类型数组(例如 [Number, String]);可能是一个对象,这个对象下的属性最少为 0个,最多为 4个,如果这个对象存在一个属性名为 type的属性,则这个属性的值也需要判断单类型和类型数组,其他属性直接取原值即可
无论 props对象的属性值是对象还是 type,都需要处理 type,所以一个专门处理 type的方法 handlerType
如此一来,如果是 type,则 handlerType直接处理好;如果是对象,则遍历这个对象的属性,发现属性是 type,则调用
handlerType进行处理,否则直接原样作为 @Prop的参数即可
data
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,但是我看了下,其实这里写两段逻辑也并不复杂,所以我就不严格规定处理的顺序了
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的修饰器,因为 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上的箭头函数时,给你一个提示
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下的每个属性都是一个需要进行 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
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语法翻译成 vue-ts语法,难点在于你要找到二者之间所有语法的映射关系,并知道如何进行处理,所以实际上大部分都是体力活
只要你明白了这其中的套路,其实换个什么 vue 转 wepy,或者 react转微信小程序,其实都是一样,都是翻译器,都是体力活,只不过有些很轻松,也就是搬几块砖的事情,而有些体力活比较辛苦还需要动脑子罢了