網頁骨架屏自動生成方案(dps)

什麼是骨架屏

什麼是骨架屏呢?骨架屏(Skeleton Screen)是指在頁面數據加載完成前,先給用戶展現出頁面的大體結構(灰色佔位圖),在拿到接口數據後渲染出實際頁面內容而後替換掉。Skeleton Screen 是近兩年開始流行的加載控件,本質上是界面加載過程當中的過渡效果。 假如能在加載前把網頁的大概輪廓預先顯示,接着再逐漸加載真正內容,這樣既下降了用戶的焦灼情緒,又能使界面加載過程變得天然通暢,不會形成網頁長時間白屏或者閃爍。這就是Skeleton Screen!css

Skeleton Screen 能給人一種頁面內容「已經渲染出一部分」的感受,相較於傳統的 loading 效果,在必定程度上可提高用戶體驗。html

骨架屏的實現方案

目前生成骨架屏的技術方案大概有三種:前端

  1. 使用圖片、svg 或者手動編寫骨架屏代碼:使用 HTML + CSS 的方式,咱們能夠很快的完成骨架屏效果,可是面對視覺設計的改版以及需求的更迭,咱們對骨架屏的跟進修改會很是被動,這種機械化重複勞做的方式此時未免顯得有些機動性不足;
  2. 經過預渲染手動書寫的代碼生成相應的骨架屏:該方案作的比較成熟的是 vue-skeleton-webpack-plugin,經過 vueSSR 結合 webpack 在構建時渲染寫好的 vue 骨架屏組件,將預渲染生成的 DOM 節點和相關樣式插入到最終輸出的 html 中。
// webpack.conf.js
 const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');

 plugins: [
  //...
  new SkeletonWebpackPlugin({
    webpackConfig: {
      entry: {
        app: resolve('./src/entry-skeleton.js')
      }
    }
  })
 ]
複製代碼

該方案的前提一樣是編寫相應頁面的骨架屏組件,而後預渲染生成骨架屏所需的 DOM 節點,但因爲該方案與 vue 相關技術直接關聯,在當今前端框架三分天下的大環境下,咱們可能須要一個更加靈活、可控的方案;vue

3 . 餓了麼內部的生成骨架頁面的工具:該方案經過一個 webpack 插件 page-skeleton-webpack-plugin 的方式與項目開發無縫集成,屬於在自動生成骨架屏方面作的很是強大的了,而且能夠啓動 UI 界面專門調整骨架屏,可是在面對複雜的頁面也會有不盡如人意的地方,並且生成的骨架屏節點是基於頁面自己的結構和 CSS,存在嵌套比較深的狀況,體積不會過小,而且只支持 history 模式。node

// webpack.conf.js
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')
 const path = require('path')

 plugins: [
  //...
  new HtmlWebpackPlugin({
    // Your HtmlWebpackPlugin config
  }),
  new SkeletonPlugin({
    pathname: path.resolve(__dirname, `${customPath}`), // 用來存儲 shell 文件的地址
    staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
    routes: ['/', '/search'], // 將須要生成骨架屏的路由添加到數組中
  })
 ]
複製代碼

咱們的實現方案

後來仔細想一想,骨架屏這幅樣子不是和一堆顏色塊拼起來的頁面同樣嗎?對比現有的骨架屏方案,這個想法有點「走捷徑」的感受。再進一步思考,這些色塊基於當前頁面去分析節點來生成,不如來段 JS 分析頁面節點,一頓 DOM 操做生成顏色塊拼成骨架屏。那麼問題來了,該怎麼樣精確的分析頁面節點,不一樣節點又該生成什麼樣的色塊呢?webpack

既然骨架屏表明了頁面的大體結構,那麼須要先用 js 對頁面的結構進行分析。分析以前,咱們須要制定一種規則,以肯定須要排除哪些節點?哪些種類的節點須要生成顏色塊?生成的顏色塊如何定位等等。咱們初步定下的規則以下:css3

  1. 只遍歷可見區域可見的 DOM 節點,包括: 非隱藏元素、寬高大於 0 的元素、非透明元素、內容不是空格的元素、位於瀏覽窗口可見區域內的元素等;
  2. 針對(背景)圖片、文字、表單項、音頻視頻、Canvas、自定義特徵的塊等區域來生成顏色塊;
  3. 頁面節點使用的樣式不可控,因此不可取 style 的尺寸相關的值,可經過 getBoundingClientRect 獲取節點寬、高、距離視口距離的絕對值,計算出與當前設備的寬高對應的百分比做爲顏色塊的單位,來適配不一樣設備;

基於這套規則,咱們開始生成骨架屏: 首先,肯定一個 rootNode 做爲入口節點,好比 document.body,同時方便之後擴展到生成頁面內局部的骨架屏,由此入口進行遞歸遍歷和篩選,初步排除不可見節點。git

function isHideStyle(node) {
    return getStyle(node, 'display') === 'none' || 
        getStyle(node, 'visibility') === 'hidden' || 
        getStyle(node, 'opacity') == 0 ||
        node.hidden;
}
複製代碼

