做为与用户联系最为密切的前端开发者,用户体验是最值得关注的问题。关于页面loading状态的展现,主流的主要有loading图和进度条两种。除此以外,愈来愈多的APP采用了“骨架屏”的方式去展现未加载内容,给予了用户面目一新的体验。随着SPA在前端界的逐渐流行,首屏加载的问题也在困扰着开发者们。那么有没有一个办法,也能让SPA用上骨架屏呢?这就是这篇文章将要探讨的问题。css
文章相关代码已经同步到 Github,欢迎查阅~
简单来讲,骨架屏就是在页面内容未加载完成的时候,先使用一些图形进行占位,待内容加载完成以后再把它替换掉。html
这个技术在一些之内容为主的APP和网页应用较多,接下来咱们以一个简单的Vue工程为例,一块儿探索如何在基于Vue的SPA项目中实现骨架屏。前端
为了简单起见,咱们使用vue-cli
搭配webpack-simple
这个模板来新建项目:vue
vue init webpack-simple vue-skeleton
这时咱们便得到了一个最基本的Vue项目:node
. ├── package.json ├── src │ ├── App.vue │ ├── assets │ └── main.js ├── index.html └── webpack.conf.js
安装完了依赖之后,即可以经过npm run dev
去运行这个项目了。可是,在运行项目以前,咱们先看看入口的html文件里面都写了些什么。webpack
<!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里面添加一些内容:git
<div id="app"> <p>Hello skeleton</p> <p>Hello skeleton</p> <p>Hello skeleton</p> </div>
打开chrome的开发者工具,在Network
里面找到throttle
功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展现了三句“Hello skeleton”,待js加载完了才会替换为本来要展现的内容。github
如今,咱们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在div#app
内直接插入骨架屏相关内容便可。web
显然,手动在div#app
里面写入骨架屏内容是不科学的,咱们须要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,咱们固然但愿所谓的骨架屏也是一个.vue
文件,它可以在构建时由工具自动注入到div#app
里面。chrome
首先,咱们在/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
字符串的功能,来完成骨架屏的注入,流程以下:
根据流程图,咱们还须要在根目录新建一个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.entry.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页面骨架屏注入实践,若是想看更复杂一些的例子,能够参考《为vue项目添加骨架屏》这篇文章,本文的许多思路也是受其启发,很是值得阅读。
若是还有任何更好的实现思路,也欢迎和我探讨,有机会我也会总结基于React
的骨架屏注入实践,敬请期待!
文章相关代码已经同步到 Github,欢迎查阅~