title: 透明封装 Vue 组件 date: 2019-09-12 15:13:00 updated: 2019-09-12 15:13:00 tags:
- Vue categories:
- 前端
在项目中我们通常会有封装一个现有组件的需求,为组件添加一些额外的特性或配置。例如封装 textarea
以提供自动调整大小的功能、封装第三方组件库中的某个组件以提供额外的默认配置等。
以 textarea
为例,textarea
有一些常用的属性如 rows
、cols
、placeholder
等,也有一堆事件如 input
、keydown
等。如果直接对其封装的话就意味着要把一堆属性和事件绑定到新的组件上,如下:
<template>
<textarea
:rows="rows"
:cols="cols"
:placeholder="placeholder"
@input="onInput"
@keydown="onKeydown"
></textarea>
</template>
<script>
// ....
</script>
不仅要在模板中把所有的属性和事件一一罗列出来,还要在 script 中注册属性、写事件代码,相当繁琐。所以有没有一种方式可以一劳永逸,代替这种一一罗列的方式?
绑定属性
Vue 中的属性被强行分为两部分,即 attrs
和 props
,这点着实不如 React 来得简洁。Vue 官方文档中对 attrs
的定义如下:
包含了父作用域中不作为
prop
被识别 (且获取) 的特性绑定 (class
和style
除外)。
其实有点绕。首先 class
和 style
是被特殊处理的,这两者除外。先说 props
,这个比较好理解:组件中以 props
的形式定义的属性被称为 props
。
Vue.component('my-component', {
props: {
type: String,
status: Number,
title: String
}
})
上面的例子中,type
、status
、title
就是 props
。那么什么是 attrs
呢?除了 props
以外的,其他全是 attrs
。
<my-component title="hello" :status="1" rows="3" placeholder="Contents"></my-component>
上面的例子中,title
和 status
是在 props
中定义的,属于 props
;而 rows
和 placeholder
没有在 props
中定义,所以 rows
和 placeholder
属于 attrs
。
Vue 实例中的 vm.$attrs
和 vm.$props
分别表示当前实例的 attrs
和 props
属性,结合 Vue 文档中 v-bind
的对象绑定语法,可以完成对组件属性的透明封装,如下:
Vue.component('my-textarea', {
template: '<textarea v-bind="$attrs"></textarea>'
})
当组件中同时有 attrs
和 props
的时,以上代码可能不太适用,因为一个元素上面只能绑定一个 v-bind
。对以上代码做一些修改,合并 attrs
和 props
,并传递给 textarea
。
Vue.component('my-textarea', {
template: '<textarea v-bind="attributes"></textarea>',
computed: {
attributes() {
const attrs = this.$attrs || {}
const props = this.$props || {}
return Object.assign({}, attrs, props)
}
}
})
绑定事件
有了上面的例子,再去写绑定事件的代码就会很简单了。Vue 实例中提供了 vm.$listeners
用于获取组件绑定的事件,vm.$listeners
在官网的描述如下:
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
所以直接在模板中添加一个 v-on
即可:
Vue.component('my-textarea', {
template: '<textarea v-bind="attributes" v-on="$listeners"></textarea>',
computed: {
attributes() {
const attrs = this.$attrs || {}
const props = this.$props || {}
return Object.assign({}, attrs, props)
}
}
})
一般来说以上代码就足够透明封装一个组件了。但 textarea
比较特殊——使用 v-model
绑定 textarea
时,文本框的内容其实是放在子元素中的,并不是真正的 value
属性。所以我们需要定义一个 value
属性并放值放到子元素中。
使用 render 函数
Vue 中提供了类似于 React 的 render 函数,使用 render 函数可以更灵活得完成一些自定义的渲染工作(事实上 vue 组件中的 template 模板最终也是被渲染成功 render 函数执行的)。render 函数的官方文档参见最后面的 “参考资料” 部分。
以上面的代码为例,添加了对 v-model
的实现:
Vue.component('my-textarea', {
props: {
value: String
},
computed: {
attributes() {
const attrs = this.$attrs || {}
const props = this.$props || {}
return Object.assign({}, attrs, props)
}
},
methods: {
input: function(event) {
this.$emit('input', event.target.value)
}
},
render(createElement) {
this.$listeners.input = this.input
return createElement('textarea', {
attrs: this.attributes,
on: this.$listeners
}, this.value)
}
})
textarea
比较特殊,不过绝大多数情况下是不需要针对 v-model
做特殊处理的。
相关的代码和演示: https://codepen.io/jerrybendy/pen/XWrqowr
又一个例子: https://codepen.io/jerrybendy/pen/LYPmqbW
第二个例子使用 element-ui
中的 datepicker
作为演示,分别使用 template 和 render 函数两种方法实现了对 el-datepicker
的透明封装。代码很简单,但足以实现对属性、v-model
以及事件的代理。
第二个例子中的 my-card
以 el-card
为例演示了 slot 的使用。但这种方式只能处理 default
插槽,针对其他具名插槽则没有找到相对应的方法。
参考资料:
- Vue 自定义渲染: https://cn.vuejs.org/v2/guide/render-function.html
- Transparent Wrapper Components in Vue: https://zendev.com/2018/05/31/transparent-wrapper-components-in-vue.html
Comments
注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。