最近在作項目的優化,除了總體的架構更改,咱們發如今每次加載的時候,首頁白屏的問題十分明顯。css
如今的前端框架, React
、Vue
、Angular
三大巨頭已經佔據了主導地位,市面上大多數前端應用也都是基於這三個框架或庫完成,這三個框架有一個共同的特色,都是 JS 驅動,在 JS 代碼解析完成以前,頁面不會展現任何內容,也就是所謂的白屏。html
用戶是極其不喜歡看到白屏的,什麼都沒有展現,用戶頗有可能懷疑網絡或者應用出了什麼問題。 拿 Vue 來講,在應用啓動時,Vue 會對組件中的 data 和 computed
中狀態值經過 Object.defineProperty
方法轉化成 set、get 訪問屬性,以便對數據變化進行監聽。 而這一過程都是在啓動應用時完成的,這也勢必致使頁面啓動階段比非 JS 驅動(好比 jQuery 應用)的頁面要慢一些。前端
因此咱們首頁就是一個典型的案例,在一次咱們前端週會上,咱們老大問了咱們一個問題,如何給用戶啊一個更好的體驗。node
這時候我第一個反應是,儘可能不出現白屏的話,能夠用VueSSR,就是服務端直出頁面。webpack
首先咱們瞭解到,服務端渲染主要有兩個目的,一是 SEO,二是加快內容展示。 在帶來這兩個好處的同時,咱們也須要評估服務端渲染的成本,首先咱們須要服務端的支持,所以涉及到了服務構建、部署等,同時 web 項目是一個流量較大的網站,也須要考慮服務器的負載,以及相應的緩存策略,特別像咱們行業,因爲地理位置的不一樣,不一樣用戶看到的頁面也是不同的,也就是所謂的千人千面,這也爲緩存形成了必定困難。git
所謂預渲染,就是在項目的構建過程當中,經過一些渲染機制,好比 puppeteer
或則 jsdom
將頁面在構建的過程當中就渲染好,而後插入到 html 中,這樣在頁面啓動以前首先看到的就是預渲染的頁面了。github
可是該方案最終也拋棄了,預渲染渲染的頁面數據是在構建過程當中就已經打包到了 html 中, 當真實訪問頁面的時候,真實數據可能已經和預渲染的數據有了很大的出入,並且預渲染的頁面也是一個不可交互的頁面,在頁面沒有啓動以前,用戶沒法和預渲染的頁面進行任何交互,預渲染頁面中的數據反而會影響到用戶獲取真實的信息,當涉及到一些價格、金額、地理位置的地方甚至會致使用戶作出一些錯誤的決定。web
骨架頁面(Skeleton Page)指的是當你打開一個移動端 web 頁面,在頁面解析和數據加載以前,首先給用戶展現頁面的大概樣式。在骨架頁面中,圖片、文字、圖標都將經過灰色矩形塊或圓形塊來展現,在真實頁面展現以前,用戶可以感知到即將加載頁面的基本 CSS 樣式和頁面佈局。數組
一開始在我腦子裏,覺得骨架屏是一個頁面去手寫一個css和Html,或者說是讓ui去設計一個骨架圖。可是這樣是有缺點的,好比產品改需求了呢,不只要去修改代碼,還要去從新修改骨架頁面或者骨架圖?瀏覽器
後來,看到了餓了麼大神的文章和很成熟的產品page-skeleton-webpack-plugin ,瞬間明白了我和大佬的區別
生成骨架頁面的基本方案
經過 puppeteer 在服務端操控 headless Chrome 打開開發中的須要生成骨架頁面的頁面,在等待頁面加載渲染完成以後,在保留頁面佈局樣式的前提下,經過對頁面中元素進行刪減或增添,對已有元素經過層疊樣式進行覆蓋,這樣達到在不改變頁面佈局下,隱藏圖片、文字和圖片的展示,經過樣式覆蓋,使得其展現爲灰色塊。而後將修改後的 HTML 和 CSS 樣式提取出來,這樣就是骨架頁面了。
在闡述具體生成骨架頁面以前,先了解下 puppeteer, GitHub 上是這樣介紹的。
Puppeteer 是一個 Node 庫,它提供了一個高級 API 來經過 DevTools 協議控制 Chromium 或 Chrome。
Puppeteer API 是分層次的,反映了瀏覽器結構。說實話,這是我第一次接觸這個 Node 庫,剛上手安裝的時候就遇到了很多坑,哈哈哈哈哈哈,尷尬。
有想了解的同窗請看puppetter安裝就踩坑-解決篇
骨架屏的開發基本上就是基於這個node庫開始的。接下來咱們來解析一下基礎代碼
skeleton.js
const puppeteer = require('puppeteer') const devices = require('puppeteer/DeviceDescriptors') //puppeteer 提供了一些設備的參數選項 const { sleep , genScriptContent } = require('./util/utils') //公共工具方法 const scriptFns = require('./util/browserUtils') const skeleton = async function(url, option = {}) { const defaultOption = { device: 'iPhone 6' } const { device, defer = 0, //延遲的時間 remove = [], //頁面想要移除的class類名數組 excludes = [], //頁面想要不包括的class類名數組 hide= [],//頁面想要隱藏的class類名數組 launch: launchOpt } = Object.assign({}, defaultOption, option) // 當 Puppeteer 鏈接到一個 Chromium 實例的時候會經過 puppeteer.launch 或 puppeteer.connect 建立一個 Browser 對象。 // 返回一個新的 [Page] 對象。[Page] 在一個默認的瀏覽器上下文中被建立。 const browser = await puppeteer.launch(launchOpt) const page = await browser.newPage() //新建一個頁面 /** * 根據指定的參數和 user agent 生成模擬器。此方法是和下面兩個方法效果相同 * @param { options } * viewport <[Object]> width <[number]> 頁面的寬度,單位像素. height <[number]> 頁面的高度,單位像素. deviceScaleFactor <[number]> 定義設備縮放, (相似於 dpr). 默認 1。 isMobile <[boolean]> 要不要包含meta viewport 標籤. 默認 false。 hasTouch<[boolean]> 指定終端是否支持觸摸。默認 false isLandscape <[boolean]> 指定終端是否是 landscape 模式. 默認 false。 userAgent <[string]> * */ await page.emulate(devices[device]) await page.goto(url) // 將一些 utils 插入到打開的頁面執行環境中,這裏會引入如何判斷圖片,文字的方法,將他們覆蓋成灰色,也是骨架圖中必不可缺的代買 await page.addScriptTag({ content: genScriptContent(...scriptFns) }) /** 還應注意一點,defer 配置,用於告訴 Puppeteer 打開頁面後需等待的時間,這是由於,在打開開發中頁面後,頁面中有些內容還未真正加載完成,若是在這以前進行骨架頁面生成,頗有可能致使最終生成的骨架頁面和真實頁面不符。使得生成骨架頁面失敗。 **/ await sleep(defer) /** * page.evaluate(pageFunction, ...args) * pageFunction <[function]|[string]> 要在頁面實例上下文中執行的方法 ...args <...[Serializable]|[JSHandle]> 要傳給 pageFunction 的參數 返回: <[Promise]<[Serializable]>> pageFunction執行的結果 */ const html = await page.evaluate(async ( remove, excludes, hide ) => { const $ = document.querySelectorAll.bind(document) if (remove.length) { const removeEle = $(remove.join(',')) Array.from(removeEle).forEach(ele => ele.parentNode.removeChild(ele)) } if (hide.length) { const hideEle = $(hide.join(',')) Array.from(hideEle).forEach(ele => ele.style.opacity = 0) } const excludesEle = excludes.length ? Array.from($(excludes.join(','))) : [] await traverse(document.documentElement, excludesEle) return document.documentElement.outerHTML }, remove, excludes,hide) // browser.close() return { html } } module.exports = skeleton 複製代碼
下一篇,咱們會認真的去分析,餓了麼骨架屏中是如何去將頁面根據不一樣元素分紅不一樣的塊:文本、圖片塊,SVG塊,僞元素塊、按鈕塊 將元素區分爲不一樣塊後,下一步就是對這些塊分別進行處理,包括元素的增減和樣式的覆蓋,目的只有一個,就是將這些塊轉化爲骨架頁面的樣式。
資源連接 餓了麼大佬-Ran Luo 一種自動化生成骨架屏的方案