成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

對開發友好的前端骨架屏自動生成方案

開發 開發工具
一份來自 Akamai 的研究報告顯示,在對 1048 名網購戶進行采訪后發現:約 47% 的用戶期望他們的頁面在兩秒之內加載完成。

 一份來自 Akamai 的研究報告顯示,在對 1048 名網購戶進行采訪后發現:

約 47% 的用戶期望他們的頁面在兩秒之內加載完成。

如果頁面加載時間超過 3s,約 40% 的用戶會選擇離開或關閉頁面。

 

一直以來,為了提升用戶在頁面加載時的體驗,無論是 Web 還是 iOS、Android 的應用中,前端開發工程師都做了許多工作。除了解決如何讓網頁展現速度更快的問題,還有很重要的一點就是提升用戶對加載等待時間的感知。「菊花圖」以及由其衍生出的各種加載動畫就是一類常見的解決方案,相信無論是開發者還是用戶對下面這個圖標都不會陌生:

 

本文將要介紹的「骨架屏」則被視為菊花圖升級版的方案。受現有骨架屏方案的啟發,馬蜂窩電商前端研發團隊實現了一種自動化生成骨架屏的方法,并在馬蜂窩商城的多個頁面中實現應用,取得了不錯的效果。

一、什么是骨架屏

骨架屏可以理解為在頁面數據尚未返回或頁面未完成完全渲染前,先給用戶呈現一個由灰白塊組成的當前頁面大致結構,讓用戶產生頁面正在逐漸渲染的感受,從而使加載過程從視覺上變得流暢。生成后的骨架屏頁面如下圖所示:

 

骨架屏的主要優勢為:

1.用戶避免看到長時間的白頁

2.可以獲知頁面的大體結構,減小用戶認為頁面出錯而離開的機率

3.與菊花圖相比視覺更加流暢

二、常見的前端骨架屏方案

在選擇骨架屏之前,我們也考慮了一些其他的方法,比如能否通過服務端渲染(SSR)的方式來避開前端白屏時間的問題。但發現需要涉及項目過多,還會涉及服務的構建與部署;或是通過 prerender-spa-plugin 提供簡單的預呈現,它對 SPA 支持友好,但需要額外的 webpack 配置,且因為包源的問題,下載時間過長,有時還會莫名失敗,等等,都因為種種原因最終放棄。

經過一系列調研后,我們對業界常見的幾種骨架屏解決方案,以及它們的優勢、不足進行了一個簡單的梳理。

1. UI 骨架屏圖

即通過 UI 提供符合頁面首頁樣式的圖來充當骨架屏,將骨架屏 base64 圖片插入 root 根節點,在 webpack 打包時嵌入項目中。

這是一種簡單粗暴的方法,實現起來比較容易。但缺點也很明顯,就是需要 UI 設計師支持和開發介入,不能自動生成。

2. 手寫骨架屏

即通過手寫 HTML、CSS 的方式為目標頁定制骨架屏。這種方式可以做到對頁面真實樣式的復刻。不過一旦由于各種原因導致頁面樣式發生改變,就需要再改一遍骨架屏的樣式和布局,極大增加了維護的成本。

3. 自動生成靜態骨架屏

目前比較受關注的是餓了么開源的插件 page-skeleton-webpack-plugin,其具體實現原理為:

  • 生成骨架屏

通過 Puppter 操控 handless Chrome 打開需要生成的骨架屏頁面,在等待頁面加載完成之后,保留頁面布局樣式的前提下,通過對頁面中元素進行增刪,對已有元素通過層疊樣式進行覆蓋,使其展示為灰白塊。然后將修改后的 HTML 和 CSS 提取出來,將頁面分為不同的塊區域,例如文本塊、圖片塊、按鈕塊、SVG、偽類元素塊等,分別對每個塊進行處理,使其盡量與原頁面保持一致。這里用到了 Puppetter page 實例的 addScriptTag 方法來將處理塊的腳本插入到 headless Chrome 打開的頁面當中。

實際生成的骨架屏頁面與原頁面可能還會存在差距,插件通過 memory-fs 將骨架屏寫入內存中,可以通過預覽頁面對生成的骨架屏進行二次編輯和效果預覽,修改完成后點擊生成按鈕就能生成一份新的骨架屏寫入到項目中。

