[重構倒數第24天] — You should use Skeleton

Mike
I am Mike
Published in
11 min readSep 29, 2021

該系列是為了讓看過Vue官方文件或學過Vue但是卻不知道怎麼下手去重構現在有的網站而去規畫的系列文章,在這邊整理了許多我自己使用Vue重構很多網站的經驗分享給讀者們。

我們在開發網站的時候常常會遇到說圖片或是文字還沒有透過 API 回傳回來的時候,我們的畫面的 UI 高度可能還沒有,這時候圖片跟文字載入完成後會突然的撐開 UI 的高度,整個 web 頁面會有種閃一下的狀況,會讓看的人有種不舒服的體驗感,所以在處理這種資料載入的方法,就延伸出一種叫做 Skeleton 的概念。

Skeleton 顧名思義就是骨架,我們可以看到上面範例左邊的卡片當資料載入進來的時候,先把文字給放上去,然後等瀏覽器把圖片給load完成之後,在把圖片給render出來,這時候我們的DOM因為圖片的關係,然後高度就被撐開來了,這樣子的呈現方式如果你的網站設計結構複雜,而且很吃圖片的話,其實很容易造成畫面因為DOM撐開來的閃動

所以為了解決這個問題,我們可以先把因為非同步所載入的圖片或是文字的範圍先給他定義出來,將高度或是寬度定義好,以減少之後圖片跟文字載入後撐開的閃動,就像下圖這樣

在這邊我就簡單的來帶大家來看一下 Skeleton 該如何實作首先我們先來定義一下html

html

<div class="card" v-for="item in DataValue" :key="item.id">
<header>
<div class="photo">
<img :src="item.avatar">
</div>
<p>{{item.username}}</p>
</header>
<p>{{item.text}}</p>
<main>
<img :src="item.content">
</main>
</div>

這邊我將整個卡片的架構先給他訂出來,包含卡片的使用者圖片,還有他的名字以及圖片

CSS

.card {
width: 300px;
height: auto;
border-radius: 10px;
overflow: hidden;
background-color: #fff;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
margin-bottom: 50px;
> p {
font-size: 14px;
padding: 0 10px;
margin-bottom: 10px;
}
header {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px;
> div {
margin-right: 10px;
> img {
border-radius: 50%;
}
}
> p {
font-size: 13px;
}
}
img {
opacity: 1;
transition: opacity .3s;
}
}

再來看他的css會發現我們現在的寫法是透過內容來撐開dom的高度,這是很常見的做法。

js

import { ref, onMounted } from "vue"
export default {
setup() {
const DataValue = ref([]);
const isLoad = ref(true);

const LoadImg = (imgUrl) => {
const imgArr = [...imgUrl];
let i = 0;
imgArr.forEach(src=> {
const img = new Image();
img.src = src;
img.onload = () => {
i += 1;
if(i === imgArr.length){
isLoad.value = false;
}
}
})
}

onMounted(()=> {
fetch('https://test.api/api/card')
.then(res => res.json()).then(res => {
DataValue.value = res;
const imgArr = res.map(item=> [item.avatar, item.content]);
LoadImg([].concat(...imgArr));
});
})

return {
isLoad,
DataValue,
};
},
}

在這邊我就透過 fetch 去打API來拿到我卡片的資料,然後寫了一個判斷圖片載入完成沒有的函式去判斷API給我的圖片路徑是否都載入完成了,然後給一個isLoad的狀態來做判斷依據。

我把所有的圖片路徑全部塞入一個陣列攤平,再透過圖片載入的函式把圖片全部載入。

現在就是我們完成的樣子

你會發現到,雖然我的API資料已經回來了,但是我的圖片卻還沒有載入完成,所以這個時候畫面上面圖片就會比文字晚出來,所以才會造成這樣的時間差抖動感覺。

在這邊要稍微重點提醒兩個重點關鍵

  1. API 是非同步的,API裡面本身會包含圖片的路徑。
  2. 瀏覽器載入圖片也是非同步的,所以文字比圖片出來的快是正常的。

接下來我們要稍微調整一下CSS的部分

