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 相关文章推荐
Mootools 1.2教程(3) 数组使用简介
Sep 14 Javascript
JS上传图片前的限制包括(jpg jpg gif及大小高宽)等
Dec 19 Javascript
关于火狐(firefox)及ie下event获取的两种方法
Dec 27 Javascript
jquery简单的拖动效果实现原理及示例
Jul 26 Javascript
AngularJS表单编辑提交功能实例
Feb 13 Javascript
JS实现超炫网页烟花动画效果的方法
Mar 02 Javascript
Javascript控制div属性动态变化实例分析
Oct 08 Javascript
jQuery实现给input绑定回车事件的方法
Feb 09 Javascript
详解给Vue2路由导航钩子和axios拦截器做个封装
Apr 10 Javascript
dts文件中删除一个node或属性的操作方法
Aug 05 Javascript
layer.open关闭父窗口 以及调用父页面的方法
Aug 17 Javascript
webpack的 rquire.context用法实现工程自动化的方法
Feb 07 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
用Flash图形化数据(二)
2006/10/09 PHP
php 进度条实现代码
2009/03/10 PHP
php上的memcache和memcached两个pecl库
2010/03/29 PHP
php截取字符串并保留完整xml标签的函数代码
2013/02/06 PHP
PHP实现的QQ空间g_tk加密算法
2015/07/09 PHP
Yii2创建多界面主题(Theme)的方法
2016/10/08 PHP
PHP多进程编程实例详解
2017/07/19 PHP
微信公众平台开发教程⑥ 微信开发集成类的使用图文详解
2019/04/10 PHP
javascript while语句和do while语句的区别分析
2007/12/08 Javascript
手把手教你自己写一个js表单验证框架的方法
2010/09/14 Javascript
js onload事件不起作用示例分析
2013/10/09 Javascript
深入理解javascript中return的作用
2013/12/30 Javascript
利用js正则表达式验证手机号,email地址,邮政编码
2014/01/23 Javascript
Jquery基础教程之DOM操作
2015/08/19 Javascript
JS实现仿微博可关闭弹出层效果
2015/09/21 Javascript
canvas绘制七巧板
2017/02/03 Javascript
利用node.js本地搭建HTTP服务器
2017/04/19 Javascript
微信小程序开发图片拖拽实例详解
2017/05/05 Javascript
vue实现微信分享朋友圈,发送朋友的示例讲解
2018/02/10 Javascript
完美解决iview 的select下拉框选项错位的问题
2018/03/02 Javascript
微信小程序滑动选择器的实现代码
2018/08/10 Javascript
解决VUE中document.body.scrollTop为0的问题
2018/09/15 Javascript
浅谈layui 表单元素的选中问题
2019/10/25 Javascript
javascript+Canvas实现画板功能
2020/06/23 Javascript
详细分析React 表单与事件
2020/07/08 Javascript
vue实现的多页面项目如何优化打包的步骤详解
2020/07/19 Javascript
从零学python系列之数据处理编程实例(二)
2014/05/22 Python
使用python在本地电脑上快速处理数据
2017/06/22 Python
详解django中url路由配置及渲染方式
2019/02/25 Python
Python异步操作MySQL示例【使用aiomysql】
2019/05/16 Python
关于圣诞节的广播稿
2014/01/26 职场文书
考试违纪检讨书
2014/02/02 职场文书
企业内控岗位的职责
2014/02/07 职场文书
会计工作总结范文2014
2014/12/23 职场文书
2015年幼儿园教育教学工作总结
2015/05/25 职场文书
Elasticsearch 基本查询和组合查询
2022/04/19 Python