一个前端非侵入式骨架屏自动生成方案

目录

背景

现有方案调研

侵入业务式手写代码

非侵入业务式手写代码

非侵入式骨架屏代码自动生成

技术方案

设计原则

架构图

骨架屏生成

骨架屏注入

优化点

部分技术细节解析

puppeteer

文本块处理

图片块处理

a 标签处理

自定义属性处理

首屏HTML处理

首屏样式处理

效果演示

业务实践

参考资料


背景

性能优化、减少页面加载时间、提升用户体验,是前端领域的一个永恒话题。在前后端分离、异步渲染在页面中被普遍应用的背景下,大量页面在用户访问时不可避免的会出现一段短时间白屏。目前的解决方案一般为以下几种:

  • 服务端同步渲染

  • 增加页面 loading

  • 增加页面首屏骨架屏

这几种方案各有优缺点,服务端同步渲染优点在于效果最好,缺点在于成本过高,需要在服务器部署、运维方面有较大的投入;页面loading是优点是通用性强、成本低,缺点是传递给用户的信息量过少;首屏骨架屏的优点在于能够给提前给用户充分信息量,预先获取用户关注点,让用户关注焦点提前聚焦到感兴趣位置,真实数据替换骨骼图过程过渡自然,渐进式渲染,要用户感知更快,缺点在于成本稍高,一般的解决方案要自己手写一份骨架屏代码。

综合以上分析,骨架屏是一种不错的解决加载过程中短暂白屏问题的解决方法。

现有方案调研

目前市面上的骨架屏方案大体可以分为以下几种。

侵入业务式手写代码

这种方式是直接在写业务代码的时候将骨架屏代码写好,该代码作为业务代码的一部分,每次修改骨架屏代码都相当于在修改业务代码。因此对业务代码有一定侵入性,后续维护成本略高。

示例如下:

  • Skeleton Screen -- 骨架屏[1]

  • Building Skeleton Screens with CSS Custom Properties[2]

非侵入业务式手写代码

这种方式依然需要手写骨架屏代码,不过该代码和业务代码分离,通过 webpack 注入的方式注入到项目源码中。好处是使骨架屏代码和业务代码解耦,后续维护成本降低,缺点是 webpack 有一定的配置成本。

示例如下:

  • Vue页面骨架屏注入实践[3]

  • Vue页面骨架屏[4]

  • Vue单页面骨架屏实践[5]

非侵入式骨架屏代码自动生成

这种方式无需手写骨架屏代码,骨架屏代码自动生成且自动注入到项目源码中。无须手写骨架屏代码,使用成本低是这种方式最明显的特点。

示例如下:

  • 一种自动化生成骨架屏的方案 - 饿了么[6]

技术方案

综合考虑现有方案的优缺点,我们决定采用使用成本最低的非侵入式骨架屏自动生成方案。站在巨人的肩膀上,参考饿了么骨架屏方案的设计思路,并加入一些新的优化思路,设计出一种新的骨架屏自动生成方案。

设计原则

设计方案前,先明确我们的设计原则。个人认为一个好的骨架屏方案应该具备以下原则:

  • 骨架屏自动生成

  • 使用和维护成本低

  • 配置灵活

  • 还原度高

  • 尽量不影响加载性能

基于以上设计原则,我们对方案进行了如下设计:

  • 骨架屏由 puppeteer 自动获取生成

  • 方案以 npm 包的方式落地,支持命令行、node 调用两种使用方式

  • 多种参数配置,可灵活配置页面地址、页面名称、viewport、输出路径、注入路径等

  • 基于真实页面做骨架处理后,获取页面截屏或源码,保证还原度

  • 采用 base64 图片作为骨架屏的默认输出形式,注入作为页面背景图片,体积小至几K,不增加额外网络请求,避免对加载性能造成影响

架构图

基于以上设计思路,对骨架屏方案进行设计。技术框架如下。

图片

该骨架屏方案分为骨架屏生成和骨架屏注入项目源码两个环节。下面对这两个环节分别进行介绍。

骨架屏生成

