一、什么是骨架屏?
骨架屏可以理解为是在需要等待加载内容的位置提供一个占位图形组合,
描绘了当前页面的大致框架的骨架屏页面,然后骨架屏中各个占位部分被实际资源完全替换,
这个过程中用户会觉得内容正在逐渐加载即将呈现,降低了用户的焦躁情绪,使得加载过程主观上变得流畅。
二、何时使用
1、网络较慢,需要长时间等待加载处理的情况下。
2、图文信息内容较多的列表/卡片中。
三、对比菊花图
第一个为骨架屏,第二个为菊花图,第三个为无优化,可以看到相比于传统的菊花图会在感官上觉得内容出现的流畅而不突兀,体验更加优良。
四、生成骨架屏的方法
1、手写HTML、CSS的方式为目标页定制骨架屏 做法可以参考.vue
文件处理为HTML
,插入到页面模板的挂载点中,完成骨架屏的注入。这种方式不甚文明,如果页面样式改变了,还得改一遍骨架屏,增加了维护成本。 骨架屏的样式实现参考 CodePen
2、 使用图片作为骨架屏; 简单暴力,让UI同学花点功夫吧哈哈;小米商城的移动端页面采用的就是这个方法,它是使用了一个Base64的图片来作为骨架屏。
3、 自动生成并自动插入静态骨架屏 这种方法跟第一种方法类似,不过是自动生成骨架屏,可以关注下饿了么开源的插件 page-skeleton-webpack-plugin ,它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中,不过要注意的是这个插件目前只支持history方式的路由,不支持hash方式,且目前只支持首页的骨架屏,并没有组件级的局部骨架屏实现,作者说以后会有计划实现(issue9)。
4、另外还有个插件 vue-skeleton-webpack-plugin,它将插入骨架屏的方式由手动改为自动
,原理在构建时使用 Vue 预渲染功能,将骨架屏组件的渲染结果 HTML 片段插入 HTML 页面模版的挂载点中,将样式内联到 head
标签中。这个插件可以给单页面的不同路由设置不同的骨架屏,也可以给多页面设置,同时为了开发时调试方便,会将骨架屏作为路由写入router中,可谓是相当体贴了。
4.1、vue-server-renderer
4.1.1 分析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文件里面都写了些什么。
vue-skeleton
可以看到,DOM里面有且仅有一个div#app
,当js被执行完成之后,此div#app
会被整个替换掉,因此,我们可以来做一下实验,在此div里面添加一些内容:
Hello skeleton
Hello skeleton
Hello skeleton
打开chrome的开发者工具,在Network
里面找到throttle
功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展示了三句“Hello skeleton”,待js加载完了才会替换为原本要展示的内容。
现在,我们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在div#app
内直接插入骨架屏相关内容即可。
4.1.2 易维护的方案
显然,手动在div#app
里面写入骨架屏内容是不科学的,我们需要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,我们当然希望所谓的骨架屏也是一个.vue
文件,它能够在构建时由工具自动注入到div#app
里面。
首先,我们在/src
目录下新建一个Skeleton.vue
文件,其内容如下:
接下来,再新建一个skeleton.entry.js
入口文件:
import Vue from 'vue'
import Skeleton from './Skeleton.vue'
export default new Vue({
components: {
Skeleton
},
template: ' '
})
在完成了骨架屏的准备之后,就轮到一个关键插件vue-server-renderer
登场了。该插件本用于服务端渲染,但是在这个例子里,我们主要利用它能够把.vue
文件处理成html
和css
字符串的功能,来完成骨架屏的注入,流程如下:
4.1.3 方案实现
接下来,在根目录下新建一个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
文件,需要在被写入内容的位置添加``占位符,本例子在div#app
里写入:
根据流程图,我们还需要在根目录新建一个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({
allowlist: /\.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文件名。
我们可以通过运行下列指令webpack --config ./webpack.skeleton.conf.js
,在/dist
目录下生成一个skeleton.json
文件,
运行node skeleton.js
,就可以完成骨架屏的注入了
在package.json
中配置ske
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"ske": "webpack --config ./webpack.skeleton.conf.js && node skeleton.js"
},
运行ske
npm run ske
skeleton.json
这个文件在记载了骨架屏的内容和样式,会提供给vue-server-renderer
使用。
vue-skeleton
可以看到,骨架屏的样式通过标签直接被插入,而骨架屏的内容也被放置在
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')
})
来看看效果:
4.2、page-skeleton-webpack-plugin
4.2.1安装page-skeleton-webpack-plugin
npm install --save-dev page-skeleton-webpack-plugin
4.2.2 根目录创建shell
文件夹(文件夹名字可以自己定义,但是要和vue.config.js中储存shell文件地址一致),用于储存shell
文件,也就是page-skeleton-webpack-plugin
自动生成生成的骨架屏html
文件
4.2.3 在index.html入口文件添加
占位符
若想要更改占位符,修改位置:修改node_modules/page-skeleton-webpack-plugin/src/util/index.js
const outputSkeletonScreen = async (originHtml, options, log) => {
const { pathname, staticDir, routes } = options
return Promise.all(routes.map(async (route) => {
const trimedRoute = route.replace(/\//g, '')
const filePath = path.join(pathname, trimedRoute ? `${trimedRoute}.html` : 'index.html')
const html = await promisify(fs.readFile)(filePath, 'utf-8')
const finalHtml = originHtml.replace('', html) # 修改此处,只要保持和index.html入口文件占位符一致即可
const outputDir = path.join(staticDir, route)
const outputFile = path.join(outputDir, 'index.html')
await fse.ensureDir(outputDir)
await promisify(fs.writeFile)(outputFile, finalHtml, 'utf-8')
log(`write ${outputFile} successfully in ${route}`)
return Promise.resolve()
}))
}
4.2.4 创建vue.config.js
const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')
const path = require('path')
module.exports = {
configureWebpack: {
plugins: [
new SkeletonPlugin({
pathname: path.resolve(__dirname, './shell'), // 用来存储 shell 文件的地址
staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
routes: ['/','/about'], // 将需要生成骨架屏的路由添加到数组中
image:{ // 可配置骨架屏元素样式
color:"#333333",
shape:"circle"
}
})
],
},
chainWebpack: (config) => { // 解决vue-cli3脚手架创建的项目压缩html 干掉导致骨架屏不生效
if (process.env.NODE_ENV !== 'development') {
config.plugin('html').tap(opts => {
opts[0].minify.removeComments = false
return opts
})
}
},
};
4.2.5 运行项目
npm run serve
报错解决办法Error:
listen EADDRINUSE: address already in use :::8989
修复vue-cli3.0项目端口被占用的bug
// 修改node_modules/page-skeleton-webpack-plugin/src/skeletonPlugin.js
if (!this.server) {
const server = this.server = new Server(this.options) // eslint-disable-line no-multi-assign
server.listen().catch(err => server.log.warn(err))
}
4.2.6 生成骨架屏
在浏览器打开页面,通过 Ctrl|Cmd + enter 呼出插件交互界面,或者在在浏览器的 JavaScript 控制台内输入toggleBar 呼出交互界面
骨架屏生成中,需要一小会儿时间
骨架屏生成好后,会跳转到以下页面
保存骨架屏后,会在项目中的shell目录下生成相关骨架页面
4.2.7查看骨架屏效果
npm run build
4.3、vue-skeleton-webpack-plugin
4.3.1 安装vue-skeleton-webpack-plugin
npm install vue-skeleton-webpack-plugin
4.3.2 创建模板文件,如果不同的路由界面显示不同的模板可以创建过个模板文件,我在src
的common文件夹下面创建了skeleton文件夹并创建三个文件,这样文件样式可以根据自己需求自定义
Skeleton1.vue
Skeleton2.vue
entry-skeleton.js
import Vue from 'vue'
import Skeleton1 from './Skeleton1'
import Skeleton2 from './Skeleton2'
export default new Vue({
components: {
Skeleton1,
Skeleton2
},
template: `
`
})
4.3.3创建vue.config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
const path = require('path')
module.exports = {
configureWebpack: (config) => {
config.plugins.push(new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: path.join(__dirname, './src/skeleton.js')
}
},
minimize: true,
quiet: true,
router: {
mode: 'hash',
routes: [
{ path: '/', skeletonId: 'skeleton1' },
{ path:'/about', skeletonId: 'skeleton2' }
]
}
}))
},
// css相关配置
css: {
// 是否使用css分离插件 ExtractTextPlugin
extract: true,
// 开启 CSS source maps?
sourceMap: false,
// 启用 CSS modules for all css / pre-processor files.
modules: false
},
// 在开发模式下分离css样式,让骨架屏的css在开发模式下生效
css: {
extract: true
}
}
vue-skeleton-webpack-plugin插件参数说明
webpackConfig 必填,渲染 skeleton 的 webpack 配置对象
insertAfter 选填,渲染 DOM 结果插入位置,默认值为字符串 '
也可以传入 Function,方法签名为 insertAfter(entryKey: string): string,返回值为挂载点字符串
quiet 选填,在服务端渲染时是否需要输出信息到控制台
router 选填 SPA 下配置各个路由路径对应的 Skeleton
mode 选填 路由模式,两个有效值 history|hash
routes 选填 路由数组,其中每个路由对象包含两个属性:
path 路由路径 string|RegExp
skeletonId Skeleton DOM 的 id string
minimize 选填 SPA 下是否需要压缩注入 HTML 的 JS 代码
来源:
https://juejin.im/post/5b79a2786fb9a01a18267362
https://segmentfault.com/a/1190000014832185