.card {
// 跟上面一樣的code
}
.load {
width: 300px;
height: 380px;
border-radius: 10px;
overflow: hidden;
background-color: #fff;
> header {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px;
> div {
width: 30px;
height: 30px;
border-radius: 50px;
margin-right: 10px;
background-color: #ededed;
> img {
border-radius: 50%;
}
}
> p {
font-size: 13px;
display: block;
width: 40%;
height: 18px;
background-color: #ededed;
}
}
> p {
font-size: 14px;
margin: 0 10px 10px 10px;
display: block;
width: 70%;
height: 18px;
background-color: #ededed;
}
> main {
width: 100%;
height: 300px;
background-color: #ededed;
}
img {
opacity: 0;
}
}
@keyframes loading {
to{
background-position-x: -20%;
}
}
.load {
.photo, p, main {
background: linear-gradient(
100deg,
rgba(256, 256, 256, 0) 30%,
rgba(256, 256, 256, 0.5) 50%,
rgba(256, 256, 256, 0) 30%)
#ededed;
background-size: 200% 100%;
background-position-x: 180%;
animation: 2s loading ease-in-out infinite;
}
}

以下是幾個重要的調整

  1. 首先新增了 .load 的 class ,然後裡面的結構完全跟 .card 一模一樣。
  2. 原本要靠內容物撐開的高度,現在全部都設定了預設的高度,也給了預設的背景顏色,讓使用者知道這個區塊。
  3. 撰寫反光漸層以及動畫,讓畫面再載入的時候不是只有單純的色塊。

然後來看 html 的部分

<div
:class="['card', {load: isLoad}]"
v-for="item in DataValue"
:key="item.id"
>
<header>
<div class="photo">
<img :src="item.avatar">
</div>
<p>{{ isLoad ? "" : item.username }}</p>
</header>
<p>{{ isLoad ? "" : item.text }}</p>
<main>
<img :src="item.content">
</main>
</div>

在這邊原本的 card class上面多加了一個 .load 的 class,也就是說一開始的時候我們其實是透過 .load 去覆蓋 .card 的 css,然後當我們圖片都載入完成的時後,透過 isLoad 來拿掉 .load 在切換回 .card 的 css,這樣就可以達到 Skeleton 的效果,在沒有抖動的情況下完成畫面的 render 。

Skeleton 開發的前提

Skeleton 開發說穿了就是把原本需要靠內容物撐開的高度先寫好,去替換裡面的內容就好,但是這種做法有幾個需要注意的

  1. 這不是只有前端需要對工法上面的處理,更應該要在事前設計的時候讓設計師把 Skeleton 的概念給考慮進去,假設今天圖片是讓使用者隨便上傳的,那就會有寬高的問題,如果不能限制使用者上傳的寬高,那就只能我們定義預設寬高度,當今天使用者上傳的圖片寬高超過或是未達預設寬高時該怎麼處理,也都是在設計師的範疇內,要做的更極端點很多資訊也需要後端提供,例如文字的多寡或是預設的高度等等,所以 Skeleton 的概念是一個會需要團隊都有對 Skeleton 有共識的時候,才能完美實現的一個概念。
  2. Skeleton 取代了以往的整個頁面的 loading 畫面,不需要再讓所有圖片都載完才可以把畫面秀出來,但是不代表整頁的 loading 畫面不需要,像是很多動態的網頁就不適合做 Skeleton,所以 Skeleton 並不是一個通用所有頁面的解法,而是需要再依照呈現的方式來決定透過哪種方式來給使用者看。
  3. Skeleton 的開發會增加許多的 code 來處理預設畫面的部分,所以很多組件的預設呈現其實是沒有辦法共用的,所以製作上面還需要考慮是不是全部的 UI 都需要 Skeleton,並非整個頁面都會需要 Skeleton。

我在codepen上面有新增了這個範例有興趣的朋友可以看看

此文章同步更新於 第 13 屆 iT 邦幫忙鐵人賽https://ithelp.ithome.com.tw/articles/10260925

那如果對於Vue3不夠熟的話呢?

Ps. 購買的時候請登入或註冊該平台的會員,然後再使用下面連結進入網站點擊「立即購課」,這樣才可以讓我獲得更多的課程分潤,還可以幫助我完成更多豐富的內容給各位。

我有開設了一堂專門針對Vue3從零開始教學的課程,如果你覺得不錯的話,可以購買我課程來學習
https://hiskio.com/packages/AYR5m7VR3

如果對於JS基礎不熟的朋友,我也有開設JS的入門課程,可以參考這個課程
https://hiskio.com/packages/Q9R4OYoyD

訂閱Mike的頻道享受精彩的教學與分享

Mike 的 Youtube 頻道
Mike的medium
MIke 的官方 line 帳號,好友搜尋 @mike_cheng

--

--

Mike
I am Mike

如果有一行code無法解決的bug,那就寫兩行!