骨架屏生成分为准备、处理和输出三个阶段。

  • 准备阶段为使用 puppeteer 模拟打开目标页面,等待页面充分加载完成后;

  • 处理阶段为调用处理器进行脚本、图片、a标签、文本、自定义属性进行处理,并获取到首屏的 html 和样式 style 代码;

  • 输出阶段为将获取的骨架屏以"base64图片"和"HTML+样式style代码"两种形式进行输出。

骨架屏注入

骨架屏生成阶段有两种形式的产出,base64图片和 HTML+样式代码,考虑到 base64 图片比HTML源码整体要小很多(测试结果,base64图片只有4k大小,HTML源码有23k大小,并且 base64 图片可以非常灵活的作为页面背景图,不对页面中其他 DOM 节点造成干扰,具有注入量更小、使用更灵活的特点,因此本方案在骨架屏注入阶段默认注入 base64 图片作为页面背景图。

优化点

本方案基于社区现有骨架屏方案,主要做了如下优化:

  • 以 npm 包为最终形态,支持 node 和命令行两种使用方式,使用更加方便灵活;

  • 骨架屏注入直接通过 node 文本写入,无需进行 webpack 配置,使用门槛更低;

  • 骨架屏生成物有 base64 图片和 html 源码两种形式,方便不同使用场景使用。目前 base64 图片一般用作页面背景图,HTML源码用于在骨架屏出现不符合预期色块时的问题定位。

  • 骨架屏注入默认使用 base64 图片作为页面背景图方式。背景图片正常只有4k大小,同时又能够有更好的拓展性,比如可以非常灵活的支持为页面增加渐现效果,这一点在 html 源码形式下,就无法很好支持,会出现明显的页面闪动。

\

部分技术细节解析

puppeteer

Puppeteer(中文翻译”木偶”) 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome 。也可以配置为使用完整(非无头)的 Chrome。

使用示例:

const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.setViewport({width: 375, height: 812});// 事件监听,可用于事件通信page.on('console', msg => console.log('PAGE LOG:', msg.text()));page.on('warning', msg => console.log('PAGE WARN:', JSON.stringify(msg)));page.on('error', msg => console.log('PAGE ERR:', ...msg.args));// waitUntil 参数为 load/domcontentload/networkidle0/networkidle2await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});// 对打开的页面进行操作const dimensions = await page.evaluate(() => {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight,deviceScaleFactor: window.devicePixelRatio};});// 将页面截图,输出为 pdf 或 图片await page.pdf({path: 'hn.pdf', format: 'A4'});await page.screenshot({path: 'example.png'});await browser.close();
})();

这里有个参数非常实用,单独拎出来给大家看下。这个参数就是 waitUntil 参数解析:

waitUntil <string|Array<string>> When to consider navigation succeeded, defaults to load. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:load - consider navigation to be finished when the load event is fired.domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.

这个参数的主要作用是让 puppeteer 在对页面做打开、回退等操作时,等待一定时间再返回。一共有四个参数,分别是 load/domcontentloaded/networkidle0/networkidle2 。其中 networkidle0 和 networkidle2 比较特殊,值得注意,networkidle0 指在 500ms 内没有任何请求发出去,networkidle2 指在 500ms 内有不多于 2 个请求发出去。这两个参数可以保证让页面能够得到充分加载。避免在页面未完全加载完时就进行相关操作,最终操作结果和预期不一致。

文本块处理

文本块的处理相对比较复杂,一段文本(单行或多行),要将文本替换为和文本长度相同的灰色背景。文本块的容器也有 2 种可能:行内元素,如 span;块级元素,如 div。

下面我们队这样一个多行文本做处理:

图片

能够想到的一个比较直接的方法是给文本容器增加灰色背景色,但是添加后效果往往是这个样子的。

  • 行内元素容器下,如 span

    图片

  • 块级元素容器下,如 div

    图片

多行文本的情况下,会发现背景是黏在一起的,十分不美观,也没法让人一眼看出这是两行文本。

这种情况下应该如何处理呢,linear-gradient 是一个不错的解决思路。

使用 linear-gradient 对文本块进行背景处理。

  • 行内元素
    html:

