这篇文章主要介绍了vue和react中的插槽怎么用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇vue和react中的插槽怎么用文章都会有所收获,下面我们一起来看看吧。
简述Slot
slot插槽是Vue对组件嵌套这种扩展机制的称谓,在react可以也这样称呼,但是并不很常见。不过叫slot确实很形象。
这样的形式就是slot插槽:
vue
<template>
<container-comp>
<content></content>
<footer></footer>
</container-comp>
</template>
react
() => (
<ContainerComp>
<Content />
<Footer />
</ContainerComp>
)
(我们可以把container-comp称之为容器组件,把content、footer称之为子组件)
这种机制的好处主要在于,在某个容器提供的模版或者数据中,可以根据需求灵活扩展需要渲染的子组件。专业点说就是通过容器和子组件之间的协议(数据交换和渲染方式),将彼此逻辑独立解藕,提升了各自的复用性。
举个例子,容器组件提供了一份渲染模版,将各个模块的位置预留出来,使用的时候根据各个子组件的顺序或者插槽名称,在不同场景可以选择不同的子组件。
再举个使用插槽的例子,容器组件中提供一些数据,比如定时请求某种接口得到数据,或者监听,订阅一些数据,比如在挂载完成后监听鼠标事件,得到位置数据。数据拿到之后,具体的对数据的渲染方式交给子组件来做,而这时候,容器可以通过参数传递的方式将得到的数据交给子组件。
通过上面简单的的例子描述,可以看到,使用插槽的的代码设计符合单一职责原则,逻辑更加内聚。
而不管是vue还是react上述描述的这些功能都是支持的,只是有的叫法略有所别,但是其目的一致。(因为近期在项目中使用vue多一点,而之前对vue的了解只是笼统的学习过响应式原理,并没有真正在项目里写过vue,所以,近期希望结合vue和react,来系统的回顾回顾相关的知识点。)
下面就先看下vue中的插槽都有什么功能。一边看vue,一边对比react。 参考这里:vue2官网:slot,可以看到,插槽相关的核心功能有:
基本渲染插槽内容;
具名插槽:一个容器多个插槽,需要分别命名;
插槽传递参数,属于相对高级点的用法;
基本插槽
vue基本插槽
最简单插槽用法就像下面这样:
<template>
<main-comp>
<div>内容</div>
<!--
可以是任何自定义组件
<my-sub></my-sub>
-->
</main-comp>
</template>
main-comp就是容器组件,而我们将
<div>内容</div>
作为容器的子元素,那么,容器里面怎么写呢?<template>
<div class="main">
<slot>后备信息,以防万一</slot>
</div>
</template>
可以看到,在容器里面简单的使用slot标签,相当于占位。当组件渲染的时候,
<slot></slot>
将会被替换为<div>内容</div>
,如果没有使用时插槽的话,会渲染出slot标签内的内容:“后备信息,以防万一”。当然,使用的插槽不止是<div>内容<div>
这么简单,可以是任何自定义组件。ok,对应react中实现对应的代码怎么写呢?
react基本插槽
使用插槽组件:
() => (
<MainComp>
<div>内容</div>
{/* <MySub /> 可以是任何自定义组件 */}
</MainComp>
)
容器中定义插槽:
const MainComp = (props) => {
return (
<div class="main">
{props.children ?? '后备信息,以防万一'}
</div>
)
}
react中,组件的子组件都存在props.children中,所以直接在jsx中渲染对应的位置渲染props.children变量就可以了,而后备内容可以应用任何js语法,来判断props.children收否存在,从而显示后备内容与否。
具名插槽
vue具名插槽
上面是最简单的场景,但有时候,一个容器需要渲染很多插槽,比如需要渲染一个内容区域content和一个底部区域footer,这时候就需要对插槽命名了,称之为具名插槽: 下面main-comp组件要使用两个插槽,分别命名为content和footer:
<template>
<main-comp>
<template v-slot:content>
<sub-comp1></sub-comp1>
</template>
<template v-slot:footer>
<footer-comp></footer-comp>
</template>
</main-comp>
</template>
定义插槽:
<template>
<div>
<slot name="content"></slot>
<slot name="footer"></slot>
</div>
</template>
slot用name属性标注了其名字“xxx”,对应使用的时候要用
v-slot:xxx
,这样“xxx”的template就会对应替换成name是xxx的slot的位置。有一点需要注意,v-slot这个指令对template生效,也就是说,要使用具名插槽,必须要用template将内容包裹起来。另外,如果slot显示指定name,其实它对应也是有name的,它默认的name叫default。
react具名插槽的讨论
具名插槽,对应react的话,我用了这些年react,还真没听过“具名插槽”这个称谓。不过尽管没有100%一致的对应vue的具名插槽,但类似的功能有几种实现方式:
模仿具名插槽
a. 用次序约定:
我们知道react的“插槽”写法(嵌套子组件),子组件都是作为props.children数组的子元素,那么其实最简单的一种方式就是,children的次序对应着某个子插槽。比如:
使用时:
() => {
return (
<MainComp>
<SubComp1 />
<FooterComp />
</MainComp>
)
}
那么,
props.children[0]
对应的就是SubComp1,而props.children[1]
对应的就是FooterComp,所以在MainComp内部就可以这样:const MainComp = (props) => {
const content = props.children[0]
const footer = props.children[1]
return (
<div>
{ content }
{ footer }
</div>
)
}
但是上述写法的问题在于:具名呢?说好的名称呢?使用的时候顺序乱了咋办?
b. 传递对象:
想要实现具名,可以下面这样写法,本质还是props.children,我们把对象作为“插槽”内容,对象的key就是插槽名称,value就是子组件:
() => {
return (
<MainComp>
{
{
content:(<SubComp1 />),
footer:(<FooterComp />)
}
}
</MainComp>
)
}
对应的MainComp:
const MainComp = (props) => {
const { content, footer } = props.children
return (
<div>
{ content }
{ footer }
</div>
)
}
这里这个props.children可以直接解构对象的属性。(有一点要注意的是:props.children不一定是数组,当只有一个元素的时候就不是数组)。
c. 判断组件的自定义静态,实现具名
上述这种写法看上去和vue的功能一致了,但是坦白讲,这样的代码在react世界里面实属罕见。不是说写法错误,但是似乎不那么符合使用习惯。
react中类似具名的插槽其实还可以通过给子组件显示命名方式实现,其实就是给子组件挂了个静态变量:
const Content = () => (<div>I am Content</div>)
const Footer = (props) => (<div>Footer Here {props.info}</div>)
// 两个子组件标记出来
Content.compName = 'content'
Footer.compName = 'footer'
这样在容器组件中就能通过这两个标记,识别出对应的组件:
export function MainComp(props) {
const isMany = React.Children.count(props.children) > 0
let footer = (<div>footer</div>)
let content = (<div>content</div>)
if (isMany) {
React.Children.forEach(props.children, item => {
const { compName } = item.type
// 判断子组件类型
if (compName === 'footer') footer = item
if (compName === 'content') content = item
})
}
return (
<div>
title text<br/>
{ content }
{ footer }
</div>
)
}
这里面用了几个React.Children的方法来判断props.children,核心逻辑是当children是数组时,遍历每一个子项,判断其“name”(这里我们的约定为compName),再根据name设置对应需要渲染的子组件的变量。
isMany部分也可以这样写:
try {
React.Children.only(children)
} catch (e) {
React.Children.forEach(children, item => {
const { compName } = item.type
if (compName === 'footer') footer = item
if (compName === 'content') content = item
})
}
这样我们在使用“插槽”组件的时候就不用担心子组件次序问题了:
() => {
return (
<MainComp>
<Footer /> {/* 先写footer还是能正确的渲染出来 */}
<Content />
</MainComp>
)
}
上面a、b、c三种方法实现了和vue一样的具名的slot,第一种只是简单的次序对应,第二种能实现,但是不太符合react习惯,第三种能实现,也是react的写法。
不过,一般简单的需求,没必要用第三种,可以直接使用属性传递组件,我也不知道应该怎么叫,就叫属性插槽吧。
属性插槽
一个更符合react习惯,近似实现vue具名slot的方法是,直接传props,但props的类型是组件:
() => {
return (
<MainComp
content={(<SubComp1 />)}
footer={(<FooterComp />)}
/>
)
}
这样在MainComp的内部直接通过props去拿对应的组件并渲染在适当的位置就行:
const MainComp = (props) => {
const { content, footer } = props
return (
<div>
{ content }
{ footer }
</div>
)
}
严格意义上说,虽然这样能实现和vue具名插槽一样的功能,但使用却不是用插槽的形式。 但是这样代码在react世界中,却是最常见的方式。这可能是两种框架不同特点导致的微小差异了。
插槽传参
vue插槽传参
插槽传递参数,可以帮助我们实现一些更高级的功能。
先看下vue中,如何给插槽传递参数:
<template>
<div class="main">
<slot
:styleProps="shareStyle"
:data="shareStyle"
:description="desc">
</slot>
</div>
</template>
上面的代码是定义slot时候,我们给slot绑定了三个属性,这看上去和我们使用组件,给组件传递参数的用法没有什么区别。
使用slot的时候这样接收参数:
<!-- slotProps 是通过main传递过来的 -->
<template v-slot:default="slotProps">
<!-- 默认插槽可以简写成 v-slot -->
<div :>{{ slotProps.description}}</div>
</template>
首先我们看到使用的时候在template标签中使用了指令v-slot,即
v-slot:default="slotProps"
,这句话什么意思呢?就是当前这里template对应default这个名称的slot(defalut可以省略,上面定义slot的地方也没写name=“default”,其实是省略了)。
而这个slotProps表示所有传递过来的属性,也就是说:
slotProps = {
styleProps, data, description
}
所以在template中,可以使用slotProps上的任何属性。也可以给作为插槽的自定义组件传递参数, 比如:
<template v-slot:rightItem="slotProps">
<staff :staff="slotProps.data"></staff>
</template>
vue插槽传参大致就是这样了,接着看看react吧。
react:render-props
react中其实没法直接给插槽传递参数,只能借助一点技术手段:函数。
这种方式有个专有名词叫:render-props。
render-props的具体的方式就是,子组件作为插槽是用函数的形式,而容器组件渲染的时候对应的就调用这个函数,在调用函数的时候,把需要传递的参数传入函数,这样在插槽函数的作用域内就拿到了数据:
() => {
return (
<MainComp>
{
(data) => (<Staff staff={data} />)
}
</MainComp>
)
}
看下容器MainComp组件:
const MainComp = (props) => {
const [data, setData] = useState({})
useEffect(() => {
const info = await getData()
setData(info)
}, [])
return (
<div>
{
props.children && props.children(data)
}
</div>
)
}
当然了,这种函数形式的“插槽”不止可以用作插槽,用在普通的props上自然也是可以的。在react世界里,凡是要从另一个组件拿数据的场合,都可以考虑传个函数:
() => {
return (
<MainComp
staff={(data) => (<Staff staff={data} />)}
/>
)
}
对应的MainComp,最终也是通过函数调用给子组件传递参数,只是获取子组件的方式换一下:
const MainComp = (props) => {
const [data, setData] = useState({})
useEffect(() => {
const info = await getData()
setData(info)
}, [])
return (
<div>
{/* 这里变一下 */}
{
props.staff && props.staff(data)
}
</div>
)
}