骨架屏效果
在我們訪問頁面時,當資源未加載完成時,通常會出現空白,這種體驗不是很好,常用的解決方法有添加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.json
中scripts
命令來傳入的,因此需要對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:server
和node skeleton.js
命令,npm run build:server
用於服務端渲內容的打包,利用vue-server-renderer/server-plugin
插件最終輸出一個json文件,node skeleton.js
用於讀取生成的json文件,並通過vue-server-renderer
的createBundleRenderer
方法將內容寫入到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:server
與serve: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應用程序