场景
很多时候,我们需要基于一些 UI
框架进行二次封装,这里以 Element UI
为例,封装一个 Input
组件类似如下:
<!-- Input 组件 -->
<template>
<div>
<el-input v-model="myc"></el-input>
</div>
</template>
这个时候,我们需要保证外面能够直接设置 el-input
的属性,比如 placeholder
、clearable
等等,最好能够透传
直接设置
第一反应,我们想到的就是,通过 props
传值进来,然后一个个的设置,如下所示:
<template>
<div>
<el-input v-model="myc"
:placeholder="configProps.placeholder"
:clearable="configProps.clearable"></el-input>
</div>
</template>
上面就是通过传入的 props
—— configProps
,来设置 placeholder
和 clearable
但是这样代码可读性差、维护不方便、而且还会有遗漏的点
通过 v-bind="$attrs" 进行透传
其实我们在一个组件内部没有声明任何 prop
时,调用该组件,传入相关的属性,会直接将属性传到根节点上,如下:
<!-- Input 组件 -->
<template>
<div class="hello input-con">
<label>输入框:</label>
<el-input ></el-input>
</div>
</template>
<script>
export default {
name: 'Input'
}
</script>
调用上面的组件
<Input placeholder="我是默认值"
:clearable="true"/>
那怎么才能将属性传递到内部的 el-input
组件中呢,直接给 el-input
加一个 v-bind="$attrs"
即可。
先看官方对 vm.$attrs
的定义:
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
大白话:调用一个组件的时候传入属性 (class
和 style
除外),而且不在该组件内部的 props
中声明,就可以通过 v-bind="$attrs"
传入该组件的内部组件
比如,上面调用 Input
组件的时候传入了 placeholder
、clearable
,然后 Input
组件内部并没有声明 placeholder
、clearable
去接收这两个 props
,这个时候可以通过 v-bind="$attrs"
传入内部组件 el-input
中
具体的代码示例如下
<template>
<div>
<el-input v-bind="$attrs" ></el-input>
</div>
</template>
看到的结果如下:
完整的代码示例放在了 codesandbox
中了,可以在线看下——普通的 v-bind="$attrs",建议大家自己试下
动态组件如何透传
虽然上面可以解决了大部分的问题了,但同事发现并不能满足场景,主要是他用了动态组件 component
。他的想法是通过 JSON Schema
的方式生成表单,其中应用了动态组件 component
,这是一个很棒的想法,相信现在很多公司应该都有类似的做法。
我们来看下他的实现思路:
<!-- 动态组件,根据 JSON 配置 调用 Input 或者 Select 组件等等 -->
<template>
<div class="hello">
<div v-for="(config, index) in configJsonArr" :key="config.type + index">
<!-- 动态组件,根据配置中的 Type 来决定调用的是 Input 还是 Select -->
<component :is="config.type" :configProps="config.props"></component>
</div>
</div>
</template>
<script>
import Input from './Input'
import Select from './Select'
export default {
name: 'Config',
components: {
Input,
Select
},
props: {
// 动态组件 JSON schema 的配置
configJsonArr: {
type: Array,
required: true,
default: () => []
}
}
}
</script>
其中 configJsonArr
为如下:
[{
type: 'Input',
props: {
placeholder: '我是默认值',
clearable: true
}
}, {
type: 'Select',
props: {
placeholder: '我是默认值'
}
}, {
type: 'Input',
props: {
placeholder: '我是默认值',
suffixIcon: 'el-icon-delete'
}
}]
其中 Input
组件类似如下:
<template>
<div class="hello input-con">
<label>输入框:</label>
<el-input ></el-input>
</div>
</template>
<script>
export default {
name: 'Input'
}
</script>
这个时候,假如我们直接在 el-input
设置 v-bind="$attrs"
是不行的,原因在于动态组件传入的属性 configProps
是一个对象,而不是解构后的对象属性,那怎么办呢?
我们可以使用渲染函数!
渲染函数大显神威
说来惭愧,之前很少用到渲染函数 (render
) 函数,来了新的公司之后,发现这边用得还挺多(大家都好厉害),自己也学习了一下。
上面提到的在标签中没法解构属性,在渲染(render
)函数中就可以解决,先来大致的了解下渲染函数,这里主要还是参考官方文档
渲染函数中的第一个参数是 createElement
,其接受的参数如下(注意第一个和第二个参数):
第一个参数可以是一个 HTML
标签名、组件选项对象,或者 resolve
了上述任何一种的一个 async
函数。必填项。这里我们挂载的是我们的 Input
组件等。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
我们再将重点放在第二个参数 Object
中,我们可以在这个 Object
中指定相关的属性值,比如 class
、style
、attrs
(普通的 HTML attribute
)、组件的 props
【这个就是我们这一期重点关注的属性值】…。具体如下:
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
可以看到,我们可以在上面这个对象中设置 props
属性的值的时候,将它解构掉就可以了。
核心代码实现,如下所示:
// 这其实就是一个组件
const CompFormItem = {
components: {
Input, Select
},
name: 'FormItem',
props: {
// 传入配置
configJson: {
required: true
}
},
// h 实际上就是 createElement 参数
render (h) {
// 第一个参数就是配置中的 type,也就是我们的组件名称
return h(`${this.configJson.type}`, {
props: {
// 针对 props 进行解构
...this.configJson.props || {}
},
attrs: {
// 针对 attrs 进行解构
...this.configJson.props || {}
}
})
}
}
这样我们再在 Input
组件中写上 v-bind="$attrs"
就可以了
<template>
<div class="hello input-con">
<label>输入框:</label>
<el-input v-bind="$attrs"></el-input>
</div>
</template>
<script>
export default {
name: 'Input'
}
</script>
结束语
以上通过渲染函数就可以完全解决透传属性的问题了,具体的我也放在了 codesandbox
中了——动态组件透传属性。也提供一下 GitHub 地址【这么用心,求个赞应该可以吧?】
其实我也还在想,是不是还有其他的方法?欢迎大家评论提出自己的想法和建议
往期优秀文章推荐
- 前端应该知道的 HTTP 知识【金九银十必备】
- 最强大的 CSS 布局 —— Grid 布局
- 如何用 Typescript 写一个完整的 Vue 应用程序
- 前端应该知道的web调试工具——whistle
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Gopal 原文链接:https://juejin.im/post/6865451649817640968