骨架屏效果

骨架屏效果

在我們訪問頁面時,當資源未加載完成時,通常會出現空白,這種體驗不是很好,常用的解決方法有添加loading頁、使用骨架屏,相比於loading頁,骨架屏更貼近實際內容。如下圖所示:

在這裏插入圖片描述
刷新頁面時資源未加載成功時,先展示骨架屏。

骨架屏原理

在使用vue框架生成的項目中,當我們訪問頁面時,最先加載的是一個名爲index.html文件,此時它裏面就只有一個id爲app的div元素,只有當js資源加載完成後,vue框架纔會生成頁面展示需要的DOM節點並注入到頁面中進行顯示,js資源加載時間的長短就決定了空白時長,如果在這段時間index.html內含有內容,那麼用戶看到的就不再是空白,而骨架屏效果就是利用這一原理,將頁面的內容佔位情況在用戶訪問之前就寫入到index.html中,這樣在js資源未加載成功時,用戶看到的就是頁面內容佔位情況。這其中需要用到服務端渲染知識,在服務端將含有骨架屏內容的頁面生成好,瀏覽器只負責展示。vue項目服務端渲染我們使用官方比較推薦的vue-server-renderer

在vue項目中如何引入骨架屏

以使用vue-cli3默認方式創建的vue項目爲例,介紹骨架屏的引入:

骨架屏的實現方式有多種,可以簡單使用一張圖片代替,也可以寫一個vue組件,也可以引用一些工具在打包時自動生成,本文是手動寫了一個骨架屏vue組件。

首先根據頁面內容編寫對應的骨架屏組件,如下:

// HelloWorldSkeleton.vue
<template>
  <div class="skeleton-wrapper">
    <div class="img-placeholder"></div>
    <h1 class="h1-placeholder"></h1>
    <p class="p-placeholder"></p>
    <h3 class="h3-placeholder"></h3>
    <ul class="ul-container">
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
    </ul>
    <h3 class="h3-placeholder"></h3>
    <ul class="ul-container">
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
    </ul>
    <h3 class="h3-placeholder"></h3>
    <ul class="ul-container">
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
      <li class="li-placeholder"></li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'hello-world-skeleton'
}
</script>

<style lang="scss" scoped>
.skeleton-wrapper{
  margin-top: 60px;
  text-align: center;
  .img-placeholder{
    display: inline-block;
    width: 200px;
    height: 200px;
    background: #e5e5e5;
  }
  .h1-placeholder, .p-placeholder{
    width: 400px;
    height: 37px;
    margin: 0 auto;
    background: #e5e5e5;
  }
  .p-placeholder{
    margin: 16px auto;
  }
  .h3-placeholder{
    width: 200px;
    height: 22px;
    margin: 0 auto;
    background: #e5e5e5;
  }
  .ul-container{
    list-style-type: none;
    padding: 0;
  }
  .li-placeholder{
    display: inline-block;
    width: 80px;
    height: 18px;
    margin: 0 10px;
    background: #e5e5e5;
  }
}
</style>

骨架屏編寫完成後,接下來就是如何引用的問題了。vue-cli3創建的項目主要是用於客戶端渲染,而骨架屏需要用到服務端渲染,即同一項目出現兩種渲染方式,這就需要我們對項目的打包配置進行修改,vue-cli3對webpack的集成度比較高,有好處也有壞處,好處就是省事,幫我們免去一些基本配置,壞處就是當我們需要更改webpack配置時會不太容易且有一定限制,vue-cli3允許我們在項目根目錄下創建vue.config.js配置文件,在該文件中通過配置configureWebpack來修改webpack相關配置。

vue-cli3創建的項目中自帶的webpack配置是用於客戶端渲染,我們可以不對客戶端渲染的打包配置進行配置,只需要添加服務端渲染的相關配置即可,具體如下:

