今天分享的主題是:「一種自動化生成骨架屏的方案」, 先看下市場上常見的骨架屏最佳化效果。
今天的分享主要分為三個部分:
1. 首屏載入狀態演進
2. 如何構建骨架屏
3. 將骨架屏打包的專案中
首屏載入的演進
我們先來看一些權威機構所做的研究報告。
一份是 [Akamai](http://www.akamai.com/html/about/press/releases/2009/press_091409.html) 這份究報告,當時總共採訪了大約 1048 名網上購物者,得出了這樣的結論:
- 大約有 47% 的使用者期望他們的頁面在兩秒之內載入完成。
- 如果頁面載入時間超過 3s,大約有 40% 的使用者選擇離開或關閉頁面。
這是 TagMan 和眼鏡零售商 Glasses Direct 合作進行的測試,研究頁面載入速度和最終轉化率的關係:
在這份測試報告中,發現了網頁載入速度和轉化率呈現明顯的負相關性,在頁面載入時間為1~2 秒時的轉化率是最高的,而當載入時間繼續增長,轉化率開始呈現一個下降的趨勢,大約頁面載入時間每增加 1s 轉化率下降6.7個百分點。
另外一份研究報告是 MIT 神經科學家在 2014 年做的研究,人類可以在 13ms 內感知到離散圖片的存在,並將圖片的大概資訊傳輸到我們的大腦中,在接下來的 100 到 140ms 之間,大腦會決定我們的眼睛具體關注圖片的什麼位置,也就是獲取圖片的關注焦點。從另一個角度來看,如果使用者進行某項互動(比如點選某按鈕),要讓使用者感知不到延遲或者資料載入,我們大概有 200 ms 的時間來準備新的介面資訊呈現給使用者。
在 200ms 到 1s 之間,使用者似乎還感知不到自己處在互動等待狀態,當一秒鐘後依然得不到任何反饋,使用者將會把其關注的焦點移到其他地方,如果等待超過 10s,使用者將對網站失去興趣,並瀏覽其他網站。
那麼我們需要做些什麼來留住使用者呢?
通常方案,我們會在首屏、或者獲取資料時,在頁面中展現一個進度條,或者轉動的 Spinner。
- 進度條:明確知道互動所需時間,或者知道一個大概值的時候我們選擇使用進度條。
- Spinner:無法預測獲取資料、或者開啟頁面的時長。
有了進度條或者 Spinner,至少告訴了使用者兩點內容:
- 你所進行的操作需要等待一段時間。
- 其次,安撫使用者,讓其耐心等待。
除此之外,進度條和 Spinner 並不能帶來其他任何作用,既無法讓使用者感知到頁面載入得更快,也無法給使用者一個焦點,讓使用者將關注集中到這個焦點上,並且知道這個焦點即將呈現使用者感興趣的內容。
那麼有沒有比進度條和 Spinner 更好的方案呢?也許我們需要的是骨架屏。
其實,骨架屏(Skeleton Screen)已經不是什麼新奇的概念了,Luke Wroblewski 早在 2013 年就首次提出了骨架屏的概念,並將這一概念成功得運用到他當時的產品「Polar app」中,2014 年,「Polar」加入 Google,Luke Wroblewski 本人也成為了Google 的一位產品總監。
A skeleton screen is essentially a blank version of a page into which information is gradually loaded.
他是這樣定義骨架屏的,他認為骨架屏是一個頁面的空白版本,透過這個空白版本傳遞資訊,我們的頁面正在漸進式的載入過程中。
蘋果公司已經將骨架屏寫入到了 iOS Human Interface Guidelines(https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/LaunchImages.html), 只是在該手冊中,其用了一個新的概念「launch images」。在該手冊中,其推薦在應用首屏中包含文字或者元素基本的輪廓。
2015 年,Facebook 也首次在其移動端 App 中使用了骨架屏的設計來預覽頁面的載入狀態。
隨後,Twitter,Medium,YouTube 也都在其產品設計中添加了骨架屏,骨架屏一時成為了首屏載入的新趨勢,國內一些公司也緊隨其後,餓了麼、知乎、掘金、騰訊新聞等也都在其 PC 端或者移動端加入了骨架屏設計。
為什麼需要骨架屏?
- 在最開始關於 MIT 2014 年的研究中已有提到,使用者大概會在 200ms 內獲取到介面的具體關注點,在資料獲取或頁面載入完成之前,給使用者首先展現骨架屏,骨架屏的樣式、佈局和真實資料渲染的頁面保持一致,這樣使用者在骨架屏中獲取到關注點,並能夠預知頁面什麼地方將要展示文字什麼地方展示圖片,這樣也就能夠將關注焦點移到感興趣的位置。當真實資料獲取後,用真實資料渲染的頁面替換骨架屏,如果整個過程在 1s 以內,使用者幾乎感知不到資料的載入過程和最終渲染的頁面替換骨架屏,而在使用者的感知上,出現骨架屏那一刻資料已經獲取到了,而後只是資料漸進式的渲染出來。這樣使用者感知頁面載入更快了。
- 再看看現在的前端框架, React 、Vue 、Angular 已經佔據了主導地位,市面上大多數前端應用也都是基於這三個框架或庫完成,這三個框架有一個共同的特點,都是 JS 驅動,在 JS 程式碼解析完成之前,頁面不會展示任何內容,也就是所謂的白屏。使用者是極其不喜歡看到白屏的,什麼都沒有展示,使用者很有可能懷疑網路或者應用出了什麼問題。 拿 Vue 來說,在應用啟動時,Vue 會對元件中的 data 和 computed 中狀態值透過 Object.defineProperty 方法轉化成 set、get 訪問屬性,以便對資料變化進行監聽。而這一過程都是在啟動應用時完成的,這也勢必導致頁面啟動階段比非 JS 驅動(比如 jQuery 應用)的頁面要慢一些。
如何構建骨架屏
餓了麼移動 web 頁面在 2016 年開始引入骨架屏,是完全透過 HTML 和 CSS 手寫的,手寫骨架屏當然可以完全復刻頁面的真實樣式,但也有弊端:
舉個例子,突然有一天,產品經理跑到了我面前,這個頁面佈局需要調整一下,然後這一塊推廣內容可以去掉了,我當時的心情可能是這樣的。
手寫骨架屏帶來的問題就是,每次需求的變更我們不僅需要修改業務程式碼, 同時也要去修改骨架屏的樣式和佈局,這往往是比較機械重複的工作,手寫骨架屏增加了維護成本。
因此餓了麼前端團隊一直在尋找一種更好、更快的將資料呈現到使用者面前的方案。
在選擇骨架屏之前,我們也調研了其他兩種備選方案:服務端渲染(ssr)和預渲染(prerender)。
現在,前端領域,不同框架下,服務端渲染的技術已經相當成熟,開箱即用的方案也有,比如 Vue 的 Nuxt.js。那麼為什麼不直接使用服務端渲染來加快內容展現?
首先我們瞭解到,服務端渲染主要有兩個目的,一是 SEO,二是加快內容展現。在帶來這兩個好處的同時,我們也需要評估服務端渲染的成本,首先我們需要服務端的支援,因此涉及到了到了服務構建、部署等,同時我們的 web 專案是一個流量較大的網站,也需要考慮伺服器的負載,以及相應的快取策略,特別是一些外賣行業,由於地理位置的不同,不同使用者看到的頁面也是不一樣的,也就是所謂的千人千面,這也為快取造成了一定困難。
其次,預渲染(prerender),所謂預渲染,就是在專案的構建過程中,透過一些渲染機制,比如 puppeteer 或則 jsdom 將頁面在構建的過程中就渲染好,然後插入到 html 中,這樣在頁面啟動之前首先看到的就是預渲染的頁面了。但是該方案最終也拋棄了,預渲染渲染的頁面資料是在構建過程中就已經打包到了 html 中, 當真實訪問頁面的時候,真實資料可能已經和預渲染的資料有了很大的出入,而且預渲染的頁面也是一個不可互動的頁面,在頁面沒有啟動之前,使用者無法和預渲染的頁面進行任何互動,預渲染頁面中的資料反而會影響到使用者獲取真實的資訊,當涉及到一些價格、金額、地理位置的地方甚至會導致使用者做出一些錯誤的決定。因此我們最終沒有選擇預渲染方案。
生成骨架屏基本方案
透過 puppeteer 在服務端操控 headless Chrome 開啟開發中的需要生成骨架屏的頁面,在等待頁面載入渲染完成之後,在保留頁面佈局樣式的前提下,透過對頁面中元素進行刪減或增添,對已有元素透過層疊樣式進行覆蓋,這樣達到在不改變頁面佈局下,隱藏圖片和文字,透過樣式覆蓋,使得其展示為灰色塊。然後將修改後的 HTML 和 CSS 樣式提取出來,這樣就是骨架屏了。
下面我將透過 page-skeleton-webpack-plugin 工具中的程式碼,來展示骨架屏的具體生成過程。
正如上面基本方案所描述的那樣,我們將頁面分成了不同的塊:
- 文字塊:僅包含文字節點(NodeType 為 Node.TEXTNODE)的元素(NodeType 為 Node.ELEMENTNODE),一個文字塊可能是一個 p 元素也可能是 div 等。文字塊將會被轉化為灰色條紋。
- 圖片塊:圖片塊是很好區分的,任何 img 元素都將被視為圖片塊,圖片塊的顏色將被處理成配置的顏色,形狀也被修改為配置的矩形或者圓型。
- 按鈕塊:任何 button 元素、 type 為 button 的 input 元素,role 為 button 的 a 元素,都將被視為按鈕塊。按鈕塊中的文字塊不在處理。
- svg 塊:任何最外層是 svg 的元素都被視為 svg 塊。
- 偽類元素塊:任何偽類元素都將視為偽類元素塊,如 ::before 或者 ::after。
- …
首先,我們為什麼要把頁面劃分為不同的塊呢?
將頁面劃分為不同的塊,然後分別對每個塊進行處理,這樣不會破壞頁面整體的樣式和佈局,當我們最終生成骨架屏後,骨架屏的佈局樣式將和真實頁面的佈局樣式完全一致,這樣就達到了複用樣式及頁面佈局的目的。
在所有分開處理之前,我們需要完成一項工作,就是將我們生成骨架屏的指令碼,插入到 puppeteer 開啟的頁面中,這樣我們才能夠執行指令碼,並最終生成骨架屏。
值得慶幸的是,puppeteer 在其生成的 page 例項中提供了一個原生的方法。
page.addScriptTag(options)
options<Object>
url
path
content
type(Use 'module' in order to load a Javascript ES6 module.)
有了這種方法,我們可以插入一段 js 指令碼的 url 或者是相對/絕對路徑,也可以直接是 js 指令碼的內容,在我們的實踐過程中,我們直接插入的指令碼內容。
async makeSkeleton(page) {
const { defer } = this.options
await page.addScriptTag({ content: this.scriptContent })
await sleep(defer)
await page.evaluate((options) => {
Skeleton.genSkeleton(options)
}, this.options)
}
有了上面插入的指令碼,並且我們在指令碼中提供了一個全域性物件 Skeleton,這樣我們就可以直接透過 page.evaluate 方法來執行指令碼內容並最終生成骨架頁面了。
由於時間有限,這兒不會對每個塊的生成骨架結構進行詳盡分析,這兒可能會重點闡述下文字塊、圖片塊、svg 塊如何生成骨架結構的,然後再談談如何對骨架結構進行最佳化。
文字塊的骨架結構生成
文字塊可以算是骨架屏生成中最複雜的一個區塊了,正如上面也說的,任何只包含文字節點的元素都將視為文字塊,在確定某個元素是文字塊後,下一步就是透過一些 CSS 樣式,以及元素的增減將其修改為骨架樣式。
在這張圖中,圖左邊虛線框內是一個 p 元素,可以看到其內部有 4 行文字,右圖是一個已經生成好的帶有 4 行文字的骨架屏。在生成文字塊骨架屏之前,我們首先需要了解一些基本的引數。
- 單行文字內容的高度,可以透過 fontSize 獲取到。
- 單行文字內容加空白間隙的高度,可以透過 lineHeight 獲取到。
- p 元素總共有多少行文字,也就是所謂行數,這個可以透過 p 元素的 (height - paddingTop - paddingBottom)/ lineHeight 大概算出。
- 文字的 textAlign 屬性。
在這些引數中,fontSize、lineHeight、paddingTop、paddingBottom 都可以透過 getComputedStyle 獲取到,而元素的高度 height 可以透過 getBoundingClientRect 獲取到,有了這些引數後我們就能夠繪製文字塊的骨架屏了。
相信很多人都讀過 @Lea Verou 的 CSS Secrets 這本書,書中有一篇專門闡述怎麼透過線性漸變生成條紋背景的文章,而在繪製文字塊骨架屏方案,正是受到了這篇文章的啟發,文字塊的骨架屏也是透過線性漸變來繪製的。核心簡化程式碼:
const textHeightRatio = parseFloat(fontSize, 10) / parseFloat(lineHeight, 10)
const firstColorPoint = ((1 - textHeightRatio) / 2 * 100).toFixed(decimal)
const secondColorPoint = (((1 - textHeightRatio) / 2 + textHeightRatio) * 100).toFixed(decimal)
const rule = `{
background-image: linear-gradient(
transparent ${firstColorPoint}%, ${color} 0%,
${color} ${secondColorPoint}%, transparent 0%);
background-size: 100% ${lineHeight};
position: ${position};
background-origin: content-box;
background-clip: content-box;
background-color: transparent;
color: transparent;
background-repeat: repeat-y;
}`
我們首先計算了 lineHeight 和 fontSize 等一些樣式引數,透過這些引數我們計算出了文字佔整個行高的比值,也就是 textHeightRadio,有了這一比值,就可以知道灰色條紋的分界點,正如 @Lea Verou 所說:
摘自:CSS Secrets “If a color stop has a position that is less than the specied position of any color stop before it in the list, set its position to be equal to the largest speci ed position of any color stop before it.” — CSS Images Level 3 (http://w3.org/TR/css3-images)
也就是說,線上性漸變中,如果我們將線性漸變的起始點設定小於前一個顏色點的起始值,或者設定為0 %,那麼線性漸變將會消失,取而代之的將是兩條顏色分明的條紋,也就是說不再有線性漸變。
在我們繪製文字塊的時候,backgroundSize 寬度為 100%, 高度為 lineHeight,也就是灰色條紋加透明條紋的高度是 lineHeight。雖然我們把灰色條紋繪製出來了,但是,我們的文字依然顯示,在最終骨架樣式效果出現之前,我們還需要隱藏文字,設定 color:‘transparent’ 這樣我們的文字就和背景色一致,最終顯示得也就是灰色條紋了。
根據 lineCount 我們可以判斷文字塊是單行文字還是多行,在處理單行文字的時候,由於文字的寬度並沒有整行寬度,因此,針對單行文字,我們還需要計算出文字的寬度,然後設定灰色條紋的寬度為文字寬度,這樣骨架樣式的效果才能夠更加接近文字樣式。
圖片塊的骨架生成
圖片塊的繪製比文字塊要相對簡單很多,但是在訂方案的過程中也踩了一些坑,這兒簡單分享下采坑經歷。
最初訂的方案是透過一個 DIV 元素來替換 IMG 元素,然後設定 DIV 元素背景為灰色,DIV 的寬高等同於原來 IMG 元素的寬高,這種方案有一個嚴重的弊端就是,原來透過元素選擇器設定到 IMG 元素上的樣式無法運用到 DIV 元素上面,導致最終圖片塊的骨架效果和真實的圖片在頁面樣式上有出入,特別是沒法適配不同的移動端裝置,因為 DIV 的寬高被硬編碼。
接下來我們又嘗試了一種看似「高階」的方法,透過 Canvas 來繪製和原來圖片大小相同的灰色塊,然後將 Canvas 轉化為 dataUrl 賦予給 IMG 元素的 src 特性上,這樣 IMG 元素就顯示成了一個灰色塊了,看似完美,當我們將生成的骨架頁面生成 HTML 檔案時,一下就傻眼了,檔案大小盡然有 200 多 kb,我們做骨架頁面渲染的一個重要原因就是希望使用者在感知上感覺頁面載入快了,如果骨架頁面都有 200 多 kb,必將導致頁面載入比之前要慢一些,違背了我們的初衷,因此該方案也只能夠放棄。
最終方案,我們選擇了將一張1 * 1 畫素的 gif 透明圖片,轉化成 dataUrl ,然後將其賦予給 IMG 元素的 src 特性上,同時設定圖片的 width 和 height 特性為之前圖片的寬高,將背景色調至為骨架樣式所配置的顏色值,完美解決了所有問題。
// 最小 1 * 1 畫素的透明 gif 圖片 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
這是1 * 1畫素的 base64 格式的圖片,總共只有幾十個位元組,明顯比之前透過 Canvas 繪製的圖片小很多。
程式碼:
function imgHandler(ele, { color, shape, shapeOpposite }) {
const { width, height } = ele.getBoundingClientRect()
const attrs = {
width,
height,
src
}
const finalShape = shapeOpposite.indexOf(ele) > -1 ? getOppositeShape(shape) : shape
setAttributes(ele, attrs)
const className = CLASS_NAME_PREFEX + 'image'
const shapeName = CLASS_NAME_PREFEX + finalShape
const rule = `{
background: ${color} !important;
}`
addStyle(`.${className}`, rule)
shapeStyle(finalShape)
addClassName(ele, [className, shapeName])
if (ele.hasAttribute('alt')) {
ele.removeAttribute('alt')
}
}
svg 塊骨架結構
svg 塊處理起來也比較簡單,首先我們需要判斷 svg 元素 hidden 屬性是否為 true,如果為 true,說明該元素不展示的,所以我們可以直接刪除該元素。
if (width === 0 || height === 0 || ele.getAttribute('hidden') === 'true') {
return removeElement(ele)
}
如果不是隱藏的元素,那麼我們將會把 svg 元素內部所有元素刪除,減少最終生成的骨架頁面體積,其次,設定svg 元素的寬、高和形狀等。
const shapeClassName = CLASS_NAME_PREFEX + shape
shapeStyle(shape)
Object.assign(ele.style, {
width: px2relativeUtil(width, cssUnit, decimal),
height: px2relativeUtil(height, cssUnit, decimal),
})
addClassName(ele, [shapeClassName])
if (color === TRANSPARENT) {
setOpacity(ele)
} else {
const className = CLASS_NAME_PREFEX + 'svg'
const rule = `{
background: ${color} !important;
}`
addStyle(`.${className}`, rule)
ele.classList.add(className)
}
一些最佳化的細節
- 首先,由上面一些程式碼可以看出,在我們生成骨架頁面的過程中,我們將所有的共用樣式透過 addStyle 方法快取起來,最後在生成骨架屏的時候,統一透過 style 標籤插入到骨架屏中。這樣保證了樣式儘可能多的複用。
- 其次,在處理列表的時候,為了生成骨架屏儘可能美觀,我們對列表進行了同化處理,也就是說將 list 中所有的 listItem 都是同一個 listItem 的克隆。這樣生成的 list 的骨架屏樣式就更加統一了。
- 還有就是,正如前文所說,骨架屏僅是一種載入狀態,並非真實頁面,因此其並不需要完整的頁面,其實只需要首屏就好了,我們對非首屏的元素進行了刪除,只保留了首屏內部元素,這樣也大大縮減了生成骨架屏的體積。
- 刪除無用的 CSS 樣式,只是我們只提取了對骨架屏有用的 CSS,然後透過 style 標籤引入。
關鍵程式碼大致是這樣的:
const checker = (selector) => {
if (DEAD_OBVIOUS.has(selector)) {
return true
}
if (/:-(ms|moz)-/.test(selector)) {
return true
}
if (/:{1,2}(before|after)/.test(selector)) {
return true
}
try {
const keep = !!document.querySelector(selector)
return keep
} catch (err) {
const exception = err.toString()
console.log(`Unable to querySelector('${selector}') [${exception}]`, 'error')
return false
}
}
可以看出,我們主要透過 document.querySelector 方法來判斷該 CSS 是否被使用到,如果該 CSS 選擇器能夠選擇上元素,說明該 CSS 樣式是有用的,保留。如果沒有選擇上元素,說明該 CSS 樣式沒有用到,所以移除。
在後面的一些 slides 中,我們來聊聊怎講將構建骨架屏和 webpack 開發、打包結合起來,最終將我們的骨架屏打包到實際專案中。
透過 webpack 將骨架屏打包到專案中
在上一個部分,我們分析了怎麼去生成骨架屏,在這一部分,我們將探討如何透過 webpack 將骨架屏打包的專案中。在這過程中,思考了以下一些問題:
為什麼在開發過程中生成骨架屏?
其主要原因還是為了骨架屏的可編輯。
在上一個部分,我們透過一些樣式和元素的修改生成了骨架屏頁面,但是我們並沒有馬上將其寫入到配置的輸出資料夾中,在寫入骨架頁面到專案之前。我們透過 memory-fs 將骨架屏寫入到記憶體中,以便我們能夠透過預覽頁面進行訪問。同時我們也將骨架屏原始碼傳送到了預覽頁面,這樣我們就可以透過修改原始碼,對骨架屏進行二次編輯。
正如這張圖片,這張圖是外掛開啟的骨架屏的預覽頁面,從左到右依次是開發中的真實頁面、骨架屏、骨架屏可編輯原始碼。
這樣我們就可以在開發過程中對骨架屏進行編輯,修改部分樣式,中部骨架屏可以進行實時預覽,這之間的通訊都是透過websocket 來完成的。當我們對生成的骨架屏滿意後,並點選右上角寫入骨架屏按鈕,將骨架屏寫入到專案中,在最後專案構建時,將骨架屏打包到專案中。
如果我們同時在構建的過程中生成骨架屏,並打包到專案中,這時的骨架屏我們是無法預覽的,因此我們對此時的骨架屏一無所知,也不能夠做任何修改,這就是我們在開發中生成骨架屏的原因所在。
演講最開始已經提到,目前流行的前端框架基本都是 JS 驅動,也就是說,在最初的 index.html 中我們不用寫太多的 html 內容,而是等框架啟動完成後,透過執行時將內容填充到 html 中,通常我們會在 html 模板中新增一個根元素:
<div id="app"></div>
當應用啟動後,會將真實的內容填充到上面的元素中。這也就給了我們一個展示骨架屏的機會,我們將骨架屏在頁面啟動之前新增到上面元素內:
<div id="app"><!-- shell.html --></div>
怎樣將骨架屏打包到專案中
Webpack 是一款優秀的前端打包工具,其也提供了一些豐富的 API 讓我們可以自己編寫一些外掛來讓 webpack 完成更多的工作,比如在構建過程中,將骨架屏打包到專案中。
Webpack 在整個打包的過程中提供了眾多生命週期事件,比如compilation 、after-emit 等,比如我們最終將骨架屏插入到 html 中就是在after-emit 鉤子函式中進行的,簡單的程式碼:
SkeletonPlugin.prototype.apply = function (compiler) {
// 其他程式碼
compiler.plugin('after-emit', async (compilation, done) => {
try {
await outputSkeletonScreen(this.originalHtml, this.options, this.server.log.info)
} catch (err) {
this.server.log.warn(err.toString())
}
done()
})
// 其他程式碼
}
我們再來看看 outputSkeletonScreen 是如何將骨架屏插入到原始的 HTML 中,並且寫入到配置的輸入資料夾的。
const outputSkeletonScreen = async (originHtml, options, log) => {
const { pathname, staticDir, routes } = options
return Promise.all(routes.map(async (route) => {
const trimedRoute = route.replace(/\//g, '')
const filePath = path.join(pathname, trimedRoute ? `${trimedRoute}.html` : 'index.html')
const html = await promisify(fs.readFile)(filePath, 'utf-8')
const finalHtml = originHtml.replace('<!-- shell -->', html)
const outputDir = path.join(staticDir, route)
const outputFile = path.join(outputDir, 'index.html')
await fse.ensureDir(outputDir)
await promisify(fs.writeFile)(outputFile, finalHtml, 'utf-8')
log(`write ${outputFile} successfully in ${route}`)
return Promise.resolve()
}))
}
更多思考
Page Skeleton webpack 外掛在我們內部團隊已經開始使用,在使用的過程中我們也得到了一些反饋資訊。
首先是對 SPA 多路由的支援,其實現在外掛已經支援多路由了,只是還沒有用到真實專案中,我們針對每一個路由頁面生成一個單獨的 index.html,也就是靜態路由。然後將每個路由生成的骨架屏插入到不同的靜態路由的 html 中。
其次,玩過服務端渲染的同學都知道,在 React 和 Vue 服務端渲染中有一種稱為 Client-side Hydration 的技術,指的是在 Vue 在瀏覽器接管由服務端傳送來的靜態 HTML,使其變為由 Vue 管理的動態 DOM 的過程。
在我們構建骨架屏的過程中,其 DOM 結構和真實頁面的 DOM 結構基本相同,只是添加了一些行內樣式和 classname,我們也在思考這些 DOM 能夠被複用,也就是在應用啟動時重新建立所有 DOM。我們只用啟用這些骨架屏 DOM,讓其能夠相應資料的變化,這似乎就可以使骨架屏和真實頁面更好的融合。
還有,在頁面啟動後,我們可能還是會透過 AJAX 獲取後端資料,這時候我們也可以透過 骨架屏 來作為一種載入狀態。也就是說,其實我們可以在「非首屏骨架屏」上做一些工作。
最後,在專案中可能會有一些效能監控的需求,比如骨架屏什麼時候建立,什麼時候被銷燬,這些我們可能都希望透過一些效能監控的工具記錄下來,以便將來做一些效能上面的分析。因此將來也會提供一些骨架屏的生命週期函式,或者提供相應的自定義事件,在生命週期不同階段,呼叫相應的生命週期鉤子函式或監聽相應事件,這樣就可以將骨架屏的一些資料記錄到效能監控軟體中。
本文摘自:一種自動化生成骨架屏的方案(http://github.com/Jocs/jocs.github.io/issues/22)
推薦閱讀
- page-skeleton-webpack-plugin外掛地址 (https://github.com/ElemeFE/page-skeleton-webpack-plugin)
- 如何讓網頁“看起來”展現地更快?骨架屏二三事 (https://zhuanlan.zhihu.com/p/48601348)
- 美團網頁首屏最佳化 (https://mp.weixin.qq.com/s/wi52VIfteEXGift95wzkWQ)
- Vue頁面骨架屏注入實踐 (https://segmentfault.com/a/1190000014832185)
- Vue專案骨架屏注入實踐 (https://juejin.cn/post/6844903661726859272)
- vue-cli專案新增骨架屏多種方式,自動生成骨架屏 (https://blog.csdn.net/zhouzuoluo/article/details/100216255)
- 小程式構建骨架屏的探索 (https://segmentfault.com/a/1190000015876164)