前言
我们都知道,目前传统的 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 产物文件的功能
...
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
prerenderer 承担的则是使用 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
至此,简易自动骨架屏效果的方案已经叙述完成,整个过程,需要我们自己动手的主要是骨架化过程的部分,其余之处,都可通过参考已有过程实现来完成,那么具体过程实现,此处就不再继续展开了,动手能力强的小伙伴,大概可以自己一把梭出来
结尾
预渲染方案待展开的功能还是有不少的,例如
- 如何内联样式?(这条比较容易做到,借助 jsdom 自身的 resourceLoader 足矣)
- 如何保留关键样式,去除无用样式?(有一定难度,可参考 uncss,配合 postcss 实现)
- 预渲染性能是否充足,能否用来做 SSR? (jsdom 渲染速度较快,此处进行了实践 santi)
以下是上述方案的自动骨架插件实现,目前自动骨架化的过程比较简陋,只具备了基础的可用性,也希望能得到大家的帮助,共同完善自动骨架化的过程