手把手系列文章,仿element-plus框架封装一套自己的组件库。
学习element-plus框架源码,有助于我们更好的理解组件设计,加深对vue的理解。
今天要写的是Space组件。
1.环境准备
克隆element-plus代码到本地
git clone https://github.com/element-plus/element-plus
cd element-plus
pnpm i
pnpm run docs:dev
当然我更推荐用我的这个组件库做调试,既实现了组件的基本功能,代码复杂性也会小很多。
git clone https://github.com/bubuui/bubu-ui
cd bubu-ui
npm i
npm run docs:dev
2.需求分析
开始阅读源码之前,先分析一波需求。
先看最基础的用法。
<el-space wrap>
<el-button>1<el-button>
<el-button>1<el-button>
</el-space>
组件需要提供一个容器,然后设置flex布局,可以根据el-space上的属性,来动态控制是否换行,水平排列还是垂直排列。
看起来,好像只需要提供一个容器组件就行。
但是,如果有size,好像不太好实现了。
<el-space :size="20">
<el-button>1<el-button>
<el-button>1<el-button>
</el-space>
因为我们只有容器,size需要设置子元素的样式。
那么怎么操作子元素呢?
是不是可以像form组件一样,再封装一个space-item组件呢?
我们来看看element-plus是怎么实现的。
3.阅读源码
从目录的文件就可以看出来,还真有item。
而且没有.vue文件,那大概率就是通过api创建的组件。
先找入口文件——index.ts。
虽然有Item,但是注册的只有Space组件。(withInstall就是提供一个install方法,并使用app.component注册组件)
我们继续看-Space.ts
renderSlot渲染插槽,该函数的作用是渲染slots里面的default插槽内容。
返回的children是一个Fragment虚拟节点,子节点内容为default插槽数组。
这么说有点抽象,我们看一下这个函数的定义。
其实就是取出slots里面的default插槽。
相当于如下代码
const children = createBlock(Fragment, {key: 0}, slots['default']?.(props))
了解了这个函数之后,我们继续看extractChildren这个函数。
创建了一个虚拟节点,并把child作为默认插槽传入。
Item组件就很简单了,就是渲染一下默认插槽。
也就是说,Space给我们自动包了一层Item组件。
<el-space :size="20">
<el-button>1<el-button>
<el-button>1<el-button>
</el-space>
// 实际渲染的是
<el-space :size="20">
<item>
<el-button>1<el-button>
</item>
<item>
<el-button>1<el-button>
</item>
</el-space>
ps(有些人可能会奇怪,为什么一会用h函数,一会用createVNode函数,其实h函数内部调用的也是createVNode,只不过h函数加了一些参数处理)
4.手写Space组件
手写的思路跟element-plus是一样的。
唯一的区别是,我是用tsx语法写的。
组件实现了基本功能,ts类型检测单独提取一个文件,看上去会直观一些。
export default defineComponent({
name: 'BuSpace',
props: sapceProps,
setup(props: SapceProps, { slots }) {
const { direction, wrap, alignment, size } = toRefs(props);
const prefixCls = 'bu-space';
const classes = computed(() => [
prefixCls,
`${prefixCls}--${direction.value}`,
]);
const sizeSpace = computed(() => {
return size.value
? typeof size.value === 'string'
? size.value
: size.value + 'px'
: '8px';
});
return () => {
const children = renderSlot(slots, 'default', { key: 0 }, () => []);
if ((children.children ?? []).length === 0) return null;
return (
<div
class={classes.value}
style={{
'flex-wrap': wrap.value ? 'wrap' : 'nowrap',
'align-items': alignment.value,
}}
>
{isArray(children.children) &&
children.children.map((item: any) => {
if (item.type === Comment) {
return h(item);
} else {
return h(
'div',
{
class: 'bu-space--item',
style: {
'margin-bottom':
direction.value === 'horizontal' ? 0 : sizeSpace.value,
'margin-right':
direction.value === 'horizontal' ? sizeSpace.value : 0,
},
},
item
);
}
})}
</div>
);
};
},
});
总结一下
space组件功能很简单,但是内部实现细节还是挺多的。
由于需要对默认插槽做包装,所以通过手动渲染,调用api的方式进行组件封装。(h函数,createVnode函数)
学习了renderSlot函数的使用。(这个api在官方文档好像并没有介绍)
也通过tsx语法,手写了一个Space组件,并实现了基本功能。