借一張圖來說明:

  • 插入骨架屏

骨架屏的 DOM 結構和 CSS 通過離線生成后,在構建時注入模板 (EJS) 中的節點下面,插入到 HTML 是在 after-emit 鉤子函數中進行。

page-skeleton-webpack-plguin 生成骨架屏的方案可以根據項目中不同的路由頁面生成相應的骨架屏頁面,并將骨架屏頁面通過 webpack 打包到對應的靜態路由頁面中。

它的不足之處在于:

  • 實際使用過程中無法監聽接口返回導致生成骨架屏的時機是否準確
  • 生成的頁面與業務人員寫的結構質量有直接關系,經常出現需要手工二次調整的情況

在這樣的背景下,馬蜂窩電商研發前端團隊希望找一種在提升用戶體驗的同時,對開發更友好的骨架屏生成方式,能針對不同的業務場景自動生成出相似的骨架屏,并且實現自動注入。對于開發而言,只需要執行一條命令,或者簡單配置,就可以生成骨架屏,不需要再考慮后續的維護工作。

在方案調研過程中,draw-page-structure 為我們的設計提供了靈感。

4. draw-page-structure

  • 生成骨架屏:
  1. // dps.config.js 
  2.   url: 'https://baidu.com'
  3.   output: { 
  4.     filepath: '/Users/famanoder/DrawPageStructure/example/index.html'
  5.     injectSelector: '#app' 
  6.   }, 
  7.   background: '#eee'
  8.   animation: 'opacity 1s linear infinite;'
  9.   // ... 

根據 URL 指定的線上地址,配合 Puppeteer 獲取當前頁面的 DOM 結構,并對其中元素節點生成骨架屏文件到 filepath 指定的文件里面,就可以生成骨架屏頁面,結果如下圖所示:

  • 插入骨架屏

將上述生成的骨架屏文件插入到頁面根節點下面一般為 id="app" 的節點,然后在通用工具里提供主動銷毀骨架屏的方法,就可以幫助開發主動控制或銷毀骨架屏,顯示頁面真實內容。

draw-page-structure 的設計思想很大程度上可以滿足我們的需求,不足的是只能對線上已經存在的 URL 生成骨架屏,不支持開發環境。另外由于是自動生成,當頁面存在重定向(如果未登錄重定向到登錄頁面)的情況時,生成的骨架屏可能與預期不一致。而且它的內部實現并不完善,可能導致某些結構復雜的頁面下生成的骨架屏需要二次優化調整。

于是,我們開始了進一步的探索。

三、對開發更友好的實現方案

1. 設計思路

基于對現有方案的借鑒,我們想到了在配置文件中指定要生成骨架屏的頁面 URL 和文件輸出的目錄,運行時讀取配置文件中的配置項,通過 Pupeteer 打開指定的頁面并注入 evalDom.js 的方法。因為此 JS 是在 Pupeteer 里面執行的,所以可以獲取到當前頁面完整的 DOM 結構,這給我們留下了非常大的發揮空間。

最初我們是從獲取到的 DOM 結構中的 body 標簽出發,遞歸去處理頁面上的所有節點,處理完成后用生成的 DIV 替換原有元素的位置。第一版方案中通過 getBoundingClientRect 和 getComputedStyle 的方法來獲取元素所有計算屬性和相對于視口的寬高和位置,然后結合元素本身的樣式屬性遞歸渲染,保留頁面原始 DOM 嵌套層次。

但由于能夠決定元素位置的屬性實在太多,如 position,z-index、width、height、top、display、box-sizing、flex 等都需要考慮,導致無法聚焦對頁面 DOM 結構處理的邏輯,而且這些屬性在處理完成后還需要加到最終生成骨架屏節點的 style 上,這樣骨架屏文件可能比原來完整的頁面結構還大,這肯定不是我們希望的。

優化后的方案是用 getBoundingClientRect 和 getComputedStyle 獲取元素相關屬性,然后直接通過絕對定位的方式來生成最終的骨架屏節點。這樣在頁面上最終需要的屬性主要是 position、z-index、top、left、width、height、background、border-radius。除了無法保證頁面原始的 DOM 結構,其它需求基本都可以滿足,也更加聚焦于節點的處理。

主要實現流程如下圖:

該方案目前主要應用于馬蜂窩電商業務的多頁面項目中,包括下單頁、簽證頁等,以下單頁為例,展示效果如下圖:

