来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

一种对开发更友好的前端骨架屏自动生成方案

时间: 2019-09-06阅读: 168标签: 骨架屏

时间: 2019-09-15阅读: 235标签: 优化前言

一份来自 Akamai 的研究报告显示,在对 1048 名网购户进行采访后发现:

自从前端三大框架React、Vue、Angular面世以来,前端开发逐渐趋向规范化、统一化,大多数时候新建前端项目,首先想到使用的技术一定是三大框架之一,框架给前端开发带来了极大的便利和规范,但是由于这三大框架都是JS驱动,在JS没有解析加载完成之前页面无法展示,会处于长时间的白屏,带来了一定的用户体验问题,接下来本篇文章会介绍本人最近在白屏优化时遇到的一些问题和思考

约 47% 的用户期望他们的页面在两秒之内加载完成。如果页面加载时间超过 3s,约 40% 的用户会选择离开或关闭页面。

SSR

一直以来,为了提升用户在页面加载时的体验,无论是 Web 还是 iOS、Android 的应用中,前端开发工程师都做了许多工作。除了解决如何让网页展现速度更快的问题,还有很重要的一点就是提升用户对加载等待时间的感知。「菊花图」以及由其衍生出的各种加载动画就是一类常见的解决方案,相信无论是开发者还是用户对下面这个图标都不会陌生:

想到白屏问题,首先想到的解决方案一般都是服务端渲染,在服务端将渲染逻辑处理好,然后将处理好的HTML直接返回给前端展示,这样就可以解决白屏的问题,也可以解决seo的问题,因为不需要动态获取数据了,但是,这和我早期的写后端时的开发模式很像,前端和后端关联在了一起,不利于维护,同时,对于前端工程师来说,要求变高来,需要了解一定的后端知识,虽然有类似Nuxt.js这类的SSR框架帮我们简化了服务端的部分,但是在要做定制或是解决bug时还是无法避免要对服务端部分进行调试、维护,成本颇高,还有需要考虑的服务端渲染会增加服务器压力,要处理并发、运行速度问题等等

本文要介绍的「骨架屏」则被视为菊花图升级版的方案。受现有骨架屏方案的启发,马蜂窝电商前端研发团队实现了一种自动化生成骨架屏的方法,并在马蜂窝商城的多个页面中实现应用,取得了不错的效果。

预渲染

一、什么是骨架屏

这个方案是相对简单直接的一个解决办法,尝试成本也比较低,这里介绍如何用prerender-spa-plugin做预渲染,这样就可以在浏览器进行渲染,而不需要将Vue或者React代码部署到服务器上,以vue-cli3的官方demo为例做配置,看具体的配置文件:

骨架屏可以理解为在页面数据尚未返回或页面未完成完全渲染前,先给用户呈现一个由灰白块组成的当前页面大致结构,让用户产生页面正在逐渐渲染的感受,从而使加载过程从视觉上变得流畅。生成后的骨架屏页面如下图所示:

const path = require('path')const PrerenderSPAPlugin = require('prerender-spa-plugin')const Renderer = PrerenderSPAPlugin.PuppeteerRenderermodule.exports = { configureWebpack: config = { let plugins = [] plugins.push(new PrerenderSPAPlugin({ staticDir: path.resolve(__dirname, 'dist'), routes: ['/', '/about'], minify: { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, keepClosingSlash: true, sortAttributes: true }, renderer: new Renderer({ renderAfterDocumentEvent: 'custom-render-trigger' }) })) config.plugins = [ ...config.plugins, ...plugins ] }}

骨架屏的主要优势为:

上面代码是常用prerender-spa-plugin的配置,staticDir预渲染输出的文件地址,routes要做预渲染的路由,minify压缩相关的配置,renderer渲染引擎相关的配置,可以传入自定以的渲染引擎或者直接使用默认的PuppeteerRenderer,renderAfterDocumentEvent是渲染引擎配置中的一个属性,指当某个事件触发时才执行预渲染,这很重要,尤其是对一些特定场景的下的需求,当然简单场景下完全可以不配置renderer渲染引擎选项,直接用默认选项。接下来执行编译,看看会发生什么?dist目录下会生成路由对应的文件夹,打开index.html