<span class="text">我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题</span>

css:
效果:

图片

  • 块级元素
    块级元素添加背景后,会铺满正行,为让效果更像多行文本,会增加额外的末行背景遮盖处理。
    html:

<div class="text">我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题我是标题<span class="text-mask"></span></div>

css:
效果如图:

图片

图片块处理

图片处理逻辑较为简单,将所有 img 标签的 src 设为 1x1px 的灰色 base64 图片 ,背景色也设为相同色值的灰色。

Array.from(document.body.querySelectorAll('img')).map(img => {img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';img.style.backgroundColor = '#EEEEEE';
});

a 标签处理

为防止骨架屏的 html 形态中 a 标签仍然可点,将所有 a 标签的 href 设为 javascript:void(0); 。

Array.from(document.body.querySelectorAll('a')).map(a => {a.href = 'javascript:void(0);';
});

自定义属性处理

一个页面中元素一般非常多,所以按照默认的规则处理后,很有可能得到的骨架屏中色块比较杂乱,不够美观。这个时候可以使用如下四个自定义属性进行设置,将骨架屏效果调至最优。

属性:
data-skeleton-remove:指定进行移除的 dom 节点属性
data-skeleton-bgcolor:指定在某 dom 节点中添加的背景色
data-skeleton-ignore:指定忽略不进行任何处理的 dom 节点属性
data-skeleton-empty: 将某dom的innerHTML置为空字符串示例:
<div data-skeleton-remove><span>abc</span></div>
<div data-skeleton-bgcolor="transparent"><span>abc</span></div>
<div data-skeleton-ignore><span>abc</span></div>
<div data-skeleton-empty><span>abc</span></div>

处理前:

图片

处理后:

图片

首屏HTML处理

为了使生成的骨架屏 HTML 源码没有冗余,代码量最小,须对非首屏节点直接移除。处理逻辑中核心代码如下。

function inViewPort(ele) {try {const rect = ele.getBoundingClientRect()return rect.top < window.innerHeight &&rect.left < window.innerWidth} catch (e) {return true;}}

首屏样式处理

样式的处理同上,非首屏节点用到的样式也要移除,避免获取的 HTML 源码中样式冗余。

获取当前页面所有样式,非首屏样式直接移除,核心处理代码如下。

const styles = Array.from(document.querySelectorAll('style')).map(style => style.innerHTML || style.innerText);
// 移除非首屏样式
function handleStyles(styles, html) {const ast = cssTree.parse(styles);const dom = new JSDOM(html);const document = dom.window.document;const cleanedChildren = [];let index = 0;ast && ast.children && ast.children.map((style) => {let slectorExisted = false,selector;switch (style.prelude && style.prelude.type) {case 'Raw':selector = style.prelude.value && style.prelude.value.replace(/,|\n/g, '');slectorExisted = selectorExistedInHtml(selector, document);break;case 'SelectorList':style.prelude.children && style.prelude.children.map(child => {const children = child && child.children;selector = getSelector(children);if (selectorExistedInHtml(selector, document)) {slectorExisted = true;}});break;}if (slectorExisted) {cleanedChildren.push(style);}});ast.children = cleanedChildren;let outputStyles = cssTree.generate(ast);outputStyles = outputStyles.replace(/},+/g, '}');return outputStyles;
}function selectorExistedInHtml(selector, document) {if (!selector) {return false;}// 查询当前样式在 html 中是否用到let selectorResult, slectorExisted = false;try {selectorResult = document.querySelectorAll(selector);} catch (e) {console.log('selector query error: ' + selector);}if (selectorResult && selectorResult.length) {slectorExisted = true;}return slectorExisted;
}

效果演示

总结一下,该方案有如下优点:

  • 无代码侵入

  • 接入成本低

  • 两种产出,使用灵活

  • 自然过渡,避免闪屏

效果演示如下:

图片

业务实践

业务实践效果如下:

图片

参考资料

[1]

http://www.bestvist.com/p/50: https://link.juejin.cn?target=http%3A%2F%2Fwww.bestvist.com%2Fp%2F50

[2]

https://css-tricks.com/building-skeleton-screens-css-custom-properties/: https://link.juejin.cn?target=https%3A%2F%2Fcss-tricks.com%2Fbuilding-skeleton-screens-css-custom-properties%2F

[3]

https://segmentfault.com/a/1190000014832185: https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000014832185

[4]

https://segmentfault.com/a/1190000014963269: https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000014963269

[5]

https://segmentfault.com/a/1190000012403177: https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000012403177

[6]

https://gitissue.com/issues/5af2a74a9c2d3728a0a0de8b: https://link.juejin.cn?target=https%3A%2F%2Fgitissue.com%2Fissues%2F5af2a74a9c2d3728a0a0de8b

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/151761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[模版总结] - 树的基本算法3 - 结构转化

二叉树结构转化 通常将二叉树根据某些要求进行结构重构&#xff0c;比如线性结构转化(链表&#xff0c;数组)&#xff0c;序列化等。 常见题型 注&#xff1a;这类题目最基本的解题思路是利用递归分治 (也可以使用迭代方法)&#xff0c;在构建树结构的时候&#xff0c;我们通…

春秋云境靶场CVE-2022-28512漏洞复现(sql手工注入)

文章目录 前言一、CVE-2022-28512靶场简述二、找注入点三、CVE-2022-28512漏洞复现1、判断注入点2、爆显位个数3、爆显位位置4 、爆数据库名5、爆数据库表名6、爆数据库列名7、爆数据库数据 总结 前言 此文章只用于学习和反思巩固sql注入知识&#xff0c;禁止用于做非法攻击。…

前置语音群呼与语音机器人群呼哪个更好

最近通过观察自己接到的营销电话&#xff0c;通过语音机器人外呼的量应该有所下降。同时和客户交流获取到的信息&#xff0c;也是和这个情况类似&#xff0c;很多AI机器人群呼的量转向了OKCC前置语音群呼。询问原因&#xff0c;说是前置语音群呼转化更快&#xff0c;AI机器人群…

通过汇编理解cortex-m3:第0章

第0章&#xff1a;准备工作 基本想法&#xff1a;利用汇编和gdb调试&#xff0c;来学习cortex-m3汇编指令&#xff0c;以及一些寄存器的功能。 软件和硬件&#xff1a; 硬件&#xff1a;韦东山瑞士军刀中的最小核心板&#xff08;STM32F103C8T6&#xff09; STLINK-V2&#…

力扣刷题-二叉树-二叉树最小深度

给定一个二叉树&#xff0c;找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说明&#xff1a;叶子节点是指没有子节点的节点。&#xff08;注意题意&#xff09; 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#x…

【数据结构(二)】队列(2)

文章目录 1. 队列的应用场景和介绍1.1. 队列的一个使用场景1.2. 队列介绍 2. 数组模拟队列2.1. 思路分析2.2. 代码实现 3. 数组模拟环形队列3.1. 思路分析3.2. 代码实现 1. 队列的应用场景和介绍 1.1. 队列的一个使用场景 银行排队的案例&#xff1a; 1.2. 队列介绍 队列是一…

基于STC12C5A60S2系列1T 8051单片的IIC总线器件数模芯片PCF8591实现数模转换应用

基于STC12C5A60S2系列1T 8051单片的IIC总线器件数模芯片PCF8591实现数模转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍IIC总线器件数模芯片PCF8591介绍通过按…

前缀和(c++,超详细,含二维)

前缀和与差分 当给定一段整数序列a1,a2,a3,a4,a5…an; 每次让我们求一段区间的和&#xff0c;正常做法是for循环遍历区间起始点到结束点&#xff0c;进行求和计算&#xff0c;但是当询问次数很多并且区间很长的时候 比如&#xff0c;10^5 个询问和10^6区间长度&#xff0c;相…

基于机器学习的居民消费影响因子分析预测

项目视频讲解: 基于机器学习的居民消费影响因子分析预测_哔哩哔哩_bilibili 主要工作内容: 完整代码: import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import missingno as msno import warnings warnings.filterwarnin…

emq Neuron工业协议采集使用

emq Neuron工业协议采集使用 Neuron 简介 EMQ X Neuron 是运行在各类物联网边缘网关硬件上的工业协议商业化网关软件&#xff0c;支持一站式接入和解析数十种工业协议&#xff0c;并转换成 MQTT 协议接入工业物联网平台。用户可以通过基于 Web 的管理控制台可以实现在线的网关…

IDEA调用接口超时,但Postman可成功调用接口

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

消息中间的应用场景

1、异步处理 比如用户在电商网站下单&#xff0c;下单完成后会给用户推送短信或邮件&#xff0c;发短信和邮件的过程就可以异步完成。因为下单付款是核心业务&#xff0c;发邮件和短信并不属于核心功能&#xff0c;并且可能耗时较长&#xff0c;所以针对这种业务场景可以选择先…

OpenCV快速入门:直方图、掩膜、模板匹配和霍夫检测

文章目录 前言一、直方图基础1.1 直方图的概念和作用1.2 使用OpenCV生成直方图1.3 直方图归一化1.3.1 直方图归一化原理1.3.2 直方图归一化公式1.3.3 直方图归一化代码示例1.3.4 OpenCV内置方法&#xff1a;normalize()1.3.4.1 normalize()方法介绍1.3.4.2 normalize()方法参数…

JUnit 单元自动化

一、Junit 是什么&#xff1f; Junit 是 Java 中用于单元测试的框架。使用 Junit 能让我们快速高效的完成单元测试。 自动化测试&#xff1a;JUnit提供了自动化测试的能力&#xff0c;开发人员可以编写一次测试用例&#xff0c;然后通过简单的命令或集成到持续集成工具中进行…

TDengine Restful Authorization 自定义Token

Restful 接口是 TDengine 最常用的接口&#xff0c;仅次于 JDBC。TDengine 支持 HTTP 和 HTTPS&#xff0c;但通常情况下&#xff0c;大家不想搞证书&#xff0c;又在内网环境中&#xff0c;采用 HTTP 方式比较多。但 HTTP 是明文传输&#xff0c;只要抓个包就知道账号密码了。…

MySQL InnoDB 引擎底层解析(一)

6. InnoDB 引擎底层解析 MySQL 对于我们来说还是一个黑盒&#xff0c;我们只负责使用客户端发送请求并等待服务器返回结果&#xff0c;表中的数据到底存到了哪里&#xff1f;以什么格式存放的&#xff1f;MySQL 是以什么方式来访问的这些数据&#xff1f;这些问题我们统统不知…

AnyTXT Searcher:本地文件内容搜索神器如何搭建与远程访问

文章目录 前言1. AnyTXT Searcher1.1 下载安装AnyTXT Searcher 2. 下载安装注册cpolar3. AnyTXT Searcher设置和操作3.1 AnyTXT结合cpolar—公网访问搜索神器3.2 公网访问测试 4. 固定连接公网地址 前言 你是否遇到过这种情况&#xff0c;异地办公或者不在公司&#xff0c;想找…

iOS_折叠展开 FoldTextView

1. 显示效果 Test1&#xff1a;直接使用&#xff1a; Test2&#xff1a;在 cell 里使用&#xff1a; 2. 使用 2.1 直接使用 // 1.1 init view private lazy var mooFoldTextView: MOOFoldTextView {let view MOOFoldTextView(frame: .zero)view.backgroundColor .cyanvie…

Redis字典实现

前言 字典又称符号表&#xff0c;关联数组或者映射(map)。是一种保存键值对的抽象数据结构。在字典中一个键和一个值进行关联。这些关联的值被称为键值对。 字典中每一个键都是独一无二的&#xff0c;没有重复的。我们可以通过键来查找值&#xff0c;更新值或者删除整个键值对等…

如何定位el-tree中的树节点当父元素滚动时如何定位子元素

使用到的方法 Element 接口的 scrollIntoView() 方法会滚动元素的父容器&#xff0c;使被调用 scrollIntoView() 的元素对用户可见。 参数 alignToTop可选 一个布尔值&#xff1a; 如果为 true&#xff0c;元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 scrollIntoV…