// vue.config.js
const path = require('path')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')
const ISSERVER = process.env.WEBPACK_TARGET === 'node'
module.exports = {
  configureWebpack: () => {
    if (ISSERVER) {
      return {
        target: 'node',
        entry: path.join(__dirname, './src/components/skeleton-entry.js'),
        devtool: 'source-map',
        output: {
          libraryTarget: 'commonjs2'
        },
        externals:nodeExternals({
          whitelist: /\.css$/
        }),
        plugins: [
          new VueSSRServerPlugin()
        ]
      }
    }
  } 
}

vue.config.js文件中通過接收變量WEBPACK_TARGET來區分是客戶端渲染還是服務端渲染,如果是服務端渲染,我們需要重新配置webpack的入口文件等屬性。變量WEBPACK_TARGET是如何傳入的呢?當然是通過執行package.jsonscripts命令來傳入的,因此需要對scripts命令進行修改:

{
	...
	"scripts": {
    	"serve:client": "vue-cli-service serve",
    	"serve:server": "npm run build:server && node skeleton.js",
    	"build:client": "vue-cli-service build",
    	"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
    	"build": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json",
    	"build:mac": "npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json",
    	"lint": "vue-cli-service lint",
    	"dev": "npm run serve:server && npm run serve:client"
  	},
  	...
}

命令解讀:其中,serve:client命令用於在本地啓動服務(用於瀏覽客戶端渲染的相關頁面);serve:server命令中串聯執行了npm run build:servernode skeleton.js命令,npm run build:server用於服務端渲內容的打包,利用vue-server-renderer/server-plugin插件最終輸出一個json文件,node skeleton.js用於讀取生成的json文件,並通過vue-server-renderercreateBundleRenderer方法將內容寫入到index.html文件中;build:client命令用於打包客戶端渲染相關的內容;build:server命令用於打包服務端渲染的相關內容,通過傳入WEBPACK_TARGET=node參數,告訴webpack使用服務端渲染相關配置進行打包,如果不帶--mode server參數,那麼打包時會報錯,如下:
在這裏插入圖片描述
附上地址

build命令就是將build:server命令與build:client命令結合,由於在執行vue-cli-service build命令時會刪除dist文件夾,當build:server生成的json文件在dist中時繼續執行build:client命令會將json文件刪除,因此使用了move命令先將生成的json文件移出dist文件夾,當build:client命令執行完後,再移入dist中;build:mac命令爲在mac下的構建命令,作用與build命令相同;dev命令就是將serve:serverserve:client命令結合,開啓本地服務(既可以看到服務端渲染的頁面也可以看到客戶端渲染的頁面)。

那麼入口文件skeleton-entry.js中是什麼呢?類似於main.js文件,創建一個vue實例,並將骨架屏組件引入,如下:

// skeleton-entry.js
import Vue from 'vue'
import HelloWorldSkeleton from './HelloWorldSkeleton.vue'

export default new Vue({
  components: {
    HelloWorldSkeleton
  },
  template: '<hello-world-skeleton />'
})

skeleton.js用於將服務端打包生成的json文件通過vue-server-renderer寫入到index.html中,具體如下:

// skeleton.js
const fs = require('fs')
const { resolve } = require('path')
const bundle = require('./dist/vue-ssr-server-bundle.json')

const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

// 讀取`skeleton.json`,以`index.html`爲模板寫入內容
const renderer = createBundleRenderer(bundle, {
  template: fs.readFileSync(resolve(__dirname, './public/index.html'), 'utf-8')
})

// 把上一步模板完成的內容寫入(替換)`index.html`
renderer.renderToString({}, (err, html) => {
  console.log(html)
  fs.writeFileSync('./public/index.html', html, 'utf-8')
})

至此,在vue項目中引用骨架屏的demo就完成了,這只是一個簡單的demo,實際項目中還需要考慮路由切換時骨架屏加載問題,以及開發過程中服務端渲染的熱更新實現問題,後面將會繼續深入。

demo源碼可在github上獲取

參考文獻:

[1] Vue頁面骨架屏
[2] Vue 頁面骨架屏注入實踐
[3] 通過vue-cli3構建一個SSR應用程序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章