首页白屏的引起的思考(一)

最近在作项目的优化,除了总体的架构更改,咱们发如今每次加载的时候,首页白屏的问题十分明显。css

为何会出现白屏

如今的前端框架, ReactVueAngular 三大巨头已经占据了主导地位,市面上大多数前端应用也都是基于这三个框架或库完成,这三个框架有一个共同的特色,都是 JS 驱动,在 JS 代码解析完成以前,页面不会展现任何内容,也就是所谓的白屏。html

用户是极其不喜欢看到白屏的,什么都没有展现,用户颇有可能怀疑网络或者应用出了什么问题。 拿 Vue 来讲,在应用启动时,Vue 会对组件中的 data 和 computed中状态值经过 Object.defineProperty 方法转化成 set、get 访问属性,以便对数据变化进行监听。 而这一过程都是在启动应用时完成的,这也势必致使页面启动阶段比非 JS 驱动(好比 jQuery 应用)的页面要慢一些。前端

因此咱们首页就是一个典型的案例,在一次咱们前端周会上,咱们老大问了咱们一个问题,如何给用户啊一个更好的体验。node

SSR

这时候我第一个反应是,尽可能不出现白屏的话,能够用VueSSR,就是服务端直出页面。webpack

首先咱们了解到,服务端渲染主要有两个目的,一是 SEO,二是加快内容展示。 在带来这两个好处的同时,咱们也须要评估服务端渲染的成本,首先咱们须要服务端的支持,所以涉及到了服务构建、部署等,同时 web 项目是一个流量较大的网站,也须要考虑服务器的负载,以及相应的缓存策略,特别像咱们行业,因为地理位置的不一样,不一样用户看到的页面也是不同的,也就是所谓的千人千面,这也为缓存形成了必定困难。git

预渲染

所谓预渲染,就是在项目的构建过程当中,经过一些渲染机制,好比 puppeteer或则 jsdom 将页面在构建的过程当中就渲染好,而后插入到 html 中,这样在页面启动以前首先看到的就是预渲染的页面了。github

可是该方案最终也抛弃了,预渲染渲染的页面数据是在构建过程当中就已经打包到了 html 中, 当真实访问页面的时候,真实数据可能已经和预渲染的数据有了很大的出入,并且预渲染的页面也是一个不可交互的页面,在页面没有启动以前,用户没法和预渲染的页面进行任何交互,预渲染页面中的数据反而会影响到用户获取真实的信息,当涉及到一些价格、金额、地理位置的地方甚至会致使用户作出一些错误的决定。web

骨架图

骨架页面(Skeleton Page)指的是当你打开一个移动端 web 页面,在页面解析和数据加载以前,首先给用户展现页面的大概样式。在骨架页面中,图片、文字、图标都将经过灰色矩形块或圆形块来展现,在真实页面展现以前,用户可以感知到即将加载页面的基本 CSS 样式和页面布局。数组

骨架屏初体验

一开始在我脑子里,觉得骨架屏是一个页面去手写一个css和Html,或者说是让ui去设计一个骨架图。可是这样是有缺点的,好比产品改需求了呢,不只要去修改代码,还要去从新修改骨架页面或者骨架图?浏览器

后来,看到了饿了么大神的文章和很成熟的产品page-skeleton-webpack-plugin ,瞬间明白了我和大佬的区别

WechatIMG2.jpeg-57.3kB

解析-饿了么骨架屏

生成骨架页面的基本方案

经过 puppeteer 在服务端操控 headless Chrome 打开开发中的须要生成骨架页面的页面,在等待页面加载渲染完成以后,在保留页面布局样式的前提下,经过对页面中元素进行删减或增添,对已有元素经过层叠样式进行覆盖,这样达到在不改变页面布局下,隐藏图片、文字和图片的展示,经过样式覆盖,使得其展现为灰色块。而后将修改后的 HTML 和 CSS 样式提取出来,这样就是骨架页面了。

在阐述具体生成骨架页面以前,先了解下 puppeteer, GitHub 上是这样介绍的。

v2-924f28bd0281fb8e45b19f1364cfaf8e_hd.jpg-19.2kB

Puppeteer 是一个 Node 库,它提供了一个高级 API 来经过 DevTools 协议控制 Chromium 或 Chrome。

