自從前端三大框架React、Vue、Angular面世以來,前端開發逐漸趨向規範化、統一化,大多數時候新建前端項目,首先想到使用的技術必定是三大框架之一,框架給前端開發帶來了極大的便利和規範,可是因爲這三大框架都是JS驅動,在JS沒有解析加載完成以前頁面沒法展現,會處於長時間的白屏,帶來了必定的用戶體驗問題,接下來本篇文章會介紹本人最近在白屏優化時遇到的一些問題和思考html
想到白屏問題,首先想到的解決方案通常都是服務端渲染,在服務端將渲染邏輯處理好,而後將處理好的HTML直接返回給前端展現,這樣就能夠解決白屏的問題,也能夠解決seo的問題,由於不須要動態獲取數據了,可是,這和我早期的寫後端時的開發模式很像,前端和後端關聯在了一塊兒,不利於維護,同時,對於前端工程師來講,要求變高來,須要瞭解必定的後端知識,雖然有相似Nuxt.js這類的SSR框架幫咱們簡化了服務端的部分,可是在要作定製或是解決bug時仍是沒法避免要對服務端部分進行調試、維護,成本頗高,還有須要考慮的服務端渲染會增長服務器壓力,要處理併發、運行速度問題等等前端
這個方案是相對簡單直接的一個解決辦法,嘗試成本也比較低,這裏介紹如何用prerender-spa-plugin作預渲染,這樣就能夠在瀏覽器進行渲染,而不須要將Vue或者React代碼部署到服務器上,以vue-cli3的官方demo爲例作配置,看具體的配置文件:vue
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
module.exports = {
configureWebpack: config => {
let plugins = []
plugins.push(new PrerenderSPAPlugin({
staticDir: path.resolve(__dirname, 'dist'),
routes: ['/', '/about'],
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true
},
renderer: new Renderer({
renderAfterDocumentEvent: 'custom-render-trigger'
})
}))
config.plugins = [
...config.plugins,
...plugins
]
}
}
複製代碼
上面代碼是經常使用prerender-spa-plugin的配置,staticDir預渲染輸出的文件地址,routes要作預渲染的路由,minify壓縮相關的配置,renderer渲染引擎相關的配置,能夠傳入自定以的渲染引擎或者直接使用默認的PuppeteerRenderer,renderAfterDocumentEvent是渲染引擎配置中的一個屬性,指當某個事件觸發時才執行預渲染,這裏 有關於渲染引擎的完整屬性介紹,這很重要,尤爲是對一些特定場景的下的需求,固然簡單場景下徹底能夠不配置renderer渲染引擎選項,直接用默認選項。
接下來執行編譯,看看會發生什麼? node
<div id="app">
<div id="nav">
<a href="/" class="router-link-exact-active router-link-active">Home</a> |
<a href="/about" class="">About</a>
</div>
<div class="home">
<img alt="Vue logo" src="/img/logo.82b9c7a5.png" />
<div class="hello" data-v-7b2de9b7="">
<h1 data-v-7b2de9b7="">Welcome to Your Vue.js App</h1>
<p data-v-7b2de9b7="">For a guide and recipes on how to configure / customize this project,<br data-v-7b2de9b7="" />check out the <a href="https://cli.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-cli documentation</a>.</p>
<h3 data-v-7b2de9b7="">Installed CLI Plugins</h3>
<ul data-v-7b2de9b7="">
<li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" data-v-7b2de9b7="" rel="noopener" target="_blank">babel</a></li>
<li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" data-v-7b2de9b7="" rel="noopener" target="_blank">eslint</a></li>
<li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" data-v-7b2de9b7="" rel="noopener" target="_blank">unit-jest</a></li>
</ul>
<h3 data-v-7b2de9b7="">Essential Links</h3>
<ul data-v-7b2de9b7="">
<li data-v-7b2de9b7=""><a href="https://vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Core Docs</a></li>
<li data-v-7b2de9b7=""><a href="https://forum.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Forum</a></li>
<li data-v-7b2de9b7=""><a href="https://chat.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Community Chat</a></li>
<li data-v-7b2de9b7=""><a href="https://twitter.com/vuejs" data-v-7b2de9b7="" rel="noopener" target="_blank">Twitter</a></li>
<li data-v-7b2de9b7=""><a href="https://news.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">News</a></li>
</ul>
<h3 data-v-7b2de9b7="">Ecosystem</h3>
<ul data-v-7b2de9b7="">
<li data-v-7b2de9b7=""><a href="https://router.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-router</a></li>
<li data-v-7b2de9b7=""><a href="https://vuex.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vuex</a></li>
<li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-devtools#vue-devtools" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-devtools</a></li>
<li data-v-7b2de9b7=""><a href="https://vue-loader.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-loader</a></li>
<li data-v-7b2de9b7=""><a href="https://github.com/vuejs/awesome-vue" data-v-7b2de9b7="" rel="noopener" target="_blank">awesome-vue</a></li>
</ul>
</div>
</div>
</div>
複製代碼
爲了方便,這裏只貼了app節點裏的代碼,以往在沒有使用預渲染插件時app節點裏面是空的沒有內容,從加載index.html文件開始到js文件解析完成以前,因爲app節點裏面是空的,所以頁面會處於白屏狀態,可是預渲染插件在編譯階段就將對應的路由編譯好插入到app節點,這樣就能在js文件解析過程當中有內容展現,js解析完成後,Vue會將app節點內的內容替換成Vue渲染好的內容,來看看chrome調試下渲染有什麼區別: 常規渲染: webpack
預渲染: 利用chrome瀏覽器的加載截屏功能能夠看出常規渲染時會有明顯的白屏時間,而預渲染則不會產生白屏,那麼預渲染有什麼缺點呢?骨架屏的實現原理和預加載相似,都是利用了Puppeteer爬取頁面的功能,Puppeteer是chrome出的一個headlessChromenode庫,提供了API能夠抓取SPA並生成預渲染內容,和預加載不太同樣的是骨架屏技術會在Puppeteer生成內容後,利用算法將生成的內容進行替換,生成骨架頁面,page-skeleton-webpack-plugin是一個用來生成骨架屏的webpack插件,接下來就來看看怎麼使用,仍是以vue-cli3生成的官方項目爲例:git
<div id="app"><!-- shell --></div>
複製代碼
const SkeletonPlugin = require('page-skeleton-webpack-plugin').SkeletonPlugin
const path = require('path')
module.exports = {
publicPath: '/',
outputDir: 'dist',
configureWebpack: config => {
let plugins = []
plugins.push(new SkeletonPlugin({
pathname: path.resolve(__dirname, './shell'), // pathname爲來存儲 shell 文件的地址
staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
routes: ['/', '/about'], // 將須要生成骨架屏的路由添加到數組中
port: '7890'
}))
config.plugins = [
...config.plugins,
...plugins
]
},
chainWebpack: config => {
if (process.env.NODE_ENV === 'production') {
config.plugin('html').tap(opts => {
console.log(opts[0])
opts[0].minify.removeComments = false
return opts
})
}
}
}
複製代碼
上面例子是對page-skeleton-webpack-plugin的簡單配置,想要完整的配置能夠自行前往github獲取,須要注意的是這段代碼:github
chainWebpack: config => {
if (process.env.NODE_ENV === 'production') {
config.plugin('html').tap(opts => {
console.log(opts[0])
opts[0].minify.removeComments = false
return opts
})
}
}
複製代碼
這是修改了vue-cli3中集成的html-webpack-plugin的壓縮配置,將移除註釋去掉了,由於page-skeleton-webpack-plugin在編譯時,注入代碼依賴註釋,而vue-cli3中集成的html-webpack-plugin會在編譯作壓縮,將註釋去掉,所以要單獨配置一下,不然會在編譯時致使生成app節點下沒有內容。
還有一個在使用時須要注意的點,若是你是vue-cli3腳手架生成的代碼,運行時可能會報這樣的錯誤: web
本篇文章簡單介紹了我的在白屏優化實踐上嘗試過的方案,每一個方案都個有本身的優劣,須要本身根據實際的業務場景進行取捨,但願對你們在解決此類問題時有所幫助。
若是有錯誤或不嚴謹的地方,歡迎批評指正,若是喜歡,歡迎點贊算法