接下來判斷元素特徵,肯定是否符合生成條件,對於符合條件的區域,」一視同仁」生成相應區域的顏色塊。」一視同仁」即對於符合條件的區域不區分具體元素、不考慮結構層級、不考慮樣式,統一根據該區域與視口的絕對距離值生成 div 的顏色塊。之因此這樣是由於生成的節點是扁平的,體積比較小,同時避免額外的讀取樣式表、經過抽離樣式維持骨架屏的外觀,這種統一輩子成的方式使得骨架屏的節點更可控。基於那上述「走捷徑」的想法,該方法生成的骨架屏是由純 DOM 顏色塊拼成的。github

生成顏色塊的方法:web

const blocks = [];
// width,height,top,left都是算好的百分比
function drawBlock({width, height, top, left, zIndex = 9999999, background, radius} = {}) {
  const styles = [
    'position: fixed',
    'z-index: '+ zIndex,
    'top: '+ top +'%',
    'left: '+ left +'%',
    'width: '+ width +'%',
    'height: '+ height +'%',
    'background: '+ background
  ];
  radius && radius != '0px' && styles.push('border-radius: ' + radius);
  // animation && styles.push('animation: ' + animation);
  blocks.push(`<div style="${ styles.join(';') }"></div>`);
}
複製代碼

繪製顏色塊並不難,繪製以前的分析確認纔是這個方案真正的核心和難點。好比,對於頁面結構比較複雜或者大圖片比較多的頁面,由圖片拼接的區域沒有邊界,生成的顏色塊就會緊挨着,出現不盡如人意的地方。再好比,一個包含不少符合生成條件的小塊的 card 塊區域,是以 card 塊爲準仍是以裏面的小塊爲準來生成顏色塊呢?若是以小塊爲準,繪製結果可能給人的感受壓根就不是一個 card 塊,再加上佈局方式和樣式的可能性太多,大大增長了不肯定因素。而若是以 card 塊爲準生成顏色塊的話還要對 card 塊作專門的規則。

目前來講,對於頁面結構不是特別複雜,不是滿屏圖片的,不是佈局方式特別「飄逸「的場景,該方式已經能夠生成比較理想的骨架屏了。而對於那些與預期相差較遠的狀況,咱們提供了兩個鉤子函數可供微調:

  1. init 函數,在開始遍歷節點以前執行,適合刪除干擾節點等操做。
  2. includeElement(node, draw) 函數,可在遍歷到指定節點時,調 用 draw 方法進行自定義繪製。

經過以上步驟就可以直接在瀏覽器中生成骨架屏代碼了。

在瀏覽器裏運行

因爲咱們的方案出發點是經過單純的 DOM 操做,遍歷頁面上的節點,根據制定的規則生成相應區域的顏色塊,最終造成頁面的骨架屏,因此核心代碼徹底能夠直接跑在瀏覽器端;

const createSkeletonHTML = require('draw-page-structure/evalDOM')

    createSkeletonHTML({
        // ...
        background: 'red',
        animation: 'opacity 1s linear infinite;'
    }).then(skeletonHTML => {
        console.log(skeletonHTML)
    }).catch(e => {
        console.error(e)
    })
複製代碼

結合 Puppeteer 自動生成骨架屏

雖然該方式已經能夠生成骨架屏代碼了,可是仍是不夠自動化,爲了讓生成的骨架屏代碼自動加載進指定頁面。因而,咱們開發了一個配套的 CLI 工具。這個工具經過 Puppeteer 運行頁面,並把 evalDOM.js 腳本注入頁面自動執行,執行的結果是生成的骨架屏代碼被插入到應用頁面。

咱們的方案大概思路以下:

接下來看看如何使用 CLI 工具生成骨架屏,最多隻需以下四步:

  1. 全局安裝,npm i draw-page-structure –g
  2. dps init 生成配置文件 dps.config.js
  3. 修改 dps.config.js 進行相關配置
  4. dps start 開始生成骨架屏

只需簡單幾步,然而並無繁瑣的配置:

通常來講,你須要按本身的項目狀況來配置 dps.config.js,常見的配置項有:

  • url: 待生成骨架屏的頁面地址
  • output.filepath: 生成的骨架屏節點寫入的文件
  • output.injectSelector: 骨架屏節點插入的位置,默認 #app
  • background: 骨架屏主題色
  • animation: css3動畫屬性
  • rootNode: 真對某個模塊生成骨架屏
  • device: 設備類型,默認 mobile
  • extraHTTPHeaders: 添加請求頭
  • init: 開始生成以前的操做
  • includeElement(node, draw): 定製某個節點如何生成
  • writePageStructure(html, filepath): 回調的骨架屏節點

詳細代碼及工具的使用請移步 Github

初步實現的效果:

  • 京東PLUS會員正式中首頁:

  • 京東PLUS會員正式中首頁,經過該方案生成的骨架屏效果:

  • 移動端百度首頁:

  • 移動端百度首頁,經過該方案生成的骨架屏效果:

總結

以上就是基於 DOM 的骨架屏自動生成方案,其核心是 evalDOM 函數。這個方案在不少場景下的表現仍是使人滿意的。不過,網頁佈局和樣式組合的可能性太多,想要在各類場景下都得到理想的效果,還有很長的路要走,但既然已經在路上,就勇敢的向前吧!

歡迎 star,歡迎提 PR!Github

相關文章
相關標籤/搜索