Chrome扩展程序一键生成网页骨架屏

Stella981
• 阅读 398

对于依赖接口渲染的页面,在拿到数据之前页面往往是空白的,为了提示用户当前正在加载中,往往会使用进度条、loading图标或骨架屏的方式。对于前两种方案而言,实现比较简单;本文主要研究骨架屏的应用及实现,并给出一种使用Chrome扩展工具快速生成骨架屏的方案。

首先看看效果 先放一个动图展示

Chrome扩展程序一键生成网页骨架屏

掘金首页

Chrome扩展程序一键生成网页骨架屏

百度首页

Chrome扩展程序一键生成网页骨架屏

知乎首页

Chrome扩展程序一键生成网页骨架屏

安装插件后访问任意网页,基本上均可以一键生成骨架屏(由于时间关系,部分节点并没有完全实现,不过目前展示Demo应该是够了

本文所有代码均放在https://github.com/tangxiangmin/web-skeleton-extension上面了,时间关系代码写的比较潦草~

(写完准备提交Chrome应用商店的时候才发现,居然有一个类似的应用:skeleton extention~囧,就当瞎折腾一番了

骨架屏方案

目前有几种比较常见的骨架屏方案

  1. 使用图片、SVG实现骨架屏效果,可以让设计在提供页面设计稿的时候同步一份当前页面的骨架屏图片资源,这种方案开发成本较低,缺点是不太灵活,如果页面迭代比较频繁,会导致设计同事的工作量增大

  2. 手写HTML+CSS实现骨架屏,较第一种图片方案而言,可以更灵活地定制骨架屏UI和动画效果,缺点也很明显,开发和维护成本都较高;目前可以使用诸如react-content-loader等现有的骨架屏库,通过配置快速生成对应的骨架屏,但对于某些高度定制的页面也不能很好地满足业务需求

  3. 饿了么前端团队提供的一套方案: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-typeignore

  • 对于忽略节点类型而言,则使用 skeleton-exclude-type

     let type = $dom.attr(KEY) || getNodeSkeletonType($dom)  // 自动检测节点类型,并附上typelet 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-typeskeleton-exclude-type两个HTML属性,在开发页面的时候就可以直接指定骨架屏区块类型,方便定制业务需要的骨架屏。

当然逐个去配置skeleton-type也会显得比较繁琐,在getNodeSkeletonType中会尽可能地推断并生成比较符合要求的骨架屏效果;此外,基于Chrome扩展程序的工具还暴露了一个配置参数

 renderSkeleton("body", {    ignore: '',    selector: {        [key]: {include: '', exclude: ''}    },})复制代码

用掘金首页试一下

原本的效果

Chrome扩展程序一键生成网页骨架屏

如果不过滤的话,因为顶部导航栏使用的是ul,会默认识别成list,导致展示出现问题

Chrome扩展程序一键生成网页骨架屏

自定义配置后的效果

{    ignore: ['.banner .label'].join(','), // 隐藏右侧广告栏的提示按钮    selector: {        block: {            // 将表单转换成一个BLOCK            include: ['.add-group .more', '.search-form'].join(',')        },        list: {            // 排除导航栏的ul标签            exclude: ['.nav-list'].join(',')        },    },}复制代码

Chrome扩展程序一键生成网页骨架屏

此外,我们也可以只传入某个节点而非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>根节点,用户看见的是一片空白,对于这种首屏应用,我们可以将得到的骨架屏代码直接嵌入到根节点中,这样当页面加载至应用初始化之前,用户都能看见完整的骨架屏效果。

Chrome扩展程序一键生成网页骨架屏

对于其他单页应用其他页面而言,也可以采用类似的原理,等待接口返回前展示骨架屏

<SkeletonFrame :frame="html" :loading="isLoading">    <RealPage></RealPage></SkeletonFrame>复制代码

比如我们可以封装一个SkeletonFrame组件,其接口包括

  • frameloading两个props,表示骨架屏HTML和是否正在加载,当loading为true时展示骨架屏

  • default slot需要真实渲染的页面组件,当loading为false时展示真正页面组件

局部骨架屏

对于一些需要持续加载的数据,如滚动加载等,可以先获取对应区块的骨架屏HTML代码(不需要整个页面的骨架屏代码),然后将其封装成待展示组件,与上面SkeletonFrame逻辑比较类似

小结

本文整理了骨架屏的实现原理,然后提供了一种通过Chrome扩展程序生成任意网页的骨架屏方案,主要包括

  • 将页面按节点类型拆分成不同区块

  • 支持自定义节点类型、忽略或隐藏节点

  • 导出并使用骨架屏HTML代码

当然,上面只是介绍了大概的实现思路,并给出了一个简单的Demo,距离实际应用还有一些距离,等后面有时间再进一步完善吧。本文所有代码均放在github上面了,欢迎提PR和issue。

近期

程序员都喜欢用的架构图工具

vue+ts打造一个酷炫的星空聊天室

Chrome扩展程序一键生成网页骨架屏

若此文有用,何不素质三连❤️

本文分享自微信公众号 - Vue中文社区(vue_fe)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
GitHub Star 10K,让你的网站更炫酷的开源库
!(https://oscimg.oschina.net/oscnet/d965c43d6fcb483facc0eb72f2996d21.png)来源:GitHub精选Hi!大家好呀!我是你们可爱的喵哥!现在不少网站都支持了骨架屏,能够在网页数据加载前,展示固定的布局,能够减少用户在进入网页时感受到白屏的不适感。今天要给大家推荐一
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
Unity横屏
Android下发现Unity里面的Player设置,并不能完全有效,比如打开了自动旋转,启动的时候还是会横屏,修改XML添加以下代码<applicationandroid:icon"@drawable/ic\_launcher"                    android:label"@string/app\_name"
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这