vue-cli項目首頁加載緩慢想要使用骨架屏效果,經過幾天的實踐,這里學習並記錄一下vue項目自動生成骨架屏方法。
前言:骨架屏的作用主要是在網絡請求較慢時,提供基礎占位,當數據加載完成,恢復數據展示。這樣給用戶一種很自然的過渡,不會造成頁面長時間白屏或者閃爍等情況。 常見的骨架屏實現方案有ssr
服務端渲染和prerender
兩種解決方案。
1、手動編寫骨架屏代碼(頁面簡單少 推薦)
該方法就是手動編寫項目index.html 入口文件,實現在vue項目初始化展示替換div#app根元素前的骨架屏效果。
2.通過預渲染手動書寫的代碼生成相應的骨架屏(頁面簡單少 推薦)
比如:vue-skeleton-webpack-plugin 自動生成並自動插入靜態骨架屏
https://github.com/lavas-project/vue-skeleton-webpack-plugin 插件源碼地址 md里面有具體的使用方法
以webpack4構建的項目為例開始擼代碼:
基於 Vue Webpack 模板應用這個插件的例子: SPA 中單個 Skeleton:
1. npm install vue-skeleton-webpack-plugin 安裝插件
2. 在 webpack 中引入插件:以4版本為例配置如下 vue.config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin'); const path = require('path') module.exports = { publicPath:'./', devServer: { proxy: { '/api':{ target: 'http://192.168.0.3:8123', // target: 'http://192.168.3.20:8154/', changeOrigin: true, pathRewrite: { '^/api': '/api' } }, }, disableHostCheck: true }, chainWebpack: config => { // 其他配置 config.entry('main').add('babel-polyfill') // main是入口js文件 // 其他配置 }, css: { extract: true, // css拆分ExtractTextPlugin插件,默認true - 骨架屏需要為true }, lintOnSave: false, configureWebpack: (config) => { // vue骨架屏插件配置 config.plugins.push(new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './src/components/entry-skeleton.js'), }, }, minimize: true, quiet: true, })) }, };
上面紅色的那行配置的是骨架屏的入口文件
骨架屏的組件如下:這是目錄
entry-skeleton.js
import Vue from 'vue'; import Skeleton from './Skeleton'; export default new Vue({ components: { Skeleton }, template: '<skeleton />' });
Skeleton.vue 這里面可以畫出自己適合的骨架拼樣子
<template> <div class="skeleton-wrapper"> <header class="skeleton-header"></header> <section class="skeleton-block"> <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg=="> <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg=="> </section> </div> </template> <script> export default { name: 'skeleton' }; </script> <style scoped> .skeleton-header { height: 152px; background: grey; margin-top: 60px; width: 152px; margin: 60px auto; } .skeleton-block { display: flex; flex-direction: column; padding-top: 8px; } </style>
預加載時:
加載后:
骨架屏的具體展現是需要自己畫的 這個實踐只是對於 SPA 中單個 Skeleton
下面同樣以這個為例去實踐SPA 中多個 Skeleton,以給單頁面的不同路由設置不同的骨架屏,也可以給多頁面設置,同時為了開發時調試方便,會將骨架屏作為路由寫入 router 中,可謂是相當體貼了。
同樣的重復上面的第一步
第二步在配置webpack的時候稍微有點不同
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin'); const path = require('path') module.exports = { publicPath:'./' , devServer: { proxy: { '/api':{ target: 'http://i.yd-jxt.com', // target: 'http://192.168.3.20:8154/', changeOrigin: true, pathRewrite: { '^/api': '/v2' } }, }, disableHostCheck: true }, chainWebpack: config => { // 其他配置 config.entry('main').add('babel-polyfill') // main是入口js文件 // 其他配置 }, lintOnSave: false, configureWebpack: (config) => { // vue骨架屏插件配置 config.plugins.push(new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './entry-skeleton.js'), }, }, minimize: true, quiet: true, router: { mode: 'hash', routes: [{ path: '/about', skeletonId: 'skeleton1' }, { path: '/', skeletonId: 'skeleton2' }, ] } })) }, };
上面着色的這段代碼就是分路由展現不同的骨架屏頁面
這是骨架屏入口文件配置
import Vue from 'vue'; import Skeleton1 from '@/components/skeleton/Skeleton1'; import Skeleton2 from '@/components/skeleton/Skeleton2'; export default new Vue({ components: { Skeleton1, Skeleton2 }, template: ` <div> <skeleton1 id="skeleton1" style="display:none"/> <skeleton2 id="skeleton2" style="display:none"/> </div> });
路由組件骨架屏頁面
缺點:vue-skeleton-webpack-plugin 樣式不能根據內容生成骨架屏,是一開始寫的什么就是什么,靈活度不高。第一種方式也是
看到這里相信都知道了如何在項目里如何使用vue-skeleton-webpack-plugin去實現骨架屏的加載,但是這樣的方式去實現骨架屏加載很明顯的
有個弊端就是每個路由頁面都需要出對應的圖然后前端還要自己繪制一遍,要是有一個能夠根據頁面結構自己生成相應頁面的骨架屏的方法就好了。
3.餓了么內部的生成骨架頁面的工具 page-skeleton-webpack-plugin(不推薦)
原理:
在等待頁面加載
渲染完成之后,在保留頁面布局樣式的前提下,通過對頁面中元素進行刪減或增添,對已有元素通過層疊樣
式進行覆蓋,這樣達到在不改變頁面布局下,隱藏圖片和文字,通過樣式覆蓋,使得其展示為灰色塊。然后
將修改后的 HTML 和 CSS 樣式提取出來,這樣就是骨架屏了.
缺點:
- 對於復雜的頁面也會有不盡如人意的地方
- 生成的骨架屏節點是基於頁面本身的結構和 CSS,存在嵌套比較深的情況,體積不會太小
- 只支持 history 模式
- 目前已經沒有人進行維護,不支持該種方式實現
4.css實現骨架屏(比較簡單的一種方式 推薦)
https://blog.csdn.net/weixin_40687883/article/details/101058899
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> @-webkit-keyframes skeleton-ani { 0% { left: -90% } to { left: 120% } } @keyframes skeleton-ani { 0% { left: -90% } to { left: 120% } } .skt-loading .skeleton { position: relative; overflow: hidden; border: none !important; border-radius: 5px; background-color: rgba(0, 0, 0, 0) !important; background-image: none !important; pointer-events: none; } .skt-loading .skeleton:after { content: ""; position: absolute; left: 0; top: 0; z-index: 9; width: 100%; height: 100%; background-color: #ebf1f8; display: block } .skt-loading .skeleton:not(.not-round):after { border-radius: 4px } .skt-loading .skeleton:not(.not-before):before { position: absolute; top: 0; width: 30%; height: 100%; content: ""; background: -webkit-gradient(linear, left top, right top, color-stop(0, hsla(0, 0%, 100%, 0)), color-stop(50%, hsla(0, 0%, 100%, .3)), to(hsla(0, 0%, 100%, 0))); background: -o-linear-gradient(left, hsla(0, 0%, 100%, 0) 0, hsla(0, 0%, 100%, .3) 50%, hsla(0, 0%, 100%, 0) 100%); background: linear-gradient(90deg, hsla(0, 0%, 100%, 0) 0, hsla(0, 0%, 100%, .3) 50%, hsla(0, 0%, 100%, 0)); -webkit-transform: skewX(-45deg); -ms-transform: skewX(-45deg); transform: skewX(-45deg); z-index: 99; -webkit-animation: skeleton-ani 1s ease infinite; animation: skeleton-ani 1s ease infinite; display: block } .skt-loading .skeleton.badge:after { background-color: #f8fafc } input { border: 1px solid #dcdcdc; width: 100%; } </style> </head> <body> <div id="skt-main" class="skt-loading"> <form> <label class="skeleton">姓名:</label> <div class="skeleton"> <input type="text" name=""> </div> <br> <label class="skeleton">愛好:</label> <div class="skeleton"> <input type="text" name=""> </div> </form> </div> <script> setTimeout(() => { var $sktmain = document.getElementById('skt-main'); $sktmain.className = $sktmain.className.replace('skt-loading', '') }, 2000) </script> </body> </html>
5.用純 DOM 的方式結合 Puppeteer 自動生成網頁骨架屏(比較靠譜的一種方式 推薦)
該方法需要下載【npm i draw-page-structure -g】 ,用純 DOM 的方式結合 Puppeteer 自動生成網頁骨架屏,原理是:
- 生成操作Dom的JavaScript腳本(該腳本用於將項目頁面轉換成色塊形式的骨架屏效果);
- 通過Puppeteer控制谷歌瀏覽器運行項目頁面並獲取頁面、將上一步的腳本注入該頁面,並生成骨架屏所需的Dom節點;
- 將自動生成的骨架屏Dom片段插入到應用頁面的根入口節點。
使用注意事項:
- 核心在於 DOM 操作,puppeteer 僅提供運行環境和導出方式
- 只要能訪問的頁面都能生成,history 與 hash 模式無限制
- 不受項目和框架的限制,vue 和 react 等項目零修改即可復用
- 生成色塊的單位為百分比,不同設備自適應
- 不需要 css-tree 來提取樣式,不依賴頁面本身的布局結構,生成扁平的 DOM 節點體積特別小
- 支持自定義生成方式與導出方式
詳細使用參考網址 https://www.imooc.com/article/253387、https://github.com/famanoder/dps
dps插件使用步驟:
1、使用命令【npm i draw-page-structure -g】安裝插件
2、dps init
生成配置文件 dps.config.js
3、
修改 dps.config.js
進行相關配置,包括想渲染的頁面url、通過includeElement和init方法調整骨架屏效果等;
一般來說,你需要按自己的項目情況來配置 dps.config.js ,常見的配置項有:
- url: 待生成骨架屏的頁面地址
- output.filepath: 生成的骨架屏節點寫入的文件
- output.injectSelector: 骨架屏節點插入的位置,默認 #app
- background: 骨架屏主題色
- animation: css3 動畫屬性
- rootNode: 真對某個模塊生成骨架屏
- device: 設備類型,默認 mobile
- extraHTTPHeaders: 添加請求頭
- init: 開始生成之前的操作
- includeElement(node, draw): 定制某個節點如何生成
- writePageStructure(html, filepath): 回調的骨架屏節點
4、dps start
開始生成骨架屏
編寫操作 DOM 的 Javascript 腳本步驟:
- 遍歷可見區域可見的 DOM 節點
包括:非隱藏元素、寬高大於 0 的元素、非透明元素、內容不是空格的元素、位於瀏覽窗口可見區域內的元素 - 針對(背景)圖片、文字、表單項、音頻視頻等區域,算出其所占區域的寬、高、距離視口的絕對距離等信息
- 對於符合生成條件的區域,一視同仁,生成相應區域的顏色塊
- “一視同仁”即對於符合條件的區域,不區分具體元素,不用考慮結構層級,不考慮樣式,統一生成 div 的顏色塊
- 該腳本的運行環境決定了獲取到的元素尺寸與相關距離單位不可控,可能需要做轉換,比如用的 rem、em、vh 等;我們采用比較簡單的方式,不取 style 的尺寸相關的值,而是通過 getBoundingClientRect 獲取寬、高、距離視口距離的絕對值,與當前設備的寬高,計算出相應的百分比作為顏色塊的單位,這樣來適配不同設備
- 對於頁面結構比較復雜或者大圖片比較多的頁面,會出現不盡如人意的地方,我們通過 includeElement(node, draw)和 init 兩個鈎子函數來支持自定義的微調
- 以上就能夠直接跑在瀏覽器生成骨架屏代碼了,手動添加到應用頁面
注意vue-cli4dps start 繪制出來的文件html需要替換public文件下面的html文件,當前前提是你沒有特意設置生成后的文件路徑,默認是在該項目文件下面,這個可解決上面方法的缺陷問題,相比之下已經比較友好,當然對於十分復雜的頁面是需要自己在方法里面寫自己指定渲染的東西,dps插件也有為此提供方法。
關於骨架屏的思考到這里並沒有結束,未完待續。。。。