2. 實現方式

  • 生成骨架屏

(1) config.js 配置

  1. const dpsConfig = { 
  2.    // 默認生成位置為當前項目目錄skeleton文件夾,已有骨架屏頁面不會再次生成,新頁面配置只需要添加新條目即可 
  3.     visa_guide: { 
  4.         url: 'https://w.mafengwo.cn/sfe-app/visa_guide.html?mdd_id=10083', // 必填項 
  5.     }, 
  6.     call_charge: { 
  7.         url: 'http://localhost:8081/sfe-app/call_charge.html?rights_id=25', // 必填項 待生成骨架屏頁面的地址,用百度(https://baidu.com)試試也可以 
  8.         //url:'https://www.baidu.com'
  9.         device: 'pc', // 非必填,默認mobile 
  10.         background: '#eee', // 非必填 
  11.         animation: 'opacity 1s linear infinite;', // 非必填 
  12.         headless:false, // 非必填 
  13.         customizeElement: function(node) { // 非必填 
  14.             //返回值枚舉如果是true表示不會向下遞歸到這層為止,如果返回值是一個對象那么節點的檔子就按照對象里面的樣式來繪制 
  15.             //如果返回值為0表示正常遞歸渲染 
  16.             //如果返回值為1表示渲染當前節點不在向下遞歸 
  17.             //如果返回值為2表示對當前節點不作任何處理 
  18.             if(node.className === 'navs-bottom-bar'){ 
  19.                 return 2; 
  20.             } 
  21.             return 0; 
  22.         }, 
  23.         showInitiativeBtn: true,// 非必填 如果此值設置為true表示開發需要主動觸發生成骨架屏了,此時headless需設置為false 
  24.         writePageStructure: function(html) { // 非必填 
  25.             // 自己處理生成的骨架屏 
  26.             // fs.writeFileSync(filepath, html); 
  27.             // console.log(html) 
  28.         }, 
  29.         init: function() { // 非必填 
  30.             // 生成骨架屏之前的操作,比如刪除干擾節點 
  31.         }   
  32.     } 
  33.  
  34.  
  35. module.exports = dpsConfig; 

(2)Pupeteer 新打開頁面并返回瀏覽器實例、openPage

  1. const ppteer = require('puppeteer'); 
  2. const { log, getAgrType } = require('./utils'); 
  3. const insertBtn = require('../insertBtn'); 
  4.  
  5.  
  6. const devices = { 
  7.   mobile: [375, 667, 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'], 
  8.   ipad: [1024, 1366, 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1'], 
  9.   pc: [1200, 1000, 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
  10. }; 
  11.  
  12. async function pp({device = 'mobile', headless = true, showInitiativeBtn = false}) { 
  13.  
  14.   const browser = await ppteer.launch({headless});//返回browser實例 
  15.  
  16.   async function openPage(url, extraHTTPHeaders) { 
  17.     const page = await browser.newPage(); 
  18.     let timeHandle = null
  19.  
  20.    if(showInitiativeBtn){ 
  21.     browser.on('targetchanged', async ()=>{//監聽頁面路由變化,并獲取當前標簽頁的最新的頁面,在showInitiativeBtn為true時插入按鈕由開發控制主動生成骨架屏 
  22.       const targets = await browser.targets(); 
  23.       const currentTarget = targets[targets.length - 1] 
  24.       const currentPage = await currentTarget.page(); 
  25.  
  26.       clearTimeout(timeHandle) 
  27.       setTimeout(()=>{ 
  28.         if(currentPage){ 
  29.           currentPage.evaluate(insertBtn); 
  30.         } 
  31.       },300) 
  32.     }) 
  33.    } 
  34.     try{ 
  35.       let deviceSet = devices[device]; 
  36.       page.setUserAgent(deviceSet[2]); 
  37.       page.setViewport({width: deviceSet[0], height: deviceSet[1]}); 
  38.  
  39.       if(extraHTTPHeaders && getAgrType(extraHTTPHeaders) === 'object') { 
  40.         await page.setExtraHTTPHeaders(new Map(Object.entries(extraHTTPHeaders))); 
  41.       } 
  42.       await page.goto(url, { 
  43.         waitUntil: 'networkidle0'//不再有網絡連接時觸發(至少500ms后) 
  44.       }); 
  45.     }catch(e){ 
  46.       console.log('\n'); 
  47.       log.error(e.message); 
  48.     } 
  49.     return page; 
  50.   } 
  51.   return { 
  52.     browser, 
  53.     openPage 
  54.   } 
  55. }; 
  56.  
  57. module.exports = pp; 

(3)在瀏覽器環境里執行 evalDom.js 和 evalDom.js 中處理 node 節點的主要邏輯

  1. agrs.unshift(evalScripts);//evalScripts = require('../evalDOM');在pupeteer里執行evalDom.js并將config.js里配置的參數傳遞給evalDom 
  2. html = await page.evaluate.apply(page, agrs); 
  1. //evalDom.js主要邏輯 
  2. startDraw: function () { 
  3.     const $this = this; 
  4.     const nodes = this.rootNode.childNodes; 
  5.     this.beforeRenderDomStyle(); 
  6.     function childNodesStyleConcat(childNodes) { 
  7.         for (let i = 0; i < childNodes.length; i++) { 
  8.             const currentChildNode = childNodes[i];//當前子節點 
  9.             //有哪些節點要跳過繪制骨架屏的過程 
  10.  
  11.             if ($this.shouldIgnoreCurrentElement(currentChildNode)) { //是否應該忽略當前節點,不采取任何措施。后續這個地方可以由用戶指定哪些節點應該被略去,todo 
  12.                 continue
  13.             } 
  14.  
  15.             const backgroundHasurl = analyseIfHadBackground(currentChildNode); 
  16.             const hasDirectTextChild = childrenNodesHasText(currentChildNode);//判斷當前元素是不是有直接的子元素并且此元素是Text 
  17.             if ($this.customizeElement && $this.customizeElement(currentChildNode) !== 0 && $this.customizeElement(currentChildNode) !==  undefined) { 
  18.                 //開發者自定義節點需要渲染的樣子,默認返回false表示使用正常遞歸的算法來處理。如果返回值是true表示不會在向下遞歸,如果返回值是一個對象那么表示開發需要自定義樣式此時直接繪制就好。todo 
  19.                 if (getArgtype($this.customizeElement(currentChildNode)) === 'object') { 
  20.                     console.log('object'); 
  21.                     //此處如果返回一個對象表示對象要自定義最后繪制的對象 
  22.                 } else if ($this.customizeElement(currentChildNode) === 1) { 
  23.                     //如果此時返回true,表示此節點要過濾 
  24.                     getRenderStyle(currentChildNode); 
  25.                 } else if ($this.customizeElement(currentChildNode) === 2){ 
  26.                     continue ; 
  27.                 } 
  28.                 continue
  29.             } 
  30.             if (backgroundHasurl || analyseIsEmptyElement(currentChildNode) || hasDirectTextChild || shouldDrawCurrentNode(currentChildNode)) { //如果當前元素是內聯元素或者當前元素非內聯元素,但是不包含子節點或者子節點都是內聯元素的話那么我們就在當前的骨架屏上繪制此節點。                    
  31.                 getRenderStyle(currentChildNode, hasDirectTextChild); 
  32.             } else if (currentChildNode.childNodes && currentChildNode.childNodes.length) { //如果當前節點包含子節點 
  33.                 //遞歸 
  34.                 childNodesStyleConcat(currentChildNode.childNodes); 
  35.             } 
  36.         } 
  37.     } 
  38.     childNodesStyleConcat(nodes); 
  39.     return this.showBlocks(); 
  40. }, 
  • 上述 rootNode 為根節點,默認為 document.body 或者可以由開發指定
  • 主要邏輯為判斷當前節點是否需要忽略、是否設置了背景圖片、是否含有文本信息、開發是否指定了當前節點的處理方式等,對滿足條件的渲染其對應的骨架屏節點,否則處理當前節點的子節點
  • 所有節點處理完成后,調用 showBlocks 將生成的骨架屏節點拼接位 HTML 字符串,以便后續處理

(4) getRenderStyle 生成骨架屏樣式

  1. const styles = [ 
  2.     'position: fixed'
  3.     `z-index: ${zIndex}`, 
  4.     `top: ${top}%`, 
  5.     `left: ${left}%`, 
  6.     `width: ${width}%`, 
  7.     `height: ${height}%`, 
  8.     'background: '+(background || '#eee'), 
  9. ]; 
  10. const radius = getStyle(node, 'border-radius'); 
  11. radius && radius != '0px' && styles.push(`border-radius: ${radius}`); 
  12. blocks.push(`<div style="${styles.join(';')}"></div>`); 

zIndex、top、left、width、height 為處理后的屬性,然后把所有骨架屏節點的字符串都 push 進 blocks 這個數組中。

(5) 最終生成骨架屏的 HTML 文件如下:

  1. <html><head></head> 
  2.     <body><div style="position: fixed;z-index: 999;top: 89.805%;left: 4.267%;width: 91.467%;height: 11.994%;background: #eee"></div></body></html> 
  • 插入骨架屏

在項目入口 index.html 文件內添加

  1. <body> 
  2.     <div id="app"
  3.     </div> 
  4.     <% if(htmlWebpackPlugin.options.hasSkeleton) { %> 
  5.         <div id="skeleton"><!-- 骨架屏通過htmlWebpackPlugin在啟動打包的時候自動注入 --> 
  6.             <%= htmlWebpackPlugin.options.loading.html %> 
  7.         </div> 
  8.     <% } %> 
  9.     <!-- built files will be auto injected --> 
  10. </body> 

四、總結

目前,該方案已經支持由開發主動控制骨架屏生成時間,這樣就避免了頁面重定向的過程中無法生成正確的骨架屏,同時可以支持在本地開發時生成骨架屏。未來我們將實現支持開發自定義生成骨架屏節點的樣式和組件骨架屏的生成,并優化 evalDom.js 內部節點過濾、處理的算法。敬請期待!

本文作者:康岑波、孫昊男,馬蜂窩電商平臺前端研發工程師。

【本文是51CTO專欄作者馬蜂窩技術的原創文章,作者微信公眾號馬蜂窩技術(ID:mfwtech)】 

戳這里,看該作者更多好文

 

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2023-08-25 09:51:21

前端開發

2025-01-23 08:36:27

CSS開發工具

2013-03-25 16:35:04

微信微信公眾平臺開發者

2021-03-30 07:47:46

SVG 濾鏡 CSS技巧

2012-06-13 02:10:46

Java并發

2012-07-19 10:59:18

Jav并發

2009-06-16 11:13:38

Javadoc生成方法Javadoc

2017-07-10 17:25:40

開發單號高可用

2025-03-28 10:27:29

2017-06-19 17:55:22

CASID分布式

2024-04-30 11:49:16

瀏覽器前端開發折疊屏應用

2015-04-23 16:28:06

開發項目編程習慣

2023-03-14 18:06:07

flink數字集成

2023-12-09 17:04:26

nDPIHyperscan

2014-09-24 10:18:29

開發者開發習慣

2014-10-13 11:00:14

編程習慣不良開發項目

2023-10-09 08:31:19

2023-09-26 07:43:22

工具骨架屏頁面

2023-03-17 16:30:15

云集成架構組件

2024-02-22 17:02:09

IDUUID雪花算法
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 中文字幕一区在线 | 午夜精品久久久久久久星辰影院 | 久久精品中文 | 午夜看片网站 | 精品久久久久久久久久久久久久 | www国产精品| 欧美精品第一区 | 国产精品一区二区在线 | 在线一区视频 | 亚洲一级二级三级 | 国内精品一区二区 | 国产精品乱码一二三区的特点 | 欧美啊v在线观看 | 一区二区三区四区毛片 | 一区二区精品电影 | 日本一区二区电影 | 欧美999 | 精彩视频一区二区三区 | 日韩免费视频 | 91在线精品一区二区 | 欧美1—12sexvideos | 久久久激情 | 精品中文视频 | 黄色片免费看 | 久久免费精彩视频 | 日韩毛片免费视频 | 美女艹b| 国产欧美一区二区三区在线看 | 99pao成人国产永久免费视频 | 欧美黑人一区 | 欧美在线免费 | 日本高清不卡视频 | 天天干,夜夜操 | www精品美女久久久tv | 国产欧美日韩综合精品一 | 久久精品亚洲 | 中文字幕中文字幕 | 三级黄色片在线 | 日韩综合在线播放 | 国内精品伊人久久久久网站 | 亚洲国产aⅴ成人精品无吗 综合国产在线 |