背景
最近团队准备对旧的 vue2 项目进行一波升级,其中很重要的一步就牵扯到 vue2 项目和 ts 的集成。
我们知道如果要使用 TS 来开发 vue 2 项目的话,通常有以下两种方式:
- vue.extend:使用基础 Vue 构造器,创建一个“子类”。此种写法与 Vue 单文件组件标准形式最为接近,唯一不同仅是组件选项需要被包裹在
Vue.extend()
中。 - vue-class-component:通常与
vue-property-decorator
一起使用,提供一系列装饰器,能让我们书写类风格的 Vue 组件。
目前在社区里比较流行的方式是使用第二种 vue-class-component
风格,主要是第二种风格在书写一些组件选项,如 props, mixins 时,比 vue.extend
的方式更加方便。
使用 vue-class-component 的方式书写代码时经常能看到@ + 函数名
,这个功能为什么能让我们在书写代码时比 vue extend 的方式更方便,这个功能究竟是什么,又有什么哪些用处呢?
什么是装饰器
以上@ + 函数名
就叫做装饰器,它是一种函数,最初在 python 语言中出现,然后在 es7 中被引入到前端领域。它的实现原理起源于面向对象中的一种设计模式 —— 装饰器模式。
什么是装饰器模式?
装饰器模式是面向对象中的一种设计模式,它允许你通过将对象放入包含行为的特殊封装对象中来为原对象来绑定新的行为,具体特点如下:
- 为对象添加新功能
- 不改变对象原有的结构和功能。
我们可以拿真实世界的例子来进行类比,比如穿衣服。
当你觉得冷时,你可以穿一件毛衣,当你还觉得冷时,可以再套一件外套,如果下雨,你还可以再穿一件雨衣,所有的这些衣服都“扩展”了你的基本行为,但它们不是你的一部分,如果你不再需要某件衣服,完全可以方便的脱掉。
装饰器在前端的应用
装饰器模式目前已经被广泛应用到了前端领域。在 es7 更新的提案中也将装饰器引入了 ECMAScript。(angular, nestjs vue-property-decorator)
面向对象的装饰器模式需要通过继承和组合来实现,而在引入了装饰器之后,js 和 Python 一样,现在除了能支持 OOP 的 decorator 外,直接从语法层次支持 decorator。
当然 TypeScript 里对于装饰器也已经做为一项实验性特性予以支持。另外社区也有一些成熟的库封装了日常我们经常使用到的一些装饰器,如 core decorators 还有我们上面提到的 vue-property-decorator。
接下来我们使用 es7 装饰器来实现一些业务上经常能见到的场景 - 日志系统,以及 mixins 混入。
日志系统的作用是记录系统的行为操作,它在不影响原有系统的功能的基础上增加记录环节 —— 好比你佩戴了一个智能手环,并不影响你日常的作息起居,但你现在却有了自己每天的行为记录。
而 minxis 则是一种很好的实现可复用功能的手段。拿 vue 中的mixins 举例,一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
vue class component 做了什么
最后,我们再回到文章开头,看一下当使用 class 来定义 vue 组件时(方案二),vue-class-component
以及其相关的 vue-property-decorator
这两个包是如何使用装饰器来辅助完成所需功能的。
我们来了解一下 vue-class-component
中最常见的 @component 背后做了哪些事情 ?
我们知道,当没有使用 class 方法定义 vue 组件时,通常 vue sfc 需要按规范导出如下一个选项对象:
<script>
export default {
props: {
name: String
},
data() {
return {
message: '新消息'
}
},
watch: {
message(){
console.log('message改变触发')
}
},
computed:{
hello: {
get(){
return this.message + 'hello';
},
set(newValue){}
}
},
methods:{
clickHandler(){}
}
mounted(){
console.log('挂载完毕');
}
}
</script>
这个对象会告诉 vue 在解析时要做哪些事情,比如 data 里的数据我们希望被响应式监听,比如钩子函数我们希望在某些特定时刻被调用。Vue 内部会调用 Vue.exntend
创建组件的构造函数,以便在模板中使用时,通过构造函数初始化此组件。
那如果我们用了 class 的方式来定义组件,就不能完全按照上面的写法来书写类了(比如 watch, computed 就不能像上面那么写了),一般 class 的写法如下:
<script lang="ts">
class Home extends Vue {
message = '新数据';
get hello(){
return this.message + 'hello';
}
set hello(newValue){}
clickHandler(){}
mounted(){}
}
</script>
但是不管代码怎么写,在运行的时候还是必须要按照上面那套数据格式传递给 vue 解析器让其正常解析,所以,我们就需要对 class 书写的数据进行重组和转换。比如把 message 重组后放在 data 中,hello 放在 computed 中。clickHandler 是方法,则放到 methos 中。
那怎么样把类定义组件中的属性重组转换成 vue 能够解析的标准数据结构?这就需要靠 vue-class-component
中提供的 @component 装饰器来实现了。
简化一下,@component 主要干了下列事情:
原来有个 class 风格的组件,写法如下:
class Home {
message: '新消息'
}
这个格式是不能被 vue 运行时解析的,需要通过 @component 把上述 class 风格的组件变成如下代码:
const App = Vue.extend({
// 混合功能
mixins:[{
data(){
// 初始化后拿到实例,就能拿到 message 属性
let data = new Home();
let plainData = {};
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
return plainData;
}
}],
data(){
return {
other: '其他data'
}
}
})
new App().$mounted('#app');
本质就是先初始类得到实例,拿属性组成对象,混合到渲染的组件中, 这里在代码里究竟怎么实现的,大家感兴趣可以去看源码,这里不做更多解释了。
另外需要注意的是,在做这种转换重组时,我们希望最好需要这种是无侵入的功能,让开发者无需感知,只要正常关注写的业务代码就好。
这种在不改变自身对象的基础上,动态增加额外的功能,就非常适合通过装饰器来实现了,这也是 vue-class-component
为什么会用装饰器来实现的原因了。另外vue-property-decorator
主要依赖 vue-class-component 实现,这里就不过多赘述了。
通过装饰器模式,大家能很方便的在不改变当前代码的基础上,添加一些新的功能。这种逻辑分层的方式对于代码维护和添加新功能都带来了很大的帮助。目前装饰器也广泛应用到了前端的各个领域之中,如三大框架之一的 angular, 以及最流行的 node 框架 - nestjs,它们都强绑定使用装饰器来维护代码结构的整洁。