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插件也有为此提供方法。
关于骨架屏的思考到这里并没有结束,未完待续。。。。