- 发布于
骨架屏
- 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() // 关闭服务