什么是骨架屏?
简单的说, 骨架屏就是在页面未渲染完成的时候, 先用一些简单的图形大致勾勒出页面的基本轮廓, 给用户造成页面正在加载的错觉, 待页面渲染完成之后再用页面替换掉骨架屏, 从而减少页面白屏的时间, 给用户带来更好的体验.
分析 vue 渲染过程
使用 vue-cli3.0 创建项目: vue create project
在生成的项目文件夹下的 public 文件夹下的 index.html 文件代码如下:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
- <link rel="icon" href="<%= BASE_URL %>favicon.ico">
- <title>
- project
- </title>
- </head>
- <body>
- <noscript>
- <strong>
- We're sorry but project doesn't work properly without JavaScript enabled.
- Please enable it to continue.
- </strong>
- </noscript>
- <div id="app">
- </div>
- <!-- built files will be auto injected -->
- </body>
- </HTML>
可以看到, DOM 里面有一个 div#App, 当 JS 被执行完成之后, 此 div#App 会被整个替换掉, 因此, 如何在 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 class="skeleton-swiper-item item-one"></div>
- <div class="skeleton-swiper-item item-two"></div>
- </div>
- </div>
- </template>
- <style>
- HTML,body,div{
- margin:0;
- padding:0;
- }
- .skeleton {
- height: 100%;
- overflow: hidden;
- box-sizing: border-box;
- background: #fff;
- }
- .skeleton-nav {
- height: 54px;
- background: #eee;
- margin-bottom: 20px;
- }
- .skeleton-swiper {
- min-height:600px;
- max-width:1280px;
- margin:0 auto;
- }
- .skeleton-swiper-item{
- min-height: 600px;
- height:100%;
- background:#eee;
- border-radius:5px;
- }
- .item-one{
- width:20%;
- float:left;
- }
- .item-two{
- width:78%;
- float:right;
- }
- </style>
接下来, 在 / src 目录再新建一个 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 字符串的功能, 来完成骨架屏的注入.
骨架屏注入
首先在 public 文件夹下新建一个 template.HTML 文件, 并且其代码和 index.HTML 文件代码相同, 但是需要在 div#App 中添加 <!--vue-ssr-outlet--> 占位符:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
- <link rel="icon" href="./favicon.ico">
- <title>
- 医生工作台
- </title>
- </head>
- <body>
- <noscript>
- <strong>
- We're sorry but yz_doctors doesn't work properly without JavaScript enabled.
- Please enable it to continue.
- </strong>
- </noscript>
- <div id="app">
- <!--vue-ssr-outlet-->
- </div>
- <!-- built files will be auto injected -->
- </body>
- </HTML>
然后, 我们还需要在根目录新建一个 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');
- const VueLoaderPlugin = require('vue-loader/lib/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 VueLoaderPlugin(),
- new VueSSRServerPlugin({
- filename: 'skeleton.json',
- }),
- ],
- };
可以看到, 该配置文件和普通的配置文件基本完全一致, 主要的区别在于其 target: 'node', 配置了 externals, 以及在 plugins 里面加入了 VueSSRServerPlugin. 在 VueSSRServerPlugin 中, 指定了其输出的 JSON 文件名. 然后通过运行下列指令, 在 / dist 目录下生成一个 skeleton.JSON 文件: webpack --config ./webpack.skeleton.conf.JS 接下来, 在根目录下新建一个 skeleton.JS, 该文件即将被用于往 index.HTML 内插入骨架屏:
- const fs = require('fs');
- const { resolve } = require('path');
- const htmlMinifier = require('html-minifier');
- const createBundleRenderer = require('vue-server-renderer').createBundleRenderer;
- // 先把 vue 的模板文件 index.HTML 置换成标准的模板, 防止骨架屏污染
- let tempData = fs.readFileSync(resolve(__dirname, './public/template.html'), 'utf-8');
- fs.writeFileSync('./public/index.html', tempData, 'utf-8');
- console.log('模板注入完成');
- // 读取 `skeleton.json`, 以 `index.html` 为模板写入内容
- const renderer = createBundleRenderer(resolve(__dirname, './dist/skeleton.json'), {
- template: fs.readFileSync(resolve(__dirname, './public/index.html'), 'utf-8'),
- });
- // 把上一步模板完成的内容写入(替换)`index.html`
- renderer.renderToString({}, (err, HTML) => {
- if (err) {
- console.log(err);
- return;
- }
- HTML = htmlMinifier.minify(HTML, {
- collapseWhitespace: true,
- minifyCSS: true,
- });
- fs.writeFileSync('./public/index.html', HTML, 'utf-8');
- });
- console.log('骨架屏注入完成');
接下来, 只要运行 node skeleton.JS, 就可以完成骨架屏的注入. 为了在 NPM run serve 的时候自动完成骨架屏的注入, 避免运行多次命令, 需要在 package.JSON 中增加一条命令 "preserve": "webpack --config ./webpack.skeleton.conf.js && node skeleton.js", 放在 "serve" 命令之前.
总结
新建 template.HTML 文件的目的是为了保存模板文件的干净, 因为每次完成骨架屏的注入后 index.HTML 文件中的 <!--vue-ssr-outlet--> 占位符已经被骨架屏代码所替换, 再次修改骨架屏后就无法完成骨架屏的注入啦, 所以在注入骨架屏时先用 template.HTML 文件中的内容替换 index.HTML 文件, 避免了每次修改骨架屏时还要手动修改 index.HTML 文件, 运行一条命令实现骨架屏的自动注入.
来源: https://juejin.im/post/5c3d952ff265da616e4ca9ad