09月12, 2019

透明封装 Vue 组件

在项目中我们通常会有封装一个现有组件的需求,为组件添加一些额外的特性或配置。例如封装 textarea 以提供自动调整大小的功能、封装第三方组件库中的某个组件以提供额外的默认配置等。

textarea 为例,textarea 有一些常用的属性如 rowscolsplaceholder 等,也有一堆事件如 inputkeydown 等。如果直接对其封装的话就意味着要把一堆属性和事件绑定到新的组件上,如下:

<template>
    <textarea
      :rows="rows"
      :cols="cols"
      :placeholder="placeholder"
      @input="onInput"
      @keydown="onKeydown"
    ></textarea>
</template>
<script>
    // ....
</script>

不仅要在模板中把所有的属性和事件一一罗列出来,还要在 script 中注册属性、写事件代码,相当繁琐。所以有没有一种方式可以一劳永逸,代替这种一一罗列的方式?

绑定属性

Vue 中的属性被强行分为两部分,即 attrsprops,这点着实不如 React 来得简洁。Vue 官方文档中对 attrs 的定义如下:

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (classstyle 除外)。

其实有点绕。首先 classstyle 是被特殊处理的,这两者除外。先说 props,这个比较好理解:组件中以 props 的形式定义的属性被称为 props

Vue.component('my-component', {
    props: {
        type: String,
        status: Number,
        title: String
    }
})

上面的例子中,typestatustitle 就是 props。那么什么是 attrs 呢?除了 props 以外的,其他全是 attrs

<my-component title="hello" :status="1" rows="3" placeholder="Contents"></my-component>

上面的例子中,titlestatus 是在 props 中定义的,属于 props;而 rowsplaceholder 没有在 props 中定义,所以 rowsplaceholder 属于 attrs

Vue 实例中的 vm.$attrsvm.$props 分别表示当前实例的 attrsprops 属性,结合 Vue 文档中 v-bind 的对象绑定语法,可以完成对组件属性的透明封装,如下:

Vue.component('my-textarea', {
    template: '<textarea v-bind="$attrs"></textarea>'
})

当组件中同时有 attrsprops 的时,以上代码可能不太适用,因为一个元素上面只能绑定一个 v-bind。对以上代码做一些修改,合并 attrsprops,并传递给 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-cardel-card 为例演示了 slot 的使用。但这种方式只能处理 default 插槽,针对其他具名插横槽则没有找到相对应的方法。

参考资料:

本文链接:https://icewing.cc/post/transparent-wrap-vue-component.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。