一、简介
1.1 骨架屏
SPA
应用,由于初始的html
内部只有一个空的div
,需要等待 js
、css
加载编译完成后才能展示首页,因此有一个白屏时间
。
如果没有对各个页面的 js
、css
进行异步加载等处理,白屏时间会更长。
骨架屏就是在页面内容未加载完成的时候,先使用一些图形进行占位,待内容加载完成之后再把它替换掉的一种方案。
二、实现
2.1 手动生成骨架屏
方案特点如下:
- 这个方案的骨架屏是通过手动生成的(自己写骨架屏页面)
- 支持 history/hash router
- 支持单页/多页应用
使用到的包:
npm i -D webpack-node-externals vue-skeleton-webpack-plugin
基本原理:
- 将骨架屏页面处理成
JSON
文件 - 使用
SSR
将JSON
文件处理成html
字符串 - 将
html
字符串插入html
中对应的div
内容中 - 第一次访问页面时,
JS
、CSS
未加载,显示包含html
字符串的骨架屏内容 - 当
SPA
的JS
、CSS
加载完成,替换div
内部内容后,显示真正首页内容
文件描述如下:
- SkeletonBox.vue 骨架屏样式以及各个页面的骨架
- skeleton-entry.js 骨架屏入口文件
- webpack.skeleton.conf.js 骨架屏 webpack 配置
- skeleton-webpack-plugin-config.js 骨架屏其他参数,包括路由和骨架屏页面的对应关系配置
// SkeletonBox.vue
<template>
<div class="skeleton-box">
<page-a id="page-a" style="display: none"/>
<page-b id="page-b" style="display: none"/>
</div>
</template>
<script>
import PageA from './PageA.vue';
import PageB from './PageB.vue';
export default {
components: {
PageA,
PageB
},
name: 'SkeletonBox'
};
</script>
<style lang="less">
.skeleton-box {
// skeleton css...
}
</style>
// skeleton-entry.js
import Vue from 'vue';
import SkeletonBox from './pages/skeleton/SkeletonBox.vue';
// 创建一个骨架屏 Vue 实例
export default new Vue({
components: {
SkeletonBox
},
template: '<skeleton-box/>'
});
// webpack.skeleton.conf.js
'use strict';
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const nodeExternals = require('webpack-node-externals')
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
function resolve(dir) {
return path.join(__dirname, dir)
}
let skeletonWebpackConfig = merge(baseWebpackConfig, {
target: 'node',
devtool: false,
entry: {
app: resolve('../src/skeleton-entry.js')
},
output: Object.assign({}, baseWebpackConfig.output, {
libraryTarget: 'commonjs2'
}),
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: []
})
// important: enable extract-text-webpack-plugin 开启样式分离
// skeletonWebpackConfig.module.rules[0].use[1].options.loaders 对应 vue-loader
skeletonWebpackConfig.module.rules[0].use[1].options.loaders = utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: true
}),
module.exports = skeletonWebpackConfig
// skeleton-webpack-plugin-config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
module.exports = // inject skeleton content(DOM & CSS) into HTML
new SkeletonWebpackPlugin({
webpackConfig: require('./webpack.skeleton.conf'),
quiet: true,
minimize: true,
router: {
mode: 'hash',
routes: [
{
path: /pagea/,
skeletonId: 'page-a'
},
{
path: /pageb/,
skeletonId: 'page-a'
}
]
}
})
// webpack.dev.conf.js
const SkeletonWebpackPluginConfig = require('./skeleton-webpack-plugin-config')
plugins: [
// ...
SkeletonWebpackPluginConfig,
// ...
]
// webpack.prod.conf.js
const SkeletonWebpackPluginConfig = require('./skeleton-webpack-plugin-config')
new ExtractTextPlugin({
// ...
SkeletonWebpackPluginConfig,
// ...
})
// PS 本想在 webpack.base.conf.js 中统一加配置,但未生效,暂未查明原因
2.2 自动生成骨架屏
方案特点如下:
- 骨架屏是自动生成的,修改UI后不需修改骨架屏页面(不需手写)
- 不支持 hash router
- 不支持嵌套路由
- 支持单页/多页应用
- 需要生成骨架屏的页面,
html
标签的使用要注意,否则不一定会生成对应的骨架屏块 - 饿了么团队提供,但是似乎已经停止维护,谨慎入坑!
使用到的包:
npm i -D page-skeleton-webpack-plugin webpack-log puppeteer
基本原理:
- 通过无头浏览器
puppeteer
打开要生成骨架屏的页面 - 监听
puppeteer
页面渲染完成,注入提取骨架屏的script
到puppeteer
- 递归遍历DOM树,生成骨架屏的
html
片段- 去掉非第一屏的元素
- 去掉隐藏的元素
- 将DOM分类成文本块、按钮块、图片块、SVG块、伪类元素块
- 将
html
字符串插入html
中对应的div
内容中 - 第一次访问页面时,
JS
、CSS
未加载,显示包含html
字符串的骨架屏内容 - 当
SPA
的JS
、CSS
加载完成,替换div
内部内容后,显示真正首页内容
文件描述如下:
- webpack.base.conf.js 配置 skeleton plugin
const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')
plugins: [
new SkeletonPlugin({
pathname: path.resolve(__dirname, './shell'), // 用来存储 shell 文件的地址
staticDir: config.build.assetsRoot, // 最好和 `output.path` 相同
routes: ['/', '/test'], // 将需要生成骨架屏的路由添加到数组中
device: 'iPhone 6'
})
]
2.3 加快骨架屏的渲染
参考:https://juejin.im/entry/5ab37c126fb9a028c06abc18
三、遇到的一些问题
3.1 待优化的点
从
骨架屏
过渡到真实的首屏
页面,能否不重新渲染整个dom
树,而是激活替换对应的骨架屏样式及数据,减小性能开销为
骨架屏
增加一些类似于生命周期的事件,便于对骨架屏进行性能监控
3.2 遇到的问题
自动生成骨架屏
- index.html 引入外部JS或img,报错,
20190903,官方仍未解决
https://github.com/ElemeFE/page-skeleton-webpack-plugin/issues/48