首頁白屏的引發的思考(一)

我母雞啊!發表於2019-03-04

最近在做專案的優化,除了整體的架構更改,我們發現在每次載入的時候,首頁白屏的問題十分明顯。

為什麼會出現白屏

現在的前端框架, ReactVueAngular 三大巨頭已經佔據了主導地位,市面上大多數前端應用也都是基於這三個框架或庫完成,這三個框架有一個共同的特點,都是 JS 驅動,在 JS 程式碼解析完成之前,頁面不會展示任何內容,也就是所謂的白屏。

使用者是極其不喜歡看到白屏的,什麼都沒有展示,使用者很有可能懷疑網路或者應用出了什麼問題。 拿 Vue 來說,在應用啟動時,Vue 會對元件中的 data 和 computed中狀態值通過 Object.defineProperty 方法轉化成 set、get 訪問屬性,以便對資料變化進行監聽。 而這一過程都是在啟動應用時完成的,這也勢必導致頁面啟動階段比非 JS 驅動(比如 jQuery 應用)的頁面要慢一些。

所以我們首頁就是一個典型的案例,在一次我們前端週會上,我們老大問了我們一個問題,如何給使用者啊一個更好的體驗。

SSR

這時候我第一個反應是,儘量不出現白屏的話,可以用VueSSR,就是服務端直出頁面。

首先我們瞭解到,服務端渲染主要有兩個目的,一是 SEO,二是加快內容展現。 在帶來這兩個好處的同時,我們也需要評估服務端渲染的成本,首先我們需要服務端的支援,因此涉及到了服務構建、部署等,同時 web 專案是一個流量較大的網站,也需要考慮伺服器的負載,以及相應的快取策略,特別像我們行業,由於地理位置的不同,不同使用者看到的頁面也是不一樣的,也就是所謂的千人千面,這也為快取造成了一定困難。

預渲染

所謂預渲染,就是在專案的構建過程中,通過一些渲染機制,比如 puppeteer或則 jsdom 將頁面在構建的過程中就渲染好,然後插入到 html 中,這樣在頁面啟動之前首先看到的就是預渲染的頁面了。

但是該方案最終也拋棄了,預渲染渲染的頁面資料是在構建過程中就已經打包到了 html 中, 當真實訪問頁面的時候,真實資料可能已經和預渲染的資料有了很大的出入,而且預渲染的頁面也是一個不可互動的頁面,在頁面沒有啟動之前,使用者無法和預渲染的頁面進行任何互動,預渲染頁面中的資料反而會影響到使用者獲取真實的資訊,當涉及到一些價格、金額、地理位置的地方甚至會導致使用者做出一些錯誤的決定。

骨架圖

骨架頁面(Skeleton Page)指的是當你開啟一個移動端 web 頁面,在頁面解析和資料載入之前,首先給使用者展示頁面的大概樣式。在骨架頁面中,圖片、文字、圖示都將通過灰色矩形塊或圓形塊來展示,在真實頁面展示之前,使用者能夠感知到即將載入頁面的基本 CSS 樣式和頁面佈局。

骨架屏初體驗

一開始在我腦子裡,以為骨架屏是一個頁面去手寫一個css和Html,或者說是讓ui去設計一個骨架圖。但是這樣是有缺點的,比如產品改需求了呢,不僅要去修改程式碼,還要去重新修改骨架頁面或者骨架圖?

後來,看到了餓了麼大神的文章和很成熟的產品page-skeleton-webpack-plugin ,瞬間明白了我和大佬的區別

WechatIMG2.jpeg-57.3kB

解析-餓了麼骨架屏

生成骨架頁面的基本方案

通過 puppeteer 在服務端操控 headless Chrome 開啟開發中的需要生成骨架頁面的頁面,在等待頁面載入渲染完成之後,在保留頁面佈局樣式的前提下,通過對頁面中元素進行刪減或增添,對已有元素通過層疊樣式進行覆蓋,這樣達到在不改變頁面佈局下,隱藏圖片、文字和圖片的展現,通過樣式覆蓋,使得其展示為灰色塊。然後將修改後的 HTML 和 CSS 樣式提取出來,這樣就是骨架頁面了。

在闡述具體生成骨架頁面之前,先了解下 puppeteer, GitHub 上是這樣介紹的。

v2-924f28bd0281fb8e45b19f1364cfaf8e_hd.jpg-19.2kB

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 一種自動化生成骨架屏的方案

相關文章