Puppeteer API 是分层次的,反映了浏览器结构。说实话,这是我第一次接触这个 Node 库,刚上手安装的时候就遇到了很多坑,哈哈哈哈哈哈,尴尬。

有想了解的同窗请看puppetter安装就踩坑-解决篇

骨架屏的开发基本上就是基于这个node库开始的。接下来咱们来解析一下基础代码

skeleton.js

const puppeteer = require('puppeteer')
const devices = require('puppeteer/DeviceDescriptors') //puppeteer 提供了一些设备的参数选项
const { sleep , genScriptContent } = require('./util/utils') //公共工具方法
const scriptFns = require('./util/browserUtils')

const skeleton = async function(url, option = {}) {

  const defaultOption = {
    device: 'iPhone 6'
  }

  const { 
    device, 
    defer = 0, //延迟的时间
    remove = [], //页面想要移除的class类名数组
    excludes = [], //页面想要不包括的class类名数组
    hide= [],//页面想要隐藏的class类名数组
    launch: launchOpt
 } = Object.assign({}, defaultOption, option)

  // 当 Puppeteer 链接到一个 Chromium 实例的时候会经过 puppeteer.launch 或 puppeteer.connect 建立一个 Browser 对象。
  // 返回一个新的 [Page] 对象。[Page] 在一个默认的浏览器上下文中被建立。
  const browser = await puppeteer.launch(launchOpt) 

  const page = await browser.newPage() //新建一个页面

  /**
   * 根据指定的参数和 user agent 生成模拟器。此方法是和下面两个方法效果相同
   * @param { options }
   * viewport <[Object]>
        width <[number]> 页面的宽度,单位像素.
        height <[number]> 页面的高度,单位像素.
        deviceScaleFactor <[number]> 定义设备缩放, (相似于 dpr). 默认 1。
        isMobile <[boolean]> 要不要包含meta viewport 标签. 默认 false。
        hasTouch<[boolean]> 指定终端是否支持触摸。默认 false
        isLandscape <[boolean]> 指定终端是否是 landscape 模式. 默认 false。
      userAgent <[string]>
   * 
   */
  await page.emulate(devices[device])
  
  await page.goto(url)

  // 将一些 utils 插入到打开的页面执行环境中,这里会引入如何判断图片,文字的方法,将他们覆盖成灰色,也是骨架图中必不可缺的代买
  await page.addScriptTag({
    content: genScriptContent(...scriptFns)
  })

/**
还应注意一点,defer 配置,用于告诉 Puppeteer 打开页面后需等待的时间,这是由于,在打开开发中页面后,页面中有些内容还未真正加载完成,若是在这以前进行骨架页面生成,颇有可能致使最终生成的骨架页面和真实页面不符。使得生成骨架页面失败。
**/
  await sleep(defer)
/**
 * page.evaluate(pageFunction, ...args)
 * pageFunction <[function]|[string]> 要在页面实例上下文中执行的方法
    ...args <...[Serializable]|[JSHandle]> 要传给 pageFunction 的参数
    返回: <[Promise]<[Serializable]>> pageFunction执行的结果
 */
  const html = await page.evaluate(async ( remove, excludes, hide ) => { 
    const $ = document.querySelectorAll.bind(document)

    if (remove.length) { 
      const removeEle = $(remove.join(','))
      Array.from(removeEle).forEach(ele => ele.parentNode.removeChild(ele))
    }

    if (hide.length) {
      const hideEle = $(hide.join(','))
      Array.from(hideEle).forEach(ele => ele.style.opacity = 0)
    }

    const excludesEle = excludes.length ? Array.from($(excludes.join(','))) : []

    await traverse(document.documentElement, excludesEle)

    return document.documentElement.outerHTML

  }, remove, excludes,hide)

  // browser.close()

  return { html }
}

module.exports = skeleton
复制代码

下一篇,咱们会认真的去分析,饿了么骨架屏中是如何去将页面根据不一样元素分红不一样的块:文本、图片块,SVG块,伪元素块、按钮块 将元素区分为不一样块后,下一步就是对这些块分别进行处理,包括元素的增减和样式的覆盖,目的只有一个,就是将这些块转化为骨架页面的样式。


资源连接 饿了么大佬-Ran Luo 一种自动化生成骨架屏的方案

相关文章
相关标签/搜索