Vue页面骨架屏注入方法


Posted in Javascript onMay 13, 2018

作为与用户联系最为密切的前端开发者,用户体验是最值得关注的问题。关于页面loading状态的展示,主流的主要有loading图和进度条两种。除此之外,越来越多的APP采用了“骨架屏”的方式去展示未加载内容,给予了用户焕然一新的体验。随着SPA在前端界的逐渐流行,首屏加载的问题也在困扰着开发者们。那么有没有一个办法,也能让SPA用上骨架屏呢?这就是这篇文章将要探讨的问题。

文章相关代码已经同步到 Github ,欢迎查阅~

一、何为骨架屏

简单来说,骨架屏就是在页面内容未加载完成的时候,先使用一些图形进行占位,待内容加载完成之后再把它替换掉。

Vue页面骨架屏注入方法 

这个技术在一些以内容为主的APP和网页应用较多,接下来我们以一个简单的Vue工程为例,一起探索如何在基于Vue的SPA项目中实现骨架屏。

二、分析Vue页面的内容加载过程

为了简单起见,我们使用 vue-cli 搭配 webpack-simple 这个模板来新建项目:

vue init webpack-simple vue-skeleton

这时我们便获得了一个最基本的Vue项目:

├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ └── main.js
├── index.html
└── webpack.conf.js

安装完了依赖以后,便可以通过 npm run dev 去运行这个项目了。但是,在运行项目之前,我们先看看入口的html文件里面都写了些什么。

<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="utf-8">
 <title>vue-skeleton</title>
 </head>
 <body>
 <div id="app"></div>
 <script src="/dist/build.js"></script>
 </body>
</html>

可以看到,DOM里

面有且仅有一个 div#app ,当js被执行完成之后,此 div#app 会被 整个替换掉 ,因此,我们可以来做一下实验,在此div里面添加一些内容:

<div id="app">
 <p>Hello skeleton</p>
 <p>Hello skeleton</p>
 <p>Hello skeleton</p>
</div>

打开chrome的开发者工具,在 Network 里面找到 throttle 功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展示了三句“Hello skeleton”,待js加载完了才会替换为原本要展示的内容。

Vue页面骨架屏注入方法 

现在,我们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在 div#app 内直接插入骨架屏相关内容即可。

三、易维护的方案

显然,手动在 div#app 里面写入骨架屏内容是不科学的,我们需要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,我们当然希望所谓的骨架屏也是一个 .vue 文件,它能够在构建时由工具自动注入到 div#app 里面。

首先,我们在 /src 目录下新建一个 Skeleton.vue 文件,其内容如下:

<template>
 <div class="skeleton page">
 <div class="skeleton-nav"></div>
 <div class="skeleton-swiper"></div>
 <ul class="skeleton-tabs">
 <li v-for="i in 8" class="skeleton-tabs-item"><span></span></li>
 </ul>
 <div class="skeleton-banner"></div>
 <div v-for="i in 6" class="skeleton-productions"></div>
 </div>
