网页骨架屏自动生成方案(dps) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
famanoder
V2EX    前端开发

网页骨架屏自动生成方案(dps)

  •  
  •   famanoder
    famanoder 2019-07-20 01:39:36 +08:00 1704 次点击
    这是一个创建于 2278 天前的主题,其中的信息可能已经有所发展或是发生改变。

    什么是骨架屏

    什么是骨架屏呢?骨架屏(Skeleton Screen)是指在页面数据加载完成前,先给用户展示出页面的大致结构(灰色占位图),在拿到接口数据后渲染出实际页面内容然后替换掉。Skeleton Screen 是近两年开始流行的加载控件,本质上是界面加载过程中的过渡效果。 假如能在加载前把网页的大概轮廓预先显示,接着再逐渐加载真正内容,这样既降低了用户的焦灼情绪,又能使界面加载过程变得自然通畅,不会造成网页长时间白屏或者闪烁。这就是 Skeleton Screen !

    Skeleton Screen 能给人一种页面内容“已经渲染出一部分”的感觉,相较于传统的 loading 效果,在一定程度上可提升用户体验。

    骨架屏的实现方案

    目前生成骨架屏的技术方案大概有三种:

    1. 使用图片、svg 或者手动编写骨架屏代码:使用 HTML + CSS 的方式,我们可以很快的完成骨架屏效果,但是面对视觉设计的改版以及需求的更迭,我们对骨架屏的跟进修改会非常被动,这种机械化重复劳作的方式此时未免显得有些机动性不足;
    2. 通过预渲染手动书写的代码生成相应的骨架屏:该方案做的比较成熟的是 vue-skeleton-webpack-plugin,通过 vueSSR 结合 webpack 在构建时渲染写好的 vue 骨架屏组件,将预渲染生成的 DOM 节点和相关样式插入到最终输出的 html 中。
     // webpack.conf.js const SkeletOnWebpackPlugin= require('vue-skeleton-webpack-plugin'); plugins: [ //... new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: resolve('./src/entry-skeleton.js') } } }) ] 

    该方案的前提同样是编写相应页面的骨架屏组件,然后预渲染生成骨架屏所需的 DOM 节点,但由于该方案与 vue 相关技术直接关联,在当今前端框架三分天下的大环境下,我们可能需要一个更加灵活、可控的方案;

    3 . 饿了么内部的生成骨架页面的工具:该方案通过一个 webpack 插件 page-skeleton-webpack-plugin 的方式与项目开发无缝集成,属于在自动生成骨架屏方面做的非常强大的了,并且可以启动 UI 界面专门调整骨架屏,但是在面对复杂的页面也会有不尽如人意的地方,而且生成的骨架屏节点是基于页面本身的结构和 CSS,存在嵌套比较深的情况,体积不会太小,并且只支持 history 模式。

     // webpack.conf.js const HtmlWebpackPlugin = require('html-webpack-plugin') const { SkeletonPlugin } = require('page-skeleton-webpack-plugin') const path = require('path') plugins: [ //... new HtmlWebpackPlugin({ // Your HtmlWebpackPlugin config }), new SkeletonPlugin({ pathname: path.resolve(__dirname, `${customPath}`), // 用来存储 shell 文件的地址 staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同 routes: ['/', '/search'], // 将需要生成骨架屏的路由添加到数组中 }) ] 

    我们的实现方案

    后来仔细想想,骨架屏这幅样子不是和一堆颜色块拼起来的页面一样吗?对比现有的骨架屏方案,这个想法有点“走捷径”的感觉。再进一步思考,这些色块基于当前页面去分析节点来生成,不如来段 JS 分析页面节点,一顿 DOM 操作生成颜色块拼成骨架屏。那么问题来了,该怎么样精确的分析页面节点,不同节点又该生成什么样的色块呢?

    既然骨架屏代表了页面的大致结构,那么需要先用 js 对页面的结构进行分析。分析之前,我们需要制定一种规则,以确定需要排除哪些节点?哪些种类的节点需要生成颜色块?生成的颜色块如何定位等等。我们初步定下的规则如下:

    1. 只遍历可见区域可见的 DOM 节点,包括: 非隐藏元素、宽高大于 0 的元素、非透明元素、内容不是空格的元素、位于浏览窗口可见区域内的元素等;
    2. 针对(背景)图片、文字、表单项、音频视频、Canvas、自定义特征的块等区域来生成颜色块;
    3. 页面节点使用的样式不可控,所以不可取 style 的尺寸相关的值,可通过 getBoundingClientRect 获取节点宽、高、距离视口距离的绝对值,计算出与当前设备的宽高对应的百分比作为颜色块的单位,来适配不同设备;

    基于这套规则,我们开始生成骨架屏: 首先,确定一个 rootNode 作为入口节点,比如 document.body,同时方便以后扩展到生成页面内局部的骨架屏,由此入口进行递归遍历和筛选,初步排除不可见节点。

    function isHideStyle(node) { return getStyle(node, 'display') === 'none' || getStyle(node, 'visibility') === 'hidden' || getStyle(node, 'opacity') == 0 || node.hidden; } 

    接下来判断元素特征,确定是否符合生成条件,对于符合条件的区域,”一视同仁”生成相应区域的颜色块。”一视同仁”即对于符合条件的区域不区分具体元素、不考虑结构层级、不考虑样式,统一根据该区域与视口的绝对距离值生成 div 的颜色块。之所以这样是因为生成的节点是扁平的,体积比较小,同时避免额外的读取样式表、通过抽离样式维持骨架屏的外观,这种统一生成的方式使得骨架屏的节点更可控。基于那上述“走捷径”的想法,该方法生成的骨架屏是由纯 DOM 颜色块拼成的。

    生成颜色块的方法:

    const blocks = []; // width,height,top,left 都是算好的百分比 function drawBlock({width, height, top, left, zIndex = 9999999, background, radius} = {}) { const styles = [ 'position: fixed', 'z-index: '+ zIndex, 'top: '+ top +'%', 'left: '+ left +'%', 'width: '+ width +'%', 'height: '+ height +'%', 'background: '+ background ]; radius && radius != '0px' && styles.push('border-radius: ' + radius); // animation && styles.push('animation: ' + animation); blocks.push(`<div style="${ styles.join(';') }"></div>`); } 

    绘制颜色块并不难,绘制之前的分析确认才是这个方案真正的核心和难点。比如,对于页面结构比较复杂或者大图片比较多的页面,由图片拼接的区域没有边界,生成的颜色块就会紧挨着,出现不尽如人意的地方。再比如,一个包含很多符合生成条件的小块的 card 块区域,是以 card 块为准还是以里面的小块为准来生成颜色块呢?如果以小块为准,绘制结果可能给人的感觉压根就不是一个 card 块,再加上布局方式和样式的可能性太多,大大增加了不确定因素。而如果以 card 块为准生成颜色块的话还要对 card 块做专门的规则。

    目前来说,对于页面结构不是特别复杂,不是满屏图片的,不是布局方式特别“飘逸“的场景,该方式已经可以生成比较理想的骨架屏了。而对于那些与预期相差较远的情况,我们提供了两个钩子函数可供微调:

    1. init 函数,在开始遍历节点之前执行,适合删除干扰节点等操作。
    2. includeElement(node, draw) 函数,可在遍历到指定节点时,调 用 draw 方法进行自定义绘制。

    通过以上步骤就能够直接在浏览器中生成骨架屏代码了。

    在浏览器里运行

    由于我们的方案出发点是通过单纯的 DOM 操作,遍历页面上的节点,根据制定的规则生成相应区域的颜色块,最终形成页面的骨架屏,所以核心代码完全可以直接跑在浏览器端;

    const createSkeletOnHTML= require('@nutui/draw-page-structure/evalDOM') createSkeletonHTML({ // ... background: 'red', animation: 'opacity 1s linear infinite;' }).then(skeletOnHTML=> { console.log(skeletonHTML) }).catch(e => { console.error(e) }) 

    结合 Puppeteer 自动生成骨架屏

    虽然该方式已经可以生成骨架屏代码了,但是还是不够自动化,为了让生成的骨架屏代码自动加载进指定页面。于是,我们开发了一个配套的 CLI 工具。这个工具通过 Puppeteer 运行页面,并把 evalDOM.js 脚本注入页面自动执行,执行的结果是生成的骨架屏代码被插入到应用页面。

    我们的方案大概思路如下:

    接下来看看如何使用 CLI 工具生成骨架屏,最多只需如下四步:

    1. 全局安装,npm i @nutui/draw-page-structure g
    2. dps init 生成配置文件 dps.config.js
    3. 修改 dps.config.js 进行相关配置
    4. dps start 开始生成骨架屏

    只需简单几步,然而并没有繁琐的配置:

    一般来说,你需要按自己的项目情况来配置 dps.config.js ,常见的配置项有:

    • url: 待生成骨架屏的页面地址
    • output.filepath: 生成的骨架屏节点写入的文件
    • output.injectSelector: 骨架屏节点插入的位置,默认 #app
    • background: 骨架屏主题色
    • animation: css3 动画属性
    • rootNode: 真对某个模块生成骨架屏
    • device: 设备类型,默认 mobile
    • extraHTTPHeaders: 添加请求头
    • init: 开始生成之前的操作
    • includeElement(node, draw): 定制某个节点如何生成
    • writePageStructure(html, filepath): 回调的骨架屏节点

    详细代码及工具的使用请移步 Github

    初步实现的效果:

    • 京东 PLUS 会员正式中首页:

    • 京东 PLUS 会员正式中首页,通过该方案生成的骨架屏效果:

    • 移动端百度首页:

    • 移动端百度首页,通过该方案生成的骨架屏效果:

    总结

    以上就是基于 DOM 的骨架屏自动生成方案,其核心是 evalDOM 函数。这个方案在很多场景下的表现还是令人满意的。不过,网页布局和样式组合的可能性太多,想要在各种场景下都获得理想的效果,还有很长的路要走,但既然已经在路上,就勇敢的向前吧!

    欢迎 star,欢迎提 PR !Github

    10 条回复    2019-07-24 16:18:50 +08:00
    also24
        1
    also24  
       2019-07-20 01:48:42 +08:00
    感谢分享。

    话说百度那张,好像多出了一行什么东西?
    famanoder
        2
    famanoder  
    OP
       2019-07-20 02:05:57 +08:00
    @also24 哦,这一行是百度提示下载 app 的按钮和图标
    linearxian
        3
    linearxian  
       2019-07-20 02:10:04 +08:00 via Android
    感谢分享
    zqx
        4
    zqx  
       2019-07-20 08:06:40 +08:00 via Android
    有个想法感觉可以做成 vscode 插件,然后一键生成骨架屏。就是借助 selenium 类似的库,在内存中运行项目,把每个路由 DOM 渲染的结果帧存为 html,再写一份脚本去自动替换元素属性,这样能生成最接近真实 DOM 的骨架
    love
        5
    love  
       2019-07-20 08:31:13 +08:00
    这个需要这么真实 1:1 吗,抽象点差不多就可以了吧
    ChefIsAwesome
        6
    ChefIsAwesome  
       2019-07-20 09:56:28 +08:00 via Android
    本末倒置的感觉。首要目标是让程序变快,不用加载,或是看不到加载。其次才是处理如果程序很慢,怎么显示 loading,让用户不觉得烦。
    现在这趋势,就跟前两年好多人在那做炫酷下拉刷新动画一样。本来 200ms 加载的东西,为了把炫酷动画展示完,硬要用户看一两秒的动画。
    这种骨架图只有界面完全固定的情况下,效果才会好,实际应用中很少出现这种场景:不同用户权限看到的东西不一样;不同用户设置导致界面不一样;广告动态插入,不可控;视频、图片的比例不固定等等。
    famanoder
        7
    famanoder  
    OP
       2019-07-20 11:05:39 +08:00
    @ChefIsAwesome em...首先,该方案只是针对网页的轮廓生成最多两层纯 DOM 节点,试验过很多个主流网站,生成的骨架屏体积几乎没有超过 30k 的,都非常小,所以对网页本身的加载渲染速度几乎没有拖累;其次,对于不同用户看到的东西不一样,骨架屏确实不好做成动态的,但我们在实际的使用及用户反馈来看,并不影响用户体验;
    famanoder
        8
    famanoder  
    OP
       2019-07-20 11:16:24 +08:00
    @zqx 这个想法非常好呀,这里 puppeteer 做的事情和 selenium 类似,都是借助一个工具来运行指定页面,得到页面的 html,再分析页面结构, 但是我们为了精简 DOM 节点和样式信息,最终选择只画出模块的轮廓,异曲同工,哈哈
    famanoder
        9
    famanoder  
    OP
       2019-07-20 11:19:21 +08:00
    @ChefIsAwesome 最后,对于不同用户看到的场景不一样的话,我们推荐按页面里的模块生成相应的骨架屏,然后在模块内取数据前后控制显示或隐藏;
    buhi
        10
    buhi  
       2019-07-24 16:18:50 +08:00
    为什么不是一张实现准备好的图片呢?
    遍历 DOM 生成色块是不是有点小题大做了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5832 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 01:35 PVG 09:35 LAX 18:35 JFK 21:35
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86