记录下我对组件库二次封装的体会,以及部分优化,有误望告知
技术:使用的是wot design uni组件库的 <wd-skeleton>组件
目的:通过骨架屏来增加用户等待体验。
呈现效果:在小程序上有骨架屏作为loading的等待,兼容浏览器的响应式网页和小程序
做二次封装的目的:组件库的组件部分功能不满足需求,譬如骨架屏,发现源码使用v-if,因此不能并行渲染,所以后续有优化。
组件地址:Skeleton 骨架屏 | Wot Design Uni
一阶段:
初始阶段,就是很直白的引入组件,在同个界面下使用 <wd-skeleton>,定义需要的值进行传递。
缺点:可复用性和可拓展性差,重复性高,多个页面维护困难
<template>
<wd-skeleton
animation="gradient"
theme="paragraph"
:custom-style="showSkeleton ? { padding: '12rpx' } : {}"
:loading="showSkeleton"
:row-col="rowcol">
<view>
<!--其他内容-->
</view>
<wd-skeleton>
</template>
<script lang="ts" setup>
const showSkeleton = ref(true)
const rowcol = reactive([
{ width: '100%', height: '110rpx' },
{ width: '74%', height: '48rpx', marginTop: '-12rpx' },
{ width: '42%', height: '54rpx', marginTop: '-12rpx', marginBottom: '18rpx' },
{ width: '40%', height: '64rpx', marginTop: '36rpx', marginBottom: '16rpx' },
{ width: '100%', height: '108rpx' },
{ width: '100%', height: '300rpx' },
])
// 还有在函数里,接口数据请求成功后,将showSkeleton 设置false隐藏
async function queryProjectInfo(Id) {
showSkeleton .value = true
const query = {
id: Id,
}
const { errcode, data } = await getData(query)
if (errcode === 0) {
// 其他判断
// XXX
}
showSkeleton .value = false
}
</script>
二阶段:
抽离公用的组件Skeleton,在component下定义所对应的skeleton组件,定义样式和数据,传值给公共组件Skeleton,然后在目标页面下引入定义的skeleton组件,相当于两次值传递
优点:可复用性提高了,提升了可维护性和可拓展性
缺点:重复性依旧很高,额外封装的页面结构相似性高
公共组件Skeleton.vue
<route lang="json5" type="component">
{
desc: '自定义业务骨架屏',
}
</route>
<template>
<wd-skeleton
animation="gradient"
theme="paragraph"
:custom-style="props.customStyle"
:loading="props.loading"
:row-col="props.rowcol"
></wd-skeleton>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
const props = defineProps({
customStyle: {
type: Object as () => Record<string, string | number>,
default: () => ({}),
},
loading: {
type: Boolean,
default: true,
},
rowcol: {
type: Array as () => Array<{
width: string
height: string
marginTop?: string
marginBottom?: string
}>,
default: () => [],
},
})
</script>
<style lang="scss" scoped></style>
定义的组件AnalyzeSkeleton.vue
<template>
<view style="position: relative">
<Skeleton
:custom-style="showContent ? { padding: '12px' } : {}"
:loading="showSkeleton"
:rowcol="rowCol"
></Skeleton>
</view>
</template>
<script lang="ts" setup>
import Skeleton from '@/components/skeleton/index.vue'
defineProps({
showSkeleton: {
type: Boolean,
default: true,
},
})
const rowCol = reactive([
{ width: '100%', height: '110rpx' },
{ width: '74%', height: '48rpx', marginTop: '-12rpx' },
{ width: '42%', height: '54rpx', marginTop: '-12rpx', marginBottom: '18rpx' },
{ width: '40%', height: '64rpx', marginTop: '36rpx', marginBottom: '16rpx' },
{ width: '100%', height: '108rpx' },
{ width: '100%', height: '300rpx' },
])
</script>
<style scoped>
引入该组件
<template>
<AnalyzeSkeleton :loading="showSkeleton">
<view>
<!--其他内容-->
</view>
<AnalyzeSkeleton >
</template>
<script setup lang="ts">
import AnalyzeSkeleton from './components/skeleton.vue'
const showSkeleton = ref(true)
// 还有在函数里,接口数据请求成功后,将showSkeleton 设置false隐藏
async function queryProjectInfo(Id) {
showSkeleton.value = true
const query = {
id: Id,
}
const { errcode, data } = await getData(query)
if (errcode === 0) {
//其他判断
XXX
}
showSkeleton .value = false
</script >
虽然提高了可复用性,但还未能很好的封装,冗余还是比较多,有点像脱裤子放屁,多此一举
三阶段:
将骨架屏结构定义通过ts文件维护,复用公用的组件Skeleton就行,通过不同的值进行结构切换。
公共组件Skeleton.vue
<route lang="json5" type="component">
{
desc: '自定义业务骨架屏',
}
</route>
<template>
<wd-skeleton
animation="gradient"
theme="paragraph"
:custom-style="props.customStyle"
:loading="props.showLoading"
:row-col="rowColMap[props.rowcol]"
></wd-skeleton>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import { rowColMap } from './rowCol'
const props = defineProps({
customStyle: {
type: Object as () => Record<string, string | number>,
default: () => ({}),
},
showLoading: {
type: Boolean,
default: true,
},
rowcol: {
type: String,
default: 'PRJ_DETIAL',
},
})
</script>
<style lang="scss" scoped>
</style>
ts文件定义骨架屏结构
export const rowColMap = {
ANALYZE_SKELETON: [
{ width: '100%', height: '110rpx' },
{ width: '74%', height: '48rpx', marginTop: '-12rpx' },
{ width: '42%', height: '54rpx', marginTop: '-12rpx', marginBottom: '18rpx' },
{ width: '40%', height: '64rpx', marginTop: '36rpx', marginBottom: '16rpx' },
{ width: '100%', height: '108rpx' },
{ width: '100%', height: '300rpx' },
],
// 其他的XXX
}
export const rowColKeys = {
ANALYZE_SKELETON: 'ANALYZE_SKELETON',
// 其他的XXX
}
引入该组件
<template>
<AnalyzeSkeleton
:show-loading="skeletonLoading"
:custom-style="skeletonLoading ? { padding: '12px' } : {}"
:rowcol="rowCol"
>
<!--其他内容-->
</<AnalyzeSkeleton>
</template>
<script ts='lang' setup>
import AnalyzeSkeleton from '@/components/skeleton/skeleton.vue'
import { rowColKeys } from '@/components/skeleton/rowCol'
const rowCol = rowColKeys.ANALYZE_SKELETON
// 改了下命名
const skeletonLoading = ref(true)
// 还有在函数里,接口数据请求成功后,将showSkeleton 设置false隐藏
async function queryProjectInfo(Id) {
skeletonLoading.value = true
const query = {
id: Id,
}
const { errcode, data } = await getData(query)
if (errcode === 0) {
//其他判断
XXX
}
skeletonLoading.value = false
</script>
优点:抽离出来,复用性和维护性提高,只需要维护ts文件就可以增加修改结构
p.s. 目前只做到这一步,可能还有更好的封装方案(dog.jpg
其他优化:
看了下<wd-skeleton>组件的代码,发现使用的v-if,因此会等骨架屏消失后再渲染,希望在加载骨架屏的同时能并行
<wd-skeleton>组件代码如下:
<template>
<view :class="`wd-skeleton ${customClass}`" :style="customStyle">
<view class="wd-skeleton__content" v-if="show">
<view class="wd-skeleton__row" v-for="(row, index) of parsedRowCols" :key="`row-${index}`">
<view v-for="(col, idx) of row" :key="`col-${idx}`" :class="col.class" :style="col.style" />
</view>
</view>
<view v-else>
<slot />
</view>
</view>
</template>
设计了三套方案:
1. 不传进<wd-skeleton>的插槽中,额外插槽使用v-show
2. 做个补丁对<wd-skeleton>处理
3. 将未加载的页面做偏移隐藏,后续骨架屏消失后再显示出来
后面考虑后使用了第三种方案,原因:
第一种,使用v-show可行,但是由于部分页面使用了雷达图等,是根据页面高度计算的,使用v-show会显示不出来
第二种,会对该组件产生全局影响,暂时不考虑
所以考虑了偏移
1. 使用absolute进行脱标处理
2. 使用translate进行偏移
3. 使用overflow:hidden,隐藏超出的页面,避免页面滚动
使用到了windicss改变样式
<route lang="json5" type="component">
{
desc: '自定义业务骨架屏',
}
</route>
<template>
<view class="bg-#f2f5f7 relative" :class="[props.showLoading && 'max-h-100vh overflow-hidden']">
<wd-skeleton
animation="gradient"
theme="paragraph"
:custom-style="props.customStyle"
:loading="props.showLoading"
:row-col="rowColMap[props.rowcol]"
></wd-skeleton>
<view :class="{ slotClass: props.showLoading }">
<slot></slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import { rowColMap } from './rowCol'
const props = defineProps({
customStyle: {
type: Object as () => Record<string, string | number>,
default: () => ({}),
},
showLoading: {
type: Boolean,
default: true,
},
rowcol: {
type: String,
default: 'ANALYZE_SKELETON',
},
})
</script>
<style lang="scss" scoped>
.slotClass {
position: absolute;
top: 0;
width: 100%;
height: auto;
transform: translateX(100%);
}
</style>
结果():