- 发布于
 
骨架屏
- Authors
 
- Name
 - 田中原
 
什么是骨架屏
简单的说,骨架屏就是在JS代码解析完成之前,先使用一些图形进行占位,等内容加载完成之后用真实的页面把它替换掉。
如图:

为什么要使用骨架屏?
现在流行的前端框架中(Vue、React、Angular),都有一个共同的特点,就是JS驱动,在JS代码解析完成之前,页面不会展示任何内容,也就是所谓的白屏。骨架屏可以给人一种页面内容已经渲染出一部分的感觉,相较于传统的 loading 效果,在一定程度上可提升用户体验。尤其在网络较慢、图文信息较多、加载数据流较大的情况下。
- PS:解决这种白屏的方式还有
- 预渲染: 启动一个浏览器,生成html,加载这个页面的时候先显示再进行替换。- 缺陷:如果数据非常实时,比如新闻列表,替换页面之前数据可能还是昨天的。比较适合静态页面
 - 服务端渲染(SSR):是通过服务端获取最新数据渲染的,像做一些博客,新闻类的都可以使用服务器端渲染 - 缺陷:- 它会占用很多服务器内存,- 由于服务器端渲染只是个字符串,它不知道DOM什么时候放到页面上。就导致一些浏览器的api无法正常使用了,比如操作DOM的api - 如果接口挂了就悲剧了
 
 
实现骨架屏的几种方案
- 通过设计师给出的骨架屏图片
 - 通过 HTML+CSS 手动编写骨架屏代码
 - 自动生成骨架屏代码
 
自动生成骨架屏的实现思路

compiler.hooks.done.tap(PLUGIN_NAME, async () => {
  await this.startServer() // 启动一个http服务器
  this.skeleton = new Skeleton(this.options)
  await this.skeleton.initialize() // 启动一个无头浏览器
  const skeletonHTML = await this.skeleton.genHTML(this.options.origin) // 生成骨架屏的DOM字符串
  const originPath = resolve(this.options.staticDir, 'index.html') // 打包后文件路径
  const originHTML = await readFileSync(originPath, 'utf8') // 读取打包后文件内容
  const finalHTML = originHTML.replace('<!--shell-->', skeletonHTML) // 把打包后的文件内容替换成生成的骨架屏内容
  await writeFileSync(originPath, finalHTML) // 向打包后的文件写入替换骨架屏后的内容
  await this.skeleton.destroy() // 销毁无头浏览器
  await this.server.close() // 关闭服务
})
启动http服务
async startServer () {
  this.server = new Server(this.options); // 创建服务
  await this.server.listen();             // 启动服务器
}
启动puppeteer
 async initialize () {
   this.brower = await puppeteer.launch({ headless: true });
 }
打开新页面
async newPage () {
  let { device } = this.options;
  let page = await this.brower.newPage();
  // puppeteer.devices[device]: 设备模拟
  await page.emulate(puppeteer.devices[device]);
  return page;
}
注入提取骨架屏的脚本 生成骨架屏代码和对应的样式
async genHTML (url) {
  let page = await this.newPage();
  let response = await page.goto(url, { waitUntil: 'networkidle2' }); // 等待网络加载完成
  // 如果访问不成功 比如断网了啥的
  if (response && !response.ok()) {
    throw new Error(`${response.status} on ${url}`);
  }
  // 创建骨架屏
  await this.makeSkeleton(page);
  const { html, styles } = await page.evaluate((options) => {
    return Skeleton.getHtmlAndStyle(options)
  }, this.options);
  let result = `
    <style>${styles.join('\n')}</style>
    ${html}
  `;
  return result;
}
用生成的骨架屏内容替换dist中的index.html
const skeletonHTML = await this.skeleton.genHTML(this.options.origin) // 生成骨架屏的DOM字符串
const originPath = resolve(this.options.staticDir, 'index.html') // 打包后文件路径
const originHTML = await readFileSync(originPath, 'utf8') // 读取打包后文件内容
const finalHTML = originHTML.replace('<!--shell-->', skeletonHTML) // 把打包后的文件内容替换成生成的骨架屏内容
await writeFileSync(originPath, finalHTML) // 向打包后的文件写入替换骨架屏后的内容
关闭无头浏览器和服务
await this.skeleton.destroy() // 销毁无头浏览器
await this.server.close() // 关闭服务