网上澳门金莎娱乐,用户避免看到长时间的白页可以获知页面的大体结构,减小用户认为页面出错而离开的机率与菊花图相比视觉更加流畅二、常见的前端骨架屏方案

 div  div  a href="/" Home/a | a href="/about" About/a /div div  img alt="Vue logo" src="/img/logo.82b9c7a5.png" / div data-v-7b2de9b7="" h1 data-v-7b2de9b7=""Welcome to Your Vue.js App/h1 p data-v-7b2de9b7=""For a guide and recipes on how to configure / customize this project,br data-v-7b2de9b7="" /check out the a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"vue-cli documentation/a./p h3 data-v-7b2de9b7=""Installed CLI Plugins/h3 ul data-v-7b2de9b7="" li data-v-7b2de9b7=""a href="-cli/tree/dev/packages/%40vue/cli-plugin-babel" data-v-7b2de9b7="" rel="noopener" target="_blank"babel/a/li li data-v-7b2de9b7=""a href="-cli/tree/dev/packages/%40vue/cli-plugin-eslint" data-v-7b2de9b7="" rel="noopener" target="_blank"eslint/a/li li data-v-7b2de9b7=""a href="-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" data-v-7b2de9b7="" rel="noopener" target="_blank"unit-jest/a/li /ul h3 data-v-7b2de9b7=""Essential Links/h3 ul data-v-7b2de9b7="" li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"Core Docs/a/li li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"Forum/a/li li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"Community Chat/a/li li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"Twitter/a/li li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"News/a/li /ul h3 data-v-7b2de9b7=""Ecosystem/h3 ul data-v-7b2de9b7="" li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"vue-router/a/li li data-v-7b2de9b7=""a href="" data-v-7b2de9b7="" rel="noopener" target="_blank"vuex/a/li li data-v-7b2de9b7=""a href="-devtools#vue-devtools" data-v-7b2de9b7="" rel="noopener" target="_blank"vue-devtools/a/li li data-v-7b2de9b7=""a href="-loader.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank"vue-loader/a/li li data-v-7b2de9b7=""a href="-vue" data-v-7b2de9b7="" rel="noopener" target="_blank"awesome-vue/a/li /ul /div /div /div

在选择骨架屏之前,我们也考虑了一些其他的方法,比如能否通过服务端渲染(SSR)的方式来避开前端白屏时间的问题。但发现需要涉及项目过多,还会涉及服务的构建与部署;或是通过 prerender-spa-plugin 提供简单的预呈现,它对 SPA 支持友好,但需要额外的 webpack 配置,且因为包源的问题,下载时间过长,有时还会莫名失败,等等,都因为种种原因最终放弃。

为了方便,这里只贴了app节点里的代码,以往在没有使用预渲染插件时app节点里面是空的没有内容,从加载index.html文件开始到js文件解析完成之前,由于app节点里面是空的,因此页面会处于白屏状态,但是预渲染插件在编译阶段就将对应的路由编译好插入到app节点,这样就能在js文件解析过程中有内容展示,js解析完成后,Vue会将app节点内的内容替换成Vue渲染好的内容,来看看chrome调试下渲染有什么区别:常规渲染:预渲染:利用chrome浏览器的加载截屏功能可以看出常规渲染时会有明显的白屏时间,而预渲染则不会产生白屏,那么预渲染有什么缺点呢?

经过一系列调研后,我们对业界常见的几种骨架屏解决方案,以及它们的优势、不足进行了一个简单的梳理。

动态数据无法展示,不同的用户看到的都是同样的页面路由很多时,代码构建时间太长用户容易误操作,由于预渲染时js还没有加载,因此展示出来的内容没有js的交互逻辑,比如按钮点击,在js加载完成之前用户点击是没有反应的预加载内容很少,当页面有内容是依赖动态数据加载时,在编译时是无法加载出动态数据的,因此会导致这部分内容编译不出来骨架屏

  1. UI 骨架屏图

骨架屏的实现原理和预加载类似,都是利用了Puppeteer爬取页面的功能,Puppeteer是chrome出的一个headlessChromenode库,提供了API可以抓取SPA并生成预渲染内容,和预加载不太一样的是骨架屏技术会在Puppeteer生成内容后,利用算法将生成的内容进行替换,生成骨架页面,page-skeleton-webpack-plugin是一个用来生成骨架屏的webpack插件,接下来就来看看怎么使用,还是以vue-cli3生成的官方项目为例:

即通过 UI 提供符合页面首页样式的图来充当骨架屏,将骨架屏 base64 图片插入 root 根节点,在 webpack 打包时嵌入项目中。

div!-- shell --/div

const SkeletonPlugin = require('page-skeleton-webpack-plugin').SkeletonPluginconst path = require('path')module.exports = { publicPath: '/', outputDir: 'dist', configureWebpack: config = { let plugins = [] plugins.push(new SkeletonPlugin({ pathname: path.resolve(__dirname, './shell'), // pathname为来存储 shell 文件的地址 staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同 routes: ['/', '/about'], // 将需要生成骨架屏的路由添加到数组中 port: '7890' })) config.plugins = [ ...config.plugins, ...plugins ] }, chainWebpack: config = { if (process.env.NODE_ENV === 'production') { config.plugin('html').tap(opts = { console.log(opts[0]) opts[0].minify.removeComments = false return opts }) } }}

这是一种简单粗暴的方法,实现起来比较容易。但缺点也很明显,就是需要 UI 设计师支持和开发介入,不能自动生成。

上面例子是对page-skeleton-webpack-plugin的简单配置,想要完整的配置可以自行前往github获取,需要注意的是这段代码:

  1. 手写骨架屏
chainWebpack: config = { if (process.env.NODE_ENV === 'production') { config.plugin('html').tap(opts = { console.log(opts[0]) opts[0].minify.removeComments = false return opts }) } }

即通过手写 HTML、CSS 的方式为目标页定制骨架屏。这种方式可以做到对页面真实样式的复刻。不过一旦由于各种原因导致页面样式发生改变,就需要再改一遍骨架屏的样式和布局,极大增加了维护的成本。

这是修改了vue-cli3中集成的html-webpack-plugin的压缩配置,将移除注释去掉了,因为page-skeleton-webpack-plugin在编译时,注入代码依赖!-- shell --注释,而vue-cli3中集成的html-webpack-plugin会在编译做压缩,将注释去掉,因此要单独配置一下,否则会在编译时导致生成app节点下没有内容。还有一个在使用时需要注意的点,如果你是vue-cli3脚手架生成的代码,运行时可能会报这样的错误:如果遇到这个错误,怎么解决呢?github上已经有对应的解决办法了,问题都说完了接下来看看怎么使用,运行项目后,在chrome调试器里执行toggleBar

3.自动生成静态骨架屏

会在页面里显示一个Preview skeleton page按钮,点击后会生成一个新窗口,这个窗口显示了当前页面的骨架屏样式和代码,可以修改骨架屏样式,然后点击右上角保存,会将对应路由的骨架屏保存到pathname对应的文件夹下然后执行编译,编译后会在staticDir中生成路由对应的html,这些html中的app节点下都被插入了路由对应骨架屏代码,然后在staticDir下启动服务访问,就能看到骨架屏的效果:从加载过程中可以看到骨架屏的加载

目前比较受关注的是饿了么开源的插件 page-skeleton-webpack-plugin,其具体实现原理为:

原文:

生成骨架屏

通过 Puppeteer 操控 handless Chrome 打开需要生成的骨架屏页面,在等待页面加载完成之后,保留页面布局样式的前提下,通过对页面中元素进行增删,对已有元素通过层叠样式进行覆盖,使其展示为灰白块。然后将修改后的 HTML 和 CSS 提取出来,将页面分为不同的块区域,例如文本块、图片块、按钮块、SVG、伪类元素块等,分别对每个块进行处理,使其尽量与原页面保持一致。这里用到了 Puppeteer page 实例的 addScriptTag 方法来将处理块的脚本插入到 headless Chrome 打开的页面当中。

实际生成的骨架屏页面与原页面可能还会存在差距,插件通过 memory-fs 将骨架屏写入内存中,可以通过预览页面对生成的骨架屏进行二次编辑和效果预览,修改完成后点击生成按钮就能生成一份新的骨架屏写入到项目中。

借一张图来说明:

插入骨架屏

骨架屏的 DOM 结构和 CSS 通过离线生成后,在构建时注入模板 (EJS) 中的节点下面,插入到 HTML 是在 after-emit 钩子函数中进行。

page-skeleton-webpack-plguin 生成骨架屏的方案可以根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中。

它的不足之处在于:

实际使用过程中无法监听接口返回导致生成骨架屏的时机是否准确生成的页面与业务人员写的结构质量有直接关系,经常出现需要手工二次调整的情况

在这样的背景下,马蜂窝电商研发前端团队希望找一种在提升用户体验的同时,对开发更友好的骨架屏生成方式,能针对不同的业务场景自动生成出相似的骨架屏,并且实现自动注入。对于开发而言,只需要执行一条命令,或者简单配置,就可以生成骨架屏,不需要再考虑后续的维护工作。

在方案调研过程中,draw-page-structure为我们的设计提供了灵感。

4.draw-page-structure生成骨架屏:

//dps.config.js{url:'',output:{filepath:'/Users/famanoder/DrawPageStructure/example/index.html',injectSelector:'#app'},background:'#eee',animation:'opacity1slinearinfinite;',//...}

根据 URL 指定的线上地址,配合 Puppeteer 获取当前页面的 DOM 结构,并对其中元素节点生成骨架屏文件到 filepath 指定的文件里面,就可以生成骨架屏页面,结果如下图所示:

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:一种对开发更友好的前端骨架屏自动生成方案

关键词: