前言
我们都知道,目前传统的 SPA 网页在完成脚本加载后,通常还需要进行接口请求,拿到远端数据后才能进行完整地内容呈现 而在接口请求的过程中,为了过渡无数据的空白场景,并提示用户“数据请求中”,常用的方法为做一个 loading 动画效果
而在用户胃口越来越刁的今天,一个简单的 loading 效果已经不太能安抚用户了,而骨架屏就是一种安抚用户的进阶方案
最终成品链接(懒人用):auto-skeleton-plugin
什么是骨架屏?
简单来说,骨架屏就是在还未产生可阅读内容时,先将网页的大致结构框架呈现给用户,以达到安抚用户等待过程中的不耐烦心理、提升用户存留的效果
骨架屏的实现,通常有两种方式
- 手动书写骨架
- 自动生成骨架
手动写骨架的方式,好处是可以做出高定制性的骨架效果,缺点是开发成本大,效率低,但本文不对此方式进行展开
那么如何实现自动骨架屏的效果呢?一个简单的方式是:将已有内容的样式进行调整,生成对应的骨架效果,例如以下代码,可以将所有文字内容,变成骨架条块
function generateSkeleton() { // 文字节点 ;[...document.querySelectorAll('*')] .filter( (node) => !['script', 'style', 'html', 'body', 'head', 'title'].includes( node.tagName.toLowerCase() ) ) .map((node) => [...node.childNodes].filter((node) => node instanceof Text)) .flat(Infinity) .forEach((node) => { let span = document.createElement('span') node.parentNode.insertBefore(span, node) span.appendChild(node) span.style = ` background: #f2f2f2; color: transparent !important; ` }) }
这样,只要我们完善不同内容如图片、图标等元素的骨架化过程,就可以得到一个相对可用的内容骨架化效果了
自动骨架化的好处是,生成骨架的效率高,开发成本很低,但缺点是定制性相对较差,需要根据已有内容来确定骨架效果
但这有一个问题,我们期望是在应用刚打开时,还未请求数据前就呈现骨架,目前显然是做不到的
而我们可以借助“预渲染”来实现期望的效果
什么是预渲染?
预渲染类似服务端渲染,它的过程大概是这样的:在应用完成打包后,立刻启动一个 headless 浏览器进行页面访问,再将访问的结果输出成 html 文件的渲染过程
通俗地说就是:打包完后本地先访问看一看,看到啥就“截个屏”存起来,然后输出一个 html 文件,覆盖原本构建生成的 index.html 这样,用户访问打包好的 index.html 时,看到的就是一个有内容的网页
那么,借助预渲染,我们可以将上述自动骨架屏的过程,放在 headless 浏览器加载出网页内容后,具备内容后再将内容骨架化,再输出成 html,就可以实现用户访问时,还未请求数据前,先呈现骨架的效果
自动骨架屏的过程实现
我们可以参考一个常用的预渲染的 webpack 插件 prerender-spa-plugin 来实现这个过程
查阅源码可知,这个插件并未实现核心渲染过程,其实只是将 prerenderer 包装成了 webpack 插件的形式,并承担了将最终结果输出成 html 产物文件的功能
关键源码:https://github.com/chrisvfritz/prerender-spa-plugin/blob/master/es6/index.js#L65-L70
... const Prerenderer = require('@prerenderer/prerenderer') ... function PrerenderSPAPlugin (...args) { ... const afterEmit = (compilation, done) => { const PrerendererInstance = new Prerenderer(this._options) PrerendererInstance.initialize() .then(() => { return PrerendererInstance.renderRoutes(this._options.routes || []) }) ... } ... } ... module.exports = PrerenderSPAPlugin
rerenderer 承担的则是使用 headless 浏览器访问网页,并输出访问结果的功能,其官方内置了两种可选的 headless 浏览器:puppeteer 和 jsdom
由于 puppeteer 需要下载的内容较大,我们考虑使用较轻量的 jsdom 来完成这个效果
在翻阅了部分 renderer-jsdom 的源码后,可以找到 headless 浏览器采集网页内容的部分
我们只需要在采集网页内容前,对内容进行骨架化,就可以得到期望的效果
const JSDOM = require('jsdom/lib/old-api.js').jsdom ... const getPageContents = function (window, options, originalRoute) { ... return new Promise((resolve, reject) => { ... function captureDocument () { // 此处可在输出 html 结果前,先对网页内容进行骨架化 // generateSkeleton 就是上边咱们整理出来的 dom 操作实现自动骨架化过程 generateSkeleton(window) const result = { ... html: serializeDocument(window.document) } ... return result } ... } ... } class JSDOMRenderer { ... async renderRoutes (routes, Prerenderer) { ... const results = Promise.all(routes.map(route => limiter(() => { return new Promise((resolve, reject) => { JSDOM.env({ url: `http://127.0.0.1:${rootOptions.server.port}${route}`, ... }) }) .then(window => { return getPageContents(window, this._rendererOptions, route) }) }))) ... return results } ... } module.exports = JSDOMRenderer
至此,简易自动骨架屏效果的方案已经叙述完成,整个过程,需要我们自己动手的主要是骨架化过程的部分,其余之处,都可通过参考已有过程实现来完成,那么具体过程实现,此处就不再继续展开了,动手能力强的小伙伴,大概可以自己一把梭出来
在 umi 中使用
简单的用法
在 config/config 中配置
chainWebpack(async (config) => { config .plugin('auto-skeleton-plugin') .use(AutoSkeletonPlugin, [{ staticDir: 'xx/demo/dist/', routes: ['/'], }]) return config; });
这里需要注意的是,staticDir 需要写 build 之后的真实路径,并且需要是绝对路径,这在 umi 项目中和你的 outputPath 配置有关。
编写插件使用
在插件中使用 api.chainWebpack 来实现,写法基本上和上面一致。好处是可以获取真实的 outputPath ,因为它可能被配置修改,也可能被其他插件修改。
api.chainWebpack(async (config) => { const { exportStatic } = api.config; const routes = await api.getRoutes() config .plugin('auto-skeleton-plugin') .use(AutoSkeletonPlugin, [{ staticDir: api?.paths?.absOutputPath!, routes: exportStatic ? getRotesPath(routes) : ['/'], }]) return config; });
我觉得这个插件最大的优势是在移动端应用中,一来现在的网络情况,pc页面已经较难看到白屏页面了。而在移动端应用中,首页白屏时间,依旧是困扰用户和开发的一大问题。如果用上骨架屏,在用户见到骨架屏的时候,从感官上项目已经启动了。但是对于程序来说,可能项目仅仅是进入页面,可能js都没有下载完成。在没有其他优化手段的前提下,用这种方式来优化用户体验,也是一个非常好的操作。
结尾
预渲染方案待展开的功能还是有不少的,例如
- 如何内联样式?(这条比较容易做到,借助 jsdom 自身的 resourceLoader 足矣)
- 如何保留关键样式,去除无用样式?(有一定难度,可参考 uncss,配合 postcss 实现)
- 预渲染性能是否充足,能否用来做 SSR? (jsdom 渲染速度较快,此处进行了实践 santi)
以下是上述方案的自动骨架插件实现,目前自动骨架化的过程比较简陋,只具备了基础的可用性,也希望能得到大家的帮助,共同完善自动骨架化的过程
auto-skeleton-plugin