使用Chrome扩展程序生成网页骨架屏
bigegpt 2024-11-09 11:06 54 浏览
作者:橙红年代
转发链接:https://juejin.im/post/6856784900775739400
前言
如果小伙伴还不知道网页骨架屏是啥的,请先看看这篇:前端骨架屏都是如何生成的
对于依赖接口渲染的页面,在拿到数据之前页面往往是空白的,为了提示用户当前正在加载中,往往会使用进度条、loading图标或骨架屏的方式。对于前两种方案而言,实现比较简单;本文主要研究骨架屏的应用及实现,并给出一种使用Chrome扩展工具快速生成骨架屏的方案。
首先看看效果 先放一个动图展示
掘金首页
百度首页
知乎首页
安装插件后访问任意网页,基本上均可以一键生成骨架屏(由于时间关系,部分节点并没有完全实现,不过目前展示Demo应该是够了
本文所有代码均放在Github上面了,时间关系代码写的比较潦草~
Github: https://github.com/tangxiangmin/web-skeleton-extension
(写完准备提交Chrome应用商店的时候才发现,居然有一个类似的应用:skeleton extention~囧,就当瞎折腾一番了
骨架屏方案
目前有几种比较常见的骨架屏方案
- 使用图片、SVG实现骨架屏效果,可以让设计在提供页面设计稿的时候同步一份当前页面的骨架屏图片资源,这种方案开发成本较低,缺点是不太灵活,如果页面迭代比较频繁,会导致设计同事的工作量增大
- 手写HTML+CSS实现骨架屏,较第一种图片方案而言,可以更灵活地定制骨架屏UI和动画效果,缺点也很明显,开发和维护成本都较高;目前可以使用诸如react-content-loader等现有的骨架屏库,通过配置快速生成对应的骨架屏,但对于某些高度定制的页面也不能很好地满足业务需求
- 饿了么前端团队提供的一套方案:page-skeleton-webpack-plugin,其大致原理是使用puppeteer向指定页面注入代码,遍历页面节点,根据节点的类型渲染对应的骨架屏样式,然后结合webpack将返回的HTML自动注入到项目模板文件中,传送门:一种自动化生成骨架屏的方案:https://github.com/Jocs/jocs.github.io/issues/22
在最开始调研骨架屏方案的时候也注意到了page-skeleton-webpack-plugin,体验了一下发现并不能完全满足我的需求,对于骨架屏方案,我的期望是
- 使用方便,能够根据给定页面,自动生成对应的骨架屏,不需要安装额外的工具(不用安装puppeteer)
- 能够灵活地处理骨架屏生成的HTML文件,支持首屏或者部分区域使用骨架屏(不需要webapck自动注入模板文件,反之,需要实现为项目单个或多个页面配置对应骨架屏)
- 能够自定义骨架屏各个区块的样式,能够指定包含或者忽略某些节点,生成的代码体积要足够小(骨架屏并不需要完全还原原本页面)
恰好调研骨架屏方案的那段时间正在处理Chrome扩展程序的工单,发现page-skeleton-webpack-plugin借助puppeteer执行页面脚本的方案,完全可以通过扩展程序的content.js实现,这样,不需要借助本地开发环境也可以渲染骨架屏了
骨架屏实现原理
何为骨架?
首先需要保留节点的布局信息,这样就可以复用节点原有的样式,不会破坏整体布局,最后生成的骨架屏样式就可以与原本的页面保持一致。
然后为了保证生成样式的统一,需要覆盖节点原本的UI信息,包括背景色、图片、边框等。
由于骨架屏仅仅作为数据返回之前的占位,并不需要完全复原原本的页面,因此为了使整个骨架屏看起来比较简洁,可以忽略不必要的节点。
那么要处理哪些节点、忽略哪些节点呢?因此针对不同节点,我们需要进行判断并分别处理。
区分不同节点
这部分大量参考了一种自动化生成骨架屏的方案:https://github.com/Jocs/jocs.github.io/issues/22 这篇文章的思路,不妨移步阅读,下面简单整理一下各种不同节点的处理方案,并增加了一些额外的节点处理策略。
由于涉及大量的节点操作,因此使用了jQuery。
文字
把文字占据的空间看做上性高、内容、下行高,
- 行高部分用透明色,
- 内容部分用灰色
这样就可以展示原本的文字区域了,对于多行文字而言,显示的就是斑马状条纹形式的骨架屏结构
.line {
background-image: linear-gradient(red 25%,blue 25%, blue 75%, red 75%);
/*background-image: linear-gradient(red 25%,blue 0, blue 75%, red 0); // 与上面等价*/
}
复制代码
这里需要计算元素的行高、字体大小等信息
renderText($dom) {
let fontSize = parseFloat($dom.css("font-size"));
let lineHeight = $dom.css("line-height");
// todo 处理浏览器默认行高、包含继承、自定义等属性
if (lineHeight === "normal") {
lineHeight = fontSize * 1.4;
} else {
lineHeight = parseFloat(lineHeight);
}
const textHeightRatio = fontSize / lineHeight;
const firstColorPoint = (((1 - textHeightRatio) / 2) * 100).toFixed(2);
const secondColorPoint = (((1 - textHeightRatio) / 2 + textHeightRatio) * 100).toFixed(2);
const style = `--fp:${firstColorPoint}%;--sp:${secondColorPoint}%;--lh:${lineHeight}px;`;
$dom.addClass('sk-text');
$dom.attr("style", style);
}
复制代码
因为每个文本节点的字体大小和行高可能不一样,如果全在style标签上添加样式,则生成的HTML文件可能比较大,因此这里使用了CSS Var,然后统一在sk-text的样式类中处理背景色
.sk-text {
--c: #eee;
--fp: 0%;
--sp: 0%;
--lh: 0;
display: inline-block;
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
background-repeat: repeat-y !important;
background-image: linear-gradient(transparent var(--fp), var(--c) 0, var(--c) var(--sp), transparent 0);
background-size: 100% var(--lh);
color: transparent !important;
}
复制代码
图片
获取原始图片节点的宽高,然后使用1像素的base64灰色图片替换原始节点的src属性
renderImg($img) {
let width = $img.width()
let height = $img.height()
// 一像素灰色图片
let emptyImage = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
$img.attr("src", emptyImage);
$img.css({
background: "#eee",
width: width + "px",
height: height + "px"
})
}
复制代码
对于包含背景图片的区块而言,只需要将其背景覆盖成灰色即可。
按钮、input
使用灰色背景的块占据
边框
替换对应边框的颜色为骨架屏灰色
列表
列表元素是骨架屏中一个比较常见的区块,列表元素可以由上面这些区块组成,但是为了保证生成规整的骨架屏,采取的策略是使用移除多余的元素,使用第一个元素克隆占位
function renderList($dom) {
$dom.addClass("sk-list")
let $children = $dom.children()
let $child = $children.first()
let len = $children.length
// 列表元素子节点统一,保证页面骨架整齐
for (let i = 1; i < len; ++i) {
$children.eq(i).remove()
}
for (let i = 1; i < len; i++) {
let tmp = $child.clone(true)
$dom.append(tmp)
}
}
复制代码
遍历DOM树
当确定了不同类型节点的处理策略之后,就可以从入口节点遍历整个DOM树执行处理方法了
function preorder($dom) {
// ...获取节点类型,执行相关策略放啊
// 遍历子节点
$dom.children().each(function () {
const $this = $(this)
preorder($this)
});
}
复制代码
在遍历期间还需要处理一些特殊情况
- 不可见的元素及其子节点应该停止遍历
- 对于某些节点而言,可能存在一些定制操作
- 由用户指定节点类型,而不是默认根据节点nodeType推断
- 忽略某些节点的处理,如一个ul标签我们可能并不希望将其转换成列表
- 直接隐藏节点,如某些小图标等,为了保证骨架屏简洁,可能需要直接隐藏节点
基于这些场景,引入了skeleton-type的概念,对应上面章节提到的节点类型,在遍历时会优先读取节点上的该属性值,只有当属性值不存在时,才会根据nodeType自行推断相关的类型;
- 对于需要隐藏的节点,直接指定skeleton-type为ignore
- 对于忽略节点类型而言,则使用skeleton-exclude-type
let type = $dom.attr(KEY) || getNodeSkeletonType($dom) // 自动检测节点类型,并附上type
let excludeType = $dom.attr(KEY_EXCLUDE)
if (!excludeType || type !== excludeType) {
let handlers = {
[TEXT]: renderText,
[IMAGE]: renderImg,
[BLOCK]: renderBlock,
[BORDER]: renderBorder,
[BUTTON]: renderButton,
[LIST]: renderList,
[BACKGROUND_IMAGE]: renderBackgroundImage,
[INPUT]: renderInput,
[IGNORE]: renderIgnore
}
let handler = handlers[type]
handler && handler($dom)
}
复制代码
这样相当于暴露了skeleton-type与skeleton-exclude-type两个HTML属性,在开发页面的时候就可以直接指定骨架屏区块类型,方便定制业务需要的骨架屏。
当然逐个去配置skeleton-type也会显得比较繁琐,在getNodeSkeletonType中会尽可能地推断并生成比较符合要求的骨架屏效果;此外,基于Chrome扩展程序的工具还暴露了一个配置参数
renderSkeleton("body", {
ignore: '',
selector: {
[key]: {include: '', exclude: ''}
},
})
复制代码
用掘金首页试一下
原本的效果
如果不过滤的话,因为顶部导航栏使用的是ul,会默认识别成list,导致展示出现问题
自定义配置后的效果
{
ignore: ['.banner .label'].join(','), // 隐藏右侧广告栏的提示按钮
selector: {
block: {
// 将表单转换成一个BLOCK
include: ['.add-group .more', '.search-form'].join(',')
},
list: {
// 排除导航栏的ul标签
exclude: ['.nav-list'].join(',')
},
},
}
复制代码
此外,我们也可以只传入某个节点而非body的选择器,这样就只会生成对应节点的HTML
将骨架屏嵌入应用
骨架屏有两种比较常见的应用场景
- 用于首屏或某个页面打开时等待数据返回时的占位
- 用于滚动加载等列表页面占位
接下来研究在这两种场景下如何嵌入骨架屏
获取骨架屏代码
在前面遍历入口节点DOM树渲染骨架屏之后,我们只需要获取对应节点的HTML代码即可。由于没有修改原本布局信息,因此这段HTML代码在同一个页面是完全可以展示的。我们要做的就是拿到对应的骨架屏HTML代码。
由于整个操作都是在当前页面上执行的,拿到当前页面上某个节点的的html内容就变得十分简单了
- 通过控制台,找到节点然后访问innerHTML
- 通过调试工具Elements面板,然后Copy HTML
上面的方式需要手动去操作,我们可以使用扩展程序在骨架屏渲染结束之后自动保存对应的HTML代码
// chromeMsg是自己封装的一个`chrome.runtime.onMessage`工具
chromeMsg.on("createSkeleton", (params) => {
const {config} = params
// 默认页面根节点,可以导出某个dom容器的骨架屏结构
let content = renderSkeleton(".page", config)
// 处理对应的骨架屏HTML代码,这里只是简单打印,也可以直接保存文件到本地,或者通过HTTP接口调用某些钩子服务完成自动注入等功能
console.log(content)
})
复制代码
整页应用
对于整页骨架屏,我们需要先使用测试数据生成对应的骨架屏,然后将得到的HTML放入页面中即可。
以Vue等单页项目而言,在整个应用初始化之前,页面上只存在<div id="app"></div>根节点,用户看见的是一片空白,对于这种首屏应用,我们可以将得到的骨架屏代码直接嵌入到根节点中,这样当页面加载至应用初始化之前,用户都能看见完整的骨架屏效果。
对于其他单页应用其他页面而言,也可以采用类似的原理,等待接口返回前展示骨架屏
<SkeletonFrame :frame="html" :loading="isLoading">
<RealPage></RealPage>
</SkeletonFrame>
复制代码
比如我们可以封装一个SkeletonFrame组件,其接口包括
- frame和loading两个props,表示骨架屏HTML和是否正在加载,当loading为true时展示骨架屏
- default slot需要真实渲染的页面组件,当loading为false时展示真正页面组件
局部骨架屏
对于一些需要持续加载的数据,如滚动加载等,可以先获取对应区块的骨架屏HTML代码(不需要整个页面的骨架屏代码),然后将其封装成待展示组件,与上面SkeletonFrame逻辑比较类似
小结
本文整理了骨架屏的实现原理,然后提供了一种通过Chrome扩展程序生成任意网页的骨架屏方案,主要包括
- 将页面按节点类型拆分成不同区块
- 支持自定义节点类型、忽略或隐藏节点
- 导出并使用骨架屏HTML代码
当然,上面只是介绍了大概的实现思路,并给出了一个简单的Demo,距离实际应用还有一些距离,等后面有时间再进一步完善吧。本文所有代码均放在github上面了,欢迎提PR和issue。
作者:橙红年代
转发链接:https://juejin.im/post/6856784900775739400
相关推荐
- 悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)
-
新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...
- 高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源
-
凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...
- 微服务架构实战:商家管理后台与sso设计,SSO客户端设计
-
SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...
- 还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑
-
在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...
- Seata源码—6.Seata AT模式的数据源代理二
-
大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...
- 30分钟了解K8S(30分钟了解微积分)
-
微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...
- SpringBoot条件化配置(@Conditional)全面解析与实战指南
-
一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...
- 一招解决所有依赖冲突(克服依赖)
-
背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...
- 你读过Mybatis的源码?说说它用到了几种设计模式
-
学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...
- golang对接阿里云私有Bucket上传图片、授权访问图片
-
1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...
- spring中的资源的加载(spring加载原理)
-
最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...
- Android资源使用(android资源文件)
-
Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...
- 如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)
-
深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...
- @Autowired与@Resource原理知识点详解
-
springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...
- java的redis连接工具篇(java redis client)
-
在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)