做骨架屏组件时遇到的问题

概述

首先,这里提到的骨架屏组件并不是指一个显示为骨架屏的组件,指的是一个容器 fallback,骨架屏和真实的组件作为两个 slot 置于 fallback 中。fallback 监听 fallback:show 事件,用于控制显示某个 slot。

实现

初版

这个需求实现起来是很简单的,通常会写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="fallback">
<slot v-if="show"></slot>
<slot name="placeholder" v-else></slot>
</div>
</template>

<script>
export default {
data () {
return {
show: false
}
},
created () {
this.$on('fallback:show', () => {
this.show = true
})
}
}
</script>

已知问题

但这种写法并不完美,因为外层元素已经写明了是 div,考虑一种情况,比如我要给 nav 中选项设置 fallback,那么这样就直接在 nav 中多加了一层 div,从 HTML 语义的角度来看是不对的,语义被改变了:

1
2
3
4
5
6
7
<nav>
<div class="fallback">
<skeleton></skeleton>
<item></item>
<item></item>
</div>
</nav>

还有一种情况,比方说有个 Flex 容器,我要给他的 item 设置 fallback,那么 fallback 加的一层 div 直接就破坏了 Flex 布局的结构:

1
2
3
4
5
6
7
<div flex="main:center">
<fallback>
<div flex-box="1"></div>
<div flex-box="1"></div>
<div flex-box="1"></div>
</fallback>
</div>

当然了,解决方法还是有的,那就是移除最外层的 Flex 容器,将 fallback 变成一个 Flex 容器,但这又回到了问题 1 上。

改版

为了将初版整合进项目,我改了很多容器元素的样式,我觉得这不是一个很好的解决方案,所以我又对他进行了改进。

理想的状态是,不要外层 div 元素,因为元素类型在开发环境中是不确定的,起到关键作用的只是两个 slot,所以只需要在两个 slot 之间切换就行了,但是这种写法在 template 中肯定无法实现,因为根元素只能有一个(即时用了 v-if 和 v-else 二选一),同时 template 也不能成为根元素

这个时候就需要使用 render 函数了,通过 createElement 动态构建一个模版:

1
2
3
4
5
render (h) {
return this.show
? h('template', {}, [this.$slots.default])
: h('template', {}, [this.$slots.placeholder])
}

与单文件组件中的 template 不一样,这里是可以作为根元素使用的,并且实现了 v-if 与 v-else 的效果,还不会被编译器理解成要设置多个根元素。

1
2
3
4
5
6
7
8
9
<fallback>
<skeleton slot="placeholder"></skeleton>
<item></item>
</fallback>

<!-- 实际效果为 -->
<skeleton></skeleton>
<!-- 或者 -->
<item></item>