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

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

前言

我们都知道,目前传统的 SPA 网页在完成脚本加载后,通常还需要进行接口请求,拿到远端数据后才能进行完整地内容呈现 而在接口请求的过程中,为了过渡无数据的空白场景,并提示用户“数据请求中”,常用的方法为做一个 loading 动画效果1.gif

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

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


什么是骨架屏?

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

1.gif

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

  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;
      `
    })
}

1.gif

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

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

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

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



什么是预渲染?

预渲染类似服务端渲染,它的过程大概是这样的:在应用完成打包后,立刻启动一个 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 浏览器采集网页内容的部分


关键源码: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

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



在 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都没有下载完成。在没有其他优化手段的前提下,用这种方式来优化用户体验,也是一个非常好的操作。

1.gif


结尾

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

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


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

auto-skeleton-plugin

目录
相关文章
|
2月前
|
UED
鸿蒙next版开发:相机开发-适配不同折叠状态的摄像头变更(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了强大的相机开发能力,特别是针对折叠屏设备的摄像头适配。本文详细介绍了如何在ArkTS中检测和适配不同折叠状态下的摄像头变更,确保相机应用在不同设备状态下的稳定性和用户体验。通过代码示例展示了具体的实现步骤。
105 8
|
8月前
自适应日落动态卡通动画404页面模板
自适应日落动态卡通动画404页面模板
39 4
自适应日落动态卡通动画404页面模板
|
UED
解决Electron窗口白屏问题的预创建方案
在使用Electron创建窗口时,有时会遇到窗口显示白屏的问题。这篇文章将介绍一种解决方案,即预创建窗口,并提供了针对窗口关闭和应用退出的管理方法,以确保 Electron 应用的顺畅运行和用户体验
777 0
|
小程序 开发者
wepy框架-触摸内容滑动组件使用步骤
wepy框架-触摸内容滑动组件使用步骤
54 0
|
编解码 JavaScript
【项目经验】:vue的PC端项目中通过vw做页面自适应,改变屏幕分辨率后页面混乱
vue的PC端项目中通过vw做页面自适应,改变屏幕分辨率后页面混乱如何处理
333 1
|
前端开发 JavaScript 异构计算
页面渲染合成(补充)
在上一篇文章老生常谈之从输入URL到页面呈现的过程中描述了页面渲染流程,其中涉及页面的布局(Layout)和绘制(Painting),实际在绘制之后还有一个步骤叫做合成(Compositing)。
|
前端开发 JavaScript Go
浏览器原理 22 # 渲染流水线:CSS如何影响首次加载时的白屏时间?
浏览器原理 22 # 渲染流水线:CSS如何影响首次加载时的白屏时间?
135 0
浏览器原理 22 # 渲染流水线:CSS如何影响首次加载时的白屏时间?
An动画基础之按钮动画与基础代码相结合
An动画基础之按钮动画与基础代码相结合
797 0
|
存储 运维 并行计算
实时渲染和预渲染有什么区别
实时渲染用于交互式渲染场景,如在3D电脑游戏中,通常每帧必须在几毫秒内渲染。它的意思是计算机在计算屏幕的同时输出和显示屏幕。典型代表是Unreal和Unity。像《黑色神话:悟空》这样的游戏便是使用虚幻引擎4创造出来的。实时绘制的特点是可以实时控制,交互非常方便。
|
异构计算
案例分享:Qt流水线图像显示控件(列刷新、1ms一次、缩放、拽拖、拽拖预览、性能优化、支持OpenGL GPU加速)
案例分享:Qt流水线图像显示控件(列刷新、1ms一次、缩放、拽拖、拽拖预览、性能优化、支持OpenGL GPU加速)