17

一个简易的预渲染自动骨架屏方案

 3 years ago
source link: https://zhuanlan.zhihu.com/p/166009071
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一个简易的预渲染自动骨架屏方案

我们都知道,目前传统的 SPA 网页在完成脚本加载后,通常还需要进行接口请求,拿到远端数据后才能进行完整地内容呈现

而在接口请求的过程中,为了过渡无数据的空白场景,并提示用户“数据请求中”,常用的方法为做一个 loading 动画效果

v2-f476cacc91c9415aeea9eb1080cda17c_b.jpg

而在用户胃口越来越刁的今天,一个简单的 loading 效果已经不太能安抚用户了,而骨架屏就是一种安抚用户的进阶方案

最终成品链接(懒人用):auto-skeleton-plugin

什么是骨架屏?

简单来说,骨架屏就是在还未产生可阅读内容时,先将网页的大致结构框架呈现给用户,以达到安抚用户等待过程中的不耐烦心理、提升用户存留的效果

v2-b0d00126466730f1516a497af46d85c6_b.jpg

骨架屏的实现,通常有两种方式

  1. 手动书写骨架
  2. 自动生成骨架

手动写骨架的方式,好处是可以做出高定制性的骨架效果,缺点是开发成本大,效率低,但本文不对此方式进行展开

那么如何实现自动骨架屏的效果呢?一个简单的方式是:将已有内容的样式进行调整,生成对应的骨架效果,例如以下代码,可以将所有文字内容,变成骨架条块

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;
      `
    })
}
v2-4baef38f4779f0b0ee4ab302f1194127_b.jpg

这样,只要我们完善不同内容如图片、图标等元素的骨架化过程,就可以得到一个相对可用的内容骨架化效果

自动骨架化的好处是,生成骨架的效率高,开发成本很低,但缺点是定制性相对较差,需要根据已有内容来确定骨架效果

但这有一个问题,我们期望是在应用刚打开时,还未请求数据前就呈现骨架,目前显然是做不到的

而我们可以借助“预渲染”来实现期望的效果

什么是预渲染?

预渲染类似服务端渲染,它的过程大概是这样的:在应用完成打包后,立刻启动一个 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

prerenderer 承担的则是使用 headless 浏览器访问网页,并输出访问结果的功能,其官方内置了两种可选的 headless 浏览器:puppeteerjsdom

由于 puppeteer 需要下载的内容较大,我们考虑使用较轻量的 jsdom 来完成这个效果

在翻阅了部分 renderer-jsdom 的源码后,可以找到 headless 浏览器采集网页内容的部分

关键源码:https://github.com/JoshTheDerf/prerenderer/blob/master/renderers/renderer-jsdom/es6/renderer.js#L25-L38

我们只需要在采集网页内容前,对内容进行骨架化,就可以得到期望的效果

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

至此,简易自动骨架屏效果的方案已经叙述完成,整个过程,需要我们自己动手的主要是骨架化过程的部分,其余之处,都可通过参考已有过程实现来完成,那么具体过程实现,此处就不再继续展开了,动手能力强的小伙伴,大概可以自己一把梭出来

预渲染方案待展开的功能还是有不少的,例如

  1. 如何内联样式?(这条比较容易做到,借助 jsdom 自身的 resourceLoader 足矣)
  2. 如何保留关键样式,去除无用样式?(有一定难度,可参考 uncss,配合 postcss 实现)
  3. 预渲染性能是否充足,能否用来做 SSR? (jsdom 渲染速度较快,此处进行了实践 santi

以下是上述方案的自动骨架插件实现,目前自动骨架化的过程比较简陋,只具备了基础的可用性,也希望能得到大家的帮助,共同完善自动骨架化的过程


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK