自從前端三大框架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渲染引擎選項,直接用默認選項。
接下來執行編譯,看看會發生什麼?
dist目錄下會生成路由對應的文件夾,打開index.htmlnode
<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調試下渲染有什麼區別:
常規渲染:
預渲染:
利用chrome瀏覽器的加載截屏功能能夠看出常規渲染時會有明顯的白屏時間,而預渲染則不會產生白屏,那麼預渲染有什麼缺點呢?webpack
骨架屏的實現原理和預加載相似,都是利用了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在編譯時,注入代碼依賴<!-- shell -->註釋,而vue-cli3中集成的html-webpack-plugin會在編譯作壓縮,將註釋去掉,所以要單獨配置一下,不然會在編譯時致使生成app節點下沒有內容。
還有一個在使用時須要注意的點,若是你是vue-cli3腳手架生成的代碼,運行時可能會報這樣的錯誤:
若是遇到這個錯誤,怎麼解決呢?github上已經有對應的解決辦法了,問題都說完了接下來看看怎麼使用,運行項目後,在chrome調試器裏執行toggleBarweb
會在頁面裏顯示一個Preview skeleton page按鈕,點擊後會生成一個新窗口
這個窗口顯示了當前頁面的骨架屏樣式和代碼,能夠修改骨架屏樣式,而後點擊右上角保存,會將對應路由的骨架屏保存到pathname對應的文件夾下
而後執行編譯,編譯後會在staticDir中生成路由對應的html,這些html中的app節點下都被插入了路由對應骨架屏代碼,而後在staticDir下啓動服務訪問,就能看到骨架屏的效果:
從加載過程當中能夠看到骨架屏的加載算法
本篇文章簡單介紹了我的在白屏優化實踐上嘗試過的方案,每一個方案都個有本身的優劣,須要本身根據實際的業務場景進行取捨,但願對你們在解決此類問題時有所幫助。
若是有錯誤或不嚴謹的地方,歡迎批評指正,若是喜歡,歡迎點贊vue-router