当我们全局引用UI框架(类似于 Quasar
Framework)的时候,为了使得整个项目风格统一,需要对某些组件进行二次封装,使得可以集中管理组件风格,使得代码易于维护。
封装需求
属性传递
二次封装后的组件与被封装组件和具有同样的参数
1 2
| <xxx-xxx v-bind="$attrs"> </xxx-xxx>
|
事件传递
1 2
| <xxx-xxx v-on="$listeners"> </xxx-xxx>
|
插槽传递
1 2 3 4 5
| <xxx-xxx> <template v-for="name in $scopedSlots" :slot="name"> <slot :name="name" /> </template> </xxx-xxx>
|
vue 相关知识点
官方解释:
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定
(class
和 style
除外)。当一个组件没有声明任何
prop 时,这里会包含所有父作用域的绑定 (class
和
style
除外),并且可以通过 v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
通俗的理解就是,子组件可以通过 $attrs
可以访问父组件传过来的所有属性,但需要注意的是如果父组件所传的属性中有在子组件
props
中有过声明,那么该属性不会出现在 $attrs
对象中。
官方解释:
包含了父作用域中的 (不含 .native
修饰器的)
v-on
事件监听器。它可以通过 v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
官方解释:
当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象 property
的访问。
官方的解释让人看着头大,通俗来讲,其实默认情况就是把
$attrs
对象上没在子组件 props
中声明的属性加在子组件的根 html
标签上。
官方解释:
用来访问作用域插槽。对于包括
默认 slot
在内的每一个插槽,该对象都包含一个返回相应 VNode
的函数。
通俗讲,就是通过该属性,可以访问所有的插槽。
封装示例
为了能够实现上述两个需求,我们使用
$attrs
、$props
和$listeners
这三个属性来实现。下文将通过封装 quasar
组件中的
QTable
来举例说明
原始组件
从 QTable
API 中我们可以知道,其有63个属性,19 个插槽,9
个事件,在封装的时候,我们需要通过预设一些属性,使得表格符合我们的使用,但是又得保证其灵活性。
二次封装
具体解释见里面的备注
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <template> <q-table v-bind="$attrs" :dense="dense" v-on="$listeners"> <!--示例: 在封装组件中增加插槽,通过后备内容进行自定义,方便父组件覆盖当前插槽--> <template v-slot:top="props"> <slot name="top" v-bind="props"> <q-space /> <q-input v-model="filter" dense debounce="300" placeholder="搜索" color="primary" > <template v-slot:append> <q-icon name="search" /> </template> </q-input> </slot> </template>
<!--根据父类插槽定义,传递插槽到被封装组件--> <template v-for="slotName in scopedSlotsName" v-slot:[slotName]="props"> <!-- v-bind 是向插槽中传递参数,使得父类的插槽可以使用--> <slot :name="slotName" v-bind="props" /> </template> </q-table> </template>
<script> export default { // 默认为 true,为 true 时会把 `$attrs` 对象上没在子组件 `props` 中声明的属性加在子组件的根 `html` 标签上。 inheritAttrs: false,
props: { // 设置组件的默认值 dense: { type: Boolean, default() { return true } } },
data() { return { filter: 'filter2' } },
computed: { attrs() { // 因为 $attrs 不包含 $props 中的值,在此处对属性进行合并,然后供被封装组件使用 // 由于 Object.assign 是浅复制,所以不会影响字段的 getter 和 setter return Object.assign({}, this.$attrs, this.$props) },
// 作用域内的插槽名称 scopedSlotsName() { let keys = Object.keys(this.$scopedSlots) // 过滤掉以$开头的字段,$ 开头的是 vue 框架的值 keys = keys.filter(key => !key.startsWith('$')) // 过滤掉已经添加插槽名称 const existSlotNames = ['top'] keys = keys.filter(key => !existSlotNames.includes(key))
return keys } } } </script>
<style scoped> </style>>
|
组件使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <wrapped-table :data="data" :columns="columns" :dense="dense"> <template v-slot:body-cell-index="props"> <q-td>{{ props.value }}_1</q-td> </template> </wrapped-table> </template>
<script> import WrappedTable from './index.vue'
export default { components: { WrappedTable },
data() { return { columns: [ { name: 'index', label: '序号', align: 'left', field: 'name' }, { name: 'name', align: 'center', label: '名称', field: 'name', sortable: true } ], data: [ { index: 1, name: 'Frozen Yogurt' }, { index: 1, name: 'Ice cream sandwich' } ],
dense: false } } } </script>
<style scoped> </style>
|
参考
- 解决Vue2.x中二次封装Vue组件时批量继承属性,方法,插槽的方法
- Vue二次封装组件,并传递props和v-on事件
- 基于UI库二次组件封装
- SegmentFault 思否
- 浅谈 Vue2.4.0
$attrs 与 inheritAttrs - SegmentFault 思否
- 插槽 —
Vue.js (vuejs.org)