</template>
<style>
.skeleton {
 position: relative;
 height: 100%;
 overflow: hidden;
 padding: 15px;
 box-sizing: border-box;
 background: #fff;
}
.skeleton-nav {
 height: 45px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-swiper {
 height: 160px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-tabs {
 list-style: none;
 padding: 0;
 margin: 0 -15px;
 display: flex;
 flex-wrap: wrap;
}
.skeleton-tabs-item {
 width: 25%;
 height: 55px;
 box-sizing: border-box;
 text-align: center;
 margin-bottom: 15px;
}
.skeleton-tabs-item span {
 display: inline-block;
 width: 55px;
 height: 55px;
 border-radius: 55px;
 background: #eee;
}
.skeleton-banner {
 height: 60px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-productions {
 height: 20px;
 margin-bottom: 15px;
 background: #eee;
}
</style>

接下来,再新建一个 skeleton.entry.js 入口文件:

import Vue from 'vue'
import Skeleton from './Skeleton.vue'
export default new Vue({
 components: {
 Skeleton
 },
 template: '<skeleton />'
})

在完成了骨架屏的准备之后,就轮到一个关键插件 vue-server-renderer 登场了。该插件本用于服务端渲染,但是在这个例子里,我们主要利用它能够把 .vue 文件处理成 html 和 css 字符串的功能,来完成骨架屏的注入,流程如下:

Vue页面骨架屏注入方法 

四、方案实现

根据流程图,我们还需要在根目录新建一个 webpack.skeleton.conf.js 文件,以专门用来进行骨架屏的构建。

const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = {
 target: 'node',
 entry: {
 skeleton: './src/skeleton.js'
 },
 output: {
 path: path.resolve(__dirname, './dist'),
 publicPath: '/dist/',
 filename: '[name].js',
 libraryTarget: 'commonjs2'
 },
 module: {
 rules: [
 {
 test: /\.css$/,
 use: [
 'vue-style-loader',
 'css-loader'
 ]
 },
 {
 test: /\.vue$/,
 loader: 'vue-loader'
 }
 ]
 },
 externals: nodeExternals({
 whitelist: /\.css$/
 }),
 resolve: {
 alias: {
 'vue$': 'vue/dist/vue.esm.js'
 },
 extensions: ['*', '.js', '.vue', '.json']
 },
 plugins: [
 new VueSSRServerPlugin({
 filename: 'skeleton.json'
 })
 ]
}

可以看到,该配置文件和普通的配置文件基本完全一致,主要的区别在于其 target: 'node' ,配置了 externals ,以及在 plugins 里面加入了 VueSSRServerPlugin 。在 VueSSRServerPlugin 中,指定了其输出的json文件名。我们可以通过运行下列指令,在 /dist 目录下生成一个 skeleton.json 文件:

webpack --config ./webpack.skeleton.conf.js

这个文件在记载了骨架屏的内容和样式,会提供给 vue-server-renderer 使用。

接下来,在根目录下新建一个 skeleton.js ,该文件即将被用于往 index.html 内插入骨架屏。

const fs = require('fs')
const { resolve } = require('path')
const createBundleRenderer = require('vue-server-renderer').createBundleRenderer
// 读取`skeleton.json`,以`index.html`为模板写入内容
const renderer = createBundleRenderer(resolve(__dirname, './dist/skeleton.json'), {
 template: fs.readFileSync(resolve(__dirname, './index.html'), 'utf-8')
})
// 把上一步模板完成的内容写入(替换)`index.html`
renderer.renderToString({}, (err, html) => {
 fs.writeFileSync('index.html', html, 'utf-8')
})

注意,作为模板的 html 文件,需要在被写入内容的位置添加 <!--vue-ssr-outlet--> 占位符,本例子在 div#app 里写入:

<div id="app">
 <!--vue-ssr-outlet-->
</div>

接下来,只要运行 node skeleton.js ,就可以完成骨架屏的注入了。运行效果如下:

<html lang="en">
 <head>
 <meta charset="utf-8">
 <title>vue-skeleton</title>
 <style data-vue-ssr-id="742d88be:0">
.skeleton {
 position: relative;
 height: 100%;
 overflow: hidden;
 padding: 15px;
 box-sizing: border-box;
 background: #fff;
}
.skeleton-nav {
 height: 45px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-swiper {
 height: 160px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-tabs {
 list-style: none;
 padding: 0;
 margin: 0 -15px;
 display: flex;
 flex-wrap: wrap;
}
.skeleton-tabs-item {
 width: 25%;
 height: 55px;
 box-sizing: border-box;
 text-align: center;
 margin-bottom: 15px;
}
.skeleton-tabs-item span {
 display: inline-block;
 width: 55px;
 height: 55px;
 border-radius: 55px;
 background: #eee;
}
.skeleton-banner {
 height: 60px;
 background: #eee;
 margin-bottom: 15px;
}
.skeleton-productions {
 height: 20px;
 margin-bottom: 15px;
 background: #eee;
}
</style></head>
 <body>
 <div id="app">
 <div data-server-rendered="true" class="skeleton page"><div class="skeleton-nav"></div> <div class="skeleton-swiper"></div> <ul class="skeleton-tabs"><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li></ul> <div class="skeleton-banner"></div> <div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div></div>
 </div>
 <script src="/dist/build.js"></script>
 </body>
</html>

可以看到,骨架屏的样式通过 <style></style> 标签直接被插入,而骨架屏的内容也被放置在 div#app 之间。当然,我们还可以进一步处理,把这些内容都压缩一下。改写 skeleton.js ,在里面添加 html-minifier :
...

+ const htmlMinifier = require('html-minifier')
...
renderer.renderToString({}, (err, html) => {
+ html = htmlMinifier.minify(html, {
+ collapseWhitespace: true,
+ minifyCSS: true
+ })
 fs.writeFileSync('index.html', html, 'utf-8')
})

来看看效果:

Vue页面骨架屏注入方法 

效果非常不错!至此,Vue页面接入骨架屏已经完全实现了。

如果还有任何更好的实现思路,也欢迎和我探讨,有机会我也会总结基于 React 的骨架屏注入实践,敬请期待!

文章相关代码已经同步到Github ,欢迎查阅~

Javascript 相关文章推荐
Javascript Math ceil()、floor()、round()三个函数的区别
Mar 09 Javascript
原生javascript实现图片轮播效果代码
Sep 03 Javascript
Eval and new funciton not the same thing
Dec 27 Javascript
jQuery选择器简明总结(含用法实例,一目了然)
Apr 25 Javascript
JS+CSS实现带关闭按钮DIV弹出窗口的方法
Feb 27 Javascript
Javascript URI 解析介绍
Mar 15 Javascript
JavaScript简单表格编辑功能实现方法
Apr 16 Javascript
基于JavaScript实现树形下拉框
Aug 10 Javascript
vue.js 初体验之Chrome 插件开发实录
May 13 Javascript
node.js 利用流实现读写同步,边读边写的方法
Sep 11 Javascript
JavaScript实现单图片上传并预览功能
Sep 30 Javascript
javascript中的with语句学习笔记及用法
Feb 17 Javascript
浅谈在node.js进入文件目录的问题
May 13 #Javascript
解决node修改后需频繁手动重启的问题
May 13 #Javascript
垃圾回收器的相关知识点总结
May 13 #Javascript
基于node搭建服务器,写接口,调接口,跨域的实例
May 13 #Javascript
深入理解js 中async 函数的含义和用法
May 13 #Javascript
如何更好的编写js async函数
May 13 #Javascript
基于jQuery实现无缝轮播与左右点击效果
May 13 #jQuery
You might like
电脑硬件及电脑配置知识大全
2020/03/17 数码科技
php预定义常量
2006/12/25 PHP
基于Zend的Config机制的应用分析
2013/05/02 PHP
PHP实现将几张照片拼接到一起的合成图片功能【便于整体打印输出】
2017/11/14 PHP
做网页的一些技巧(续)
2007/02/01 Javascript
javascript 从if else 到 switch case 再到抽象
2010/07/17 Javascript
js 判断checkbox是否选中的实现代码
2010/11/23 Javascript
javascript内存管理详细解析
2013/11/11 Javascript
SeaJS入门教程系列之使用SeaJS(二)
2014/03/03 Javascript
jquery做的一个简单的屏幕锁定提示框
2014/03/26 Javascript
JavaScript将XML转成JSON的方法
2015/03/12 Javascript
详解数组Array.sort()排序的方法
2020/05/09 Javascript
微信小程序中setInterval的使用方法
2017/09/29 Javascript
浅谈Vuejs中nextTick()异步更新队列源码解析
2017/12/31 Javascript
JavaScript常用进制转换及位运算实例解析
2020/10/14 Javascript
[47:36]Optic vs Newbee 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
[01:00:10]完美世界DOTA2联赛PWL S2 FTD vs Inki 第二场 11.21
2020/11/24 DOTA
python 全文检索引擎详解
2017/04/25 Python
JS设计模式之责任链模式实例详解
2018/02/03 Python
python安装模块如何通过setup.py安装(超简单)
2018/05/05 Python
Python模块的制作方法实例分析
2019/12/21 Python
pytorch 实现打印模型的参数值
2019/12/30 Python
Python 如何操作 SQLite 数据库
2020/08/17 Python
耐克美国官网:Nike.com
2016/08/01 全球购物
行政总经理岗位职责
2013/12/05 职场文书
竟聘演讲稿范文
2013/12/31 职场文书
档案接收函范文
2014/01/10 职场文书
学生周末长期请假条
2014/02/15 职场文书
企业安全生产目标责任书
2014/07/23 职场文书
检讨书范文2000字
2015/01/28 职场文书
公司财务经理岗位职责
2015/04/08 职场文书
优秀范文:《但愿人长久》教学反思3篇
2019/10/24 职场文书
MySql开发之自动同步表结构
2021/05/28 MySQL
Java 数组内置函数toArray详解
2021/06/28 Java/Android
Python音乐爬虫完美绕过反爬
2021/08/30 Python
阿里云ECS云服务器快照的概念以及如何使用
2022/04/21 Servers