分享一個(gè)自用實(shí)用骨架屏實(shí)現(xiàn)方案!
共 18801字,需瀏覽 38分鐘
·
2024-04-15 07:55
作者:背對(duì)疾風(fēng) 原文:https://juejin.cn/post/6945748911147450405
在我的前前公司,接口經(jīng)常出問題,基本每個(gè)接口都會(huì)出問題,這就要求我們前端對(duì)每個(gè)接口都要做錯(cuò)誤信息的提醒,非常麻煩,當(dāng)然這也是必須的,所以就想著做一個(gè)組件能夠幫我們自動(dòng)處理和展示這些錯(cuò)誤信息,當(dāng)時(shí)開發(fā)用的是Flutter,所以應(yīng)該叫widget。后來又做vue和小程序了,想著能不能把那個(gè)解決方法帶到vue上來,于是就有了這個(gè)
功能
-
以骨架屏的形式展示加載中,而且可以絲滑過渡到加載完成 -
加載失敗展示錯(cuò)誤信息
先看效果
原理
一般情況下,我們請(qǐng)求接口然后渲染數(shù)據(jù)時(shí)會(huì)先判斷是否有數(shù)據(jù)來來渲染不同的視圖,比如加載的展位圖,空數(shù)據(jù)的占位圖,有數(shù)據(jù)時(shí)就展示數(shù)據(jù)。
ClamView的思路則是 數(shù)據(jù)加載未完成時(shí)也給渲染的模板一個(gè)數(shù)據(jù),然后通過給負(fù)責(zé)顯示數(shù)據(jù)span、img等標(biāo)簽設(shè)置背景色和字體顏色來達(dá)到“骨架”的效果,待數(shù)據(jù)請(qǐng)求完成后,再使用動(dòng)畫將骨架隱去,完成過渡。
代碼
-
ClamView.tsx
ClamView 需要傳入一個(gè) ResponseBean點(diǎn)此了解[1] 對(duì)象 res 來判斷當(dāng)前數(shù)據(jù)的狀態(tài),還需要傳入一個(gè)加載過程中用到的假數(shù)據(jù) emptyData 用來撐起你的 span 標(biāo)簽
import { defineComponent,computed } from 'vue';
import {ResponseBean} from "bdjf_http";
import './clam_view.css'
import './skeleton.css'
/**
* 定義 ClamView 的四種狀態(tài)
* 1. LOADING res 為空或者res.code === -100 時(shí)狀態(tài)為LOADING,此時(shí)顯示骨架屏
* 2. EMPTY res.code === 0 且 res.data 為空時(shí)
* 3. SHOW res.code === 0 且 res.data 不為空時(shí)
* 4. ERROR res.code !== 0 時(shí)
**/
type ViewStatusType = 'LOADING'|'EMPTY'|'SHOW'|'ERROR';
export default defineComponent({
name:'ClamView',
props:{
res: {
type:ResponseBean,
default:()=>{
// 默認(rèn)顯示一個(gè)loading
return new ResponseBean().loading();
}
},
showLoading :{
type: Boolean,
default:()=>{
return false;
}
},
emptyText:{
type: String,
default:()=>{
return '暫無數(shù)據(jù)';
}
},
emptyData:{
type:Object,
default:()=>{
return {}
}
},
noPackage:{
type: Boolean,
default:()=>{
return false;
}
}
},
setup(props,{ slots }) {
// 根據(jù) res 的狀態(tài)來判斷如何顯示
const viewStatusAdapter = (response: ResponseBean): ViewStatusType => {
// console.log('----viewStatusAdapter----',response)
if (props.showLoading) {
return "LOADING";
}
if (!response) {
return "LOADING";
}
switch (response.code) {
case 0:
if (!response.data || response.data.length === 0) {
return "EMPTY";
} else {
return "SHOW";
}
case -100:
return "LOADING";
default:
return "ERROR";
}
}
// 用 computed 包一下
const viewStatus = computed<ViewStatusType>(()=>{
return viewStatusAdapter(props.res)
})
const noDataView = (text:string)=>{
return (
<div class="empty_view col-center item-center">
{text}
</div>
)
}
const emptyView = ()=>{
if(viewStatus.value === 'EMPTY'){
return slots.empty?slots.empty():noDataView(props.emptyText);
}
}
const errorView = ()=>{
if(viewStatus.value === 'ERROR'){
return slots.error?slots.error():noDataView(props.res.msg);
}
}
return () => {
if(viewStatus.value === 'EMPTY'){
return emptyView();
}else if(viewStatus.value === 'ERROR'){
return errorView();
}else {
// noPackage 為 false 時(shí),ClamView將會(huì)在 slots 外面包一層 div ,通過給div更換樣式來實(shí)現(xiàn)狀態(tài)切換;
// 為 true 時(shí),將不會(huì)包裹div,會(huì)通過 vClass 屬性傳替給 需要使用的地方綁定樣式進(jìn)行切換
if(props.noPackage){
return slots.default({
data:viewStatus.value==='LOADING'?props.emptyData:props.res.data,
vClass:viewStatus.value==='LOADING'?'skeleton-view-empty-view':'skeleton-view-default-view'
})
}else {
return (
<div class={viewStatus.value==='LOADING'?'skeleton-view-empty-view':'skeleton-view-default-view'}>
{slots.default({
data:viewStatus.value==='LOADING'?props.emptyData:props.res.data
})}
</div>
)
}
}
}
}
})
-
skeleton.css
/**
正常狀態(tài)下的 樣式,
設(shè)置 transition 來讓過渡平滑
*/
.skeleton-view-default-view span,
.skeleton-view-default-view a,
.skeleton-view-default-view img
{
transition: all .7s ease;
background-color: rgba(0, 0, 0, 0);
}
/**
加載時(shí)的樣式,首先設(shè)置不監(jiān)聽任何事件,省的用戶亂點(diǎn)
然后給 span、a、img標(biāo)簽設(shè)置可以動(dòng)的背景,字體顏色設(shè)成透明,
就形成 “骨架” 了
*/
.skeleton-view-empty-view {
pointer-events: none;
}
.skeleton-view-empty-view span,
.skeleton-view-empty-view a {
color: rgba(0, 0, 0, 0) !important;
border-radius: 2px;
background: linear-gradient(
-45deg,
#F5F5F5 0%,
#DCDCDC 25%,
#F5F5F5 50%,
#DCDCDC 75%,
#F5F5F5 100%
);
animation: gradientBG 4s ease infinite;
background-size: 400% 400%;
background-color:#DCDCDC;
transition: all 1s ease;
}
.skeleton-view-empty-view img {
/* 這里是一個(gè)透明的小圖片 */
content: url(../../assets/img/no_url.png);
border-radius: 2px;
background: linear-gradient(
-45deg,
#F5F5F5 0%,
#DCDCDC 25%,
#F5F5F5 50%,
#DCDCDC 75%,
#F5F5F5 100%
);
animation: gradientBG 4s ease infinite;
background-size: 400% 400%;
background-color:#DCDCDC;
transition: all 1s ease;
}
@keyframes gradientBG {
0% {
background-position: 100% 100%;
}
50% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
-
clam_view.css
.clam-box{
width: 100%;
height: 100%;
}
.empty_view{
padding-top: 100px;
width: 100%;
height: 100%;
padding-bottom: 100px;
}
.empty_img{
width: 310px;
height: 218px;
}
.trip_text{
font-size: 28px;
color: #999999;
}
使用
<template>
<div class="home col">
<clam-view :res="response" v-slot="{data}" :empty-data="emptyData">
<p><span>{{data.name}}</span></p>
<p>Home</p>
<router-link to="/about" >{{data.route}}</router-link>
</clam-view>
</div>
</template>
ts
復(fù)制代碼
<script lang="ts">
import { defineComponent,reactive,toRefs,onMounted } from 'vue';
import {ResponseBean} from 'bdjf_http'
export default defineComponent({
name: 'Home',
setup(){
const state = reactive({
response:new ResponseBean().loading()
})
onMounted(()=>{
setTimeout(()=>{
state.response = new ResponseBean(0,'',{
name:'Home',
route:'About'
})
},2500)
})
const emptyData = {
name:'站位文字',
route:'站位文字'
}
return {
...toRefs(state),
emptyData
}
}
});
</script>
<style scoped>
</style>
配合bdjf_http
如果你配合 bdjf_http 使用,就能用極少的代碼完成所需功能 點(diǎn)此了解bdjf\_http[2]
post(API.getData())
.then(res => state.response = res;)
https://juejin.cn/post/6945373247659573278
[2]https://juejin.cn/post/6945373247659573278
最后
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我個(gè)小忙:
點(diǎn)個(gè)「喜歡」或「在看」,讓更多的人也能看到這篇內(nèi)容
我組建了個(gè)氛圍非常好的前端群,里面有很多前端小伙伴,歡迎加我微信「sherlocked_93」拉你加群,一起交流和學(xué)習(xí)
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。
