現(xiàn)在的前端開發(fā)領(lǐng)域,都是前后端分離,前端框架主流的都是 SPA
,MPA
;這就意味著,頁面渲染以及等待的白屏?xí)r間,成為我們需要解決的問題點(diǎn);而且大項(xiàng)目,這個(gè)問題尤為突出。
webpack 可以實(shí)現(xiàn)按需加載,減小我們首屏需要加載的代碼體積;再配合上 CDN 以及一些靜態(tài)代碼(框架,組件庫等等…)緩存技術(shù),可以很好的緩解這個(gè)加載渲染的時(shí)間過長(zhǎng)的問題。
但即便如此,首屏的加載依然還是存在這個(gè)加載以及渲染的等待時(shí)間問題;
目前主流,常見的解決方案是使用骨架屏技術(shù),包括很多原生的APP,在頁面渲染時(shí),也會(huì)使用骨架屏。(下圖中,紅圈中的部分,即為骨架屏在內(nèi)容還沒有出現(xiàn)之前的頁面骨架填充,以免留白)
在 Vue 中,我們是通過 $mount
實(shí)例方法去掛載 vm 的;我們來簡(jiǎn)單看一下 Vue 代碼里面關(guān)于 $mount
方法的實(shí)現(xiàn):
const mount = Vue.prototype.$mountVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && query(el) /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { ... } return mount.call(this, el, hydrating)}
我們可以看到:這段代碼首先緩存了原型上的 $mount 方法,再重新定義該方法,我們先來分析這段代碼。首先,它對(duì) el 做了限制,Vue 不能掛載在 body、html 這樣的根節(jié)點(diǎn)上。為什么??
因?yàn)閞ender生成的vNode,通過 $mount 方法,掛載在我們的定義的 DOM 元素上;這里的掛載是【替換】的意思。
默認(rèn)情況下我們的模版 index.html
里面有一個(gè) id 為 app 的 div 元素。我們最終的應(yīng)用程序代碼會(huì)替換掉這個(gè)元素,也就是 <div id="app"></div>
;對(duì),我們 Vue 渲染出來的內(nèi)容是替換掉它,而不是插入在這個(gè)節(jié)點(diǎn)中。
這也就是 Vue 不能掛載在 body、html 這樣的根節(jié)點(diǎn)的原因。你總不能把 body、html 這樣的元素節(jié)點(diǎn)替換掉把。
知識(shí)點(diǎn)補(bǔ)充:
如果沒有定義 render 方法,則會(huì)把 el 或者 template 字符串轉(zhuǎn)換成 render 方法。這里我們要牢記,在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無論我們是用單文件 .vue 方式開發(fā)組件,還是寫了 el 或者 template 屬性,最終都會(huì)轉(zhuǎn)換成 render 方法,那么這個(gè)過程是 Vue 的一個(gè)“在線編譯”的過程,它是調(diào)用 compileToFunctions 方法實(shí)現(xiàn)的。最后,調(diào)用原先原型上的 $mount 方法掛載。
參考: Vue 實(shí)例掛載的實(shí)現(xiàn)
我們模版(index.html)里面的內(nèi)容是這樣的:
<body> <div id="app"> <span style="color: red;font-size: 34px;">你好</span> </div> <!-- built files will be auto injected --></body>
模版里面的掛載點(diǎn)是 div#app
,App.vue
里面的根節(jié)點(diǎn)是 div#app-two
,渲染完成以后,頁面上的 div#app
就變成了 div#app-two
。
那么,這里分析總結(jié)出來的最重要的一點(diǎn)就是:Vue 的 $mount 方法掛載元素,采用的是【替換】模版中的掛載點(diǎn) 這樣的方法,知道了這個(gè)知識(shí)點(diǎn)以后,我們要實(shí)現(xiàn)骨架屏,就有了很好的實(shí)現(xiàn)思路了。
思路:在 index.html 中的 div#app 中來實(shí)現(xiàn)骨架屏,程序渲染后就會(huì)替換掉 index.html 里面的 div#app 骨架屏內(nèi)容;
使用圖片作為骨架屏; 簡(jiǎn)單暴力,讓UI同學(xué)花點(diǎn)功夫吧;小米商城的移動(dòng)端頁面采用的就是這個(gè)方法,它是使用了一個(gè)Base64的圖片來作為骨架屏。
按照方案一的方案,將這個(gè) Base64 的圖片寫在我們的 index.html
模塊中的 div#app
里面。
我們可能不希望在默認(rèn)的模版(index.html)上來進(jìn)行代碼的coding;想在方案一的基礎(chǔ)上,將骨架屏的代碼抽離出來,使用一個(gè) .vue 文件來 coding,易于維護(hù)。
1、我們?cè)?src
下建一個(gè) skeleton
目錄,在里面創(chuàng)建兩個(gè)文件(skeleton.vue
、skeleton.entry.js
);skeleton.vue
就是我們的骨架屏頁面的代碼,skeleton.entry.js
是編譯 skeleton.vue
的入口文件,類似于我們 Vue 項(xiàng)目中的 main.js
文件;
// skeleton.entry.jsimport Vue from 'vue'import Skeleton from './skeleton.vue'export default new Vue({ // 根實(shí)例簡(jiǎn)單的渲染應(yīng)用程序組件 render: h => h(Skeleton)})
<!-- skeleton.vue --><template> <div class="skeleton page"> <span>骨架屏</span> </div></template><style scoped></style>
2、我們還需要在新建一個(gè) webpack.skeleton.conf.js
文件,以專門用來進(jìn)行骨架屏的構(gòu)建(這個(gè)文件放在哪里無所謂,可以放在根目錄下,也可以放在 build
目錄中)。這是一個(gè) webpack 的配置文件,配合使用 vue-server-renderer
將我們的 skeleton.vue
文件內(nèi)容構(gòu)建為單個(gè)的 json 格式的文件(這是 Vue SSR 渲染的策略)
// webpack.skeleton.conf.js'use strict'const path = require('path')const nodeExternals = require('webpack-node-externals')const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')module.exports = { target: 'node', devtool: '#source-map', entry: './src/skeleton/skeleton.entry.js', output: { path: path.resolve(__dirname, '../dist'), publicPath: '/dist/', filename: '[name].js', libraryTarget: 'commonjs2' }, module: { noParse: /es6-promise\.js$/, // avoid webpack shimming process rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { compilerOptions: { preserveWhitespace: false } } }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] } ] }, performance: { hints: false }, externals: nodeExternals({ // do not externalize CSS files in case we need to import it from a dep whitelist: /\.css$/ }), plugins: [ // 這是將服務(wù)器的整個(gè)輸出構(gòu)建為單個(gè) JSON 文件的插件。 // 默認(rèn)文件名為 `vue-ssr-server-bundle.json` new VueSSRServerPlugin({ filename: 'skeleton.json' }) ]}
3、寫完 skeleton.vue
的內(nèi)容以后,使用 webpack-cli
運(yùn)行這個(gè) webpack.skeleton.conf.js
配置文件。
// package.json"skeleton": "webpack --progress --config build/webpack.skeleton.conf.js"
然后運(yùn)行:
npm i webpack-cli@3.3.10 -Dnpm run skeleton
就會(huì)在 dist
文件夾中生成一個(gè)skeleton.json
文件。
4、將 skeleton.json
內(nèi)容插入到模版文件 index.html
中。(在根目錄下創(chuàng)建一個(gè) skeleton.js
文件)
// skeleton.jsconst fs = require('fs')const { resolve } = require('path')const { createBundleRenderer } = require('vue-server-renderer')function createRenderer(bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { // recommended for performance // runInNewContext: false }))}const handleError = err => { console.error(`error during render : ${req.url}`) console.error(err.stack)}const bundle = require('./dist/skeleton.json')const templatePath = resolve('./index.html')const template = fs.readFileSync(templatePath, 'utf-8')const renderer = createRenderer(bundle, { template})// console.log(renderer)/** * 說明: * 默認(rèn)的index.html中包含<%= BASE_URL %>的插值語法 * 我們不在生成骨架屏這一步改變模板中的這個(gè)插值 * 因?yàn)檫@個(gè)插值會(huì)在項(xiàng)目構(gòu)建時(shí)完成 * 但是如果模板中有這個(gè)插值語法,而我們?cè)趘ue-server-renderder中使用這個(gè)模板,而不傳值的話,是會(huì)報(bào)錯(cuò)的 * 所以,我們?nèi)サ裟0逯械牟逯?,而使用這個(gè)傳參的方式,再將這兩個(gè)插值原模原樣返回到模板中 * * 文檔: https://cli.vuejs.org/zh/guide/html-and-static-assets.html#%E6%8F%92%E5%80%BC */const context = { title: '', // default title meta: `<meta name="theme-color" content="#4285f4"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="<%= BASE_URL %>css/reset.css">`}renderer.renderToString(context, (err, html) => { if(err) { return handleError(err) } fs.writeFileSync(resolve(__dirname, './index.html'), html, 'utf-8')})
5、模版 index.html 加上插槽注解
這里需要注意的是:index.html 中的 div#app 中要加一個(gè)注解插槽,<!--vue-ssr-outlet-->
這個(gè)是必須的,Vue SSR 文檔中有說這個(gè)。這個(gè)注解是必須的,請(qǐng)注意!
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>vue-for-test</title> </head> <body> <div id="app"> <!--vue-ssr-outlet--> </div> <!-- built files will be auto injected --> </body></html>
6、執(zhí)行
node skeleton.js
執(zhí)行成功后,模版 index.html
中的 div#app
中的內(nèi)容就會(huì)變成我們的骨架屏代碼;
7、看一下效果
線上可以看到效果的例子也是有的: map-chart;記得選擇,瀏覽器 -> network -> slow 3G 模式來預(yù)覽 骨架屏效果。
在方案三中,還涉及到了 Vue SSR 的內(nèi)容,關(guān)于 SSR 的知識(shí)的學(xué)習(xí),可以參考我之前寫的一個(gè)教程: https://github.com/Neveryu/vue-ssr-lessons
餓了么開源的插件 page-skeleton-webpack-plugin ,它根據(jù)項(xiàng)目中不同的路由頁面生成相應(yīng)的骨架屏頁面,并將骨架屏頁面通過 webpack 打包到對(duì)應(yīng)的靜態(tài)路由頁面中,不過要注意的是這個(gè)插件目前只支持 history 方式的路由,不支持 hash 方式,且目前只支持首頁的骨架屏,并沒有組件級(jí)的局部骨架屏實(shí)現(xiàn),作者說以后會(huì)有計(jì)劃實(shí)現(xiàn)(issue9)。
另外還有個(gè)插件 vue-skeleton-webpack-plugin,它將插入骨架屏的方式由手動(dòng)改為自動(dòng),原理在構(gòu)建時(shí)使用 Vue 預(yù)渲染功能,將骨架屏組件的渲染結(jié)果 HTML 片段插入 HTML 頁面模版的掛載點(diǎn)中,將樣式內(nèi)聯(lián)到 head 標(biāo)簽中。這個(gè)插件可以給單頁面的不同路由設(shè)置不同的骨架屏,也可以給多頁面設(shè)置,同時(shí)為了開發(fā)時(shí)調(diào)試方便,會(huì)將骨架屏作為路由寫入 router 中,可謂是相當(dāng)體貼了。
聯(lián)系客服