Ajax 技術的出現,讓咱們的 Web 應用可以在不刷新的狀態下顯示不一樣頁面的內容,這就是單頁應用。在一個單頁應用中,每每只有一個 html 文件,而後根據訪問的 url 來匹配對應的路由腳本,動態地渲染頁面內容。單頁應用在優化了用戶體驗的同時,也給咱們帶來了許多問題,例如 SEO 不友好、首屏可見時間過長等。服務端渲染(SSR)和預渲染(Prerender)技術正是爲解決這些問題而生的。javascript
閱讀本文,你可以瞭解到什麼是預渲染、預渲染與服務端渲染的異同以及預渲染在 Vue.js 項目中的使用。css
下圖簡單展現了客戶端渲染、服務端渲染和預渲染的請求流程。html
本文示例使用 vue-cli 生成,點擊這裏查看示例。dist
目錄是啓用了預渲染的打包目錄,dist2
目錄則是普通客戶端渲染的打包目錄。經過對比目錄中的文件,你能夠對預渲染有個初步的瞭解。若你仍是不知道什麼是預渲染,不妨先通讀全文。前端
針對單頁應用,服務端渲染和預渲染共同解決的問題:vue
預渲染能與服務端渲染同樣提升 SEO 優化,但前者比後者須要更少的配置,實現成本低。弱網環境下,預渲染能更快地呈現頁面內容,減小頁面可見時間。java
那什麼場景下不適合使用預渲染呢:webpack
prerender-spa-plugin 是一個 webpack 插件用於在單頁應用中預渲染靜態 html 內容。所以,該插件限定了你的單頁應用必須使用 webpack 構建,且它是框架無關的,不管你是使用 React 或 Vue 甚至不使用框架,都能用來進行預渲染。本文示例基於 Vue.js 2.0 + vue-router。nginx
下文會從生成項目講起,而後看下沒有配置預渲染前的樣子,再配置預渲染進行構建,對比先後的差異。git
首先生成一個項目並安裝依賴。github
vue init webpack vue-prerender-demo
cd vue-prerender-demo && npm install複製代碼
組件開發過程咱們不關注,具體能夠查看示例源代碼。開發完成視圖以下。
這是一個新聞應用的頁面,包括了最新、最熱兩個列表頁和一個文章頁。路由配置以下。
new Router({
mode: 'history',
routes: [
{
path: '/',
component: Home,
children: [
{
path: 'new',
alias: '/',
component: () => import('@/components/New')
},
{
path: 'hot',
component: () => import('@/components/Hot')
}
]
},
{
path: '/article/:id',
component: Article
}
]
})複製代碼
預渲染的單頁應用路由須要使用 History 模式而不是 Hash 模式。緣由很簡單,Hash 不會帶到服務器,路由信息會丟失。vue-router 啓用 History 模式參考這裏。
History 模式須要後臺配置支持,最簡單的是經過 nginx 配置 try_files 指令。
location / {
try_files $uri $uri/ /index.html;
}複製代碼
配置完成後執行構建 npm run build
,根據 nginx 配置,如今不管訪問哪一個路由都會返回 dist/index.html。
訪問 / 路由。
能夠看到,在 Fast 3G 網絡下,首屏可見時間是 4.34s,頁面至少在加載下面文件後才能被看到。
其中 vendor 文件包含了引用的第三方庫,文件規模較大。加載文件多,增長了白屏時間。因此,最有效的優化方案是減小首屏依賴文件。這裏開始配置預渲染。
安裝 prerender-spa-plugin,安裝時件略長,由於其依賴了 phantomjs,請耐心等待。
npm install prerender-spa-plugin --save-dev複製代碼
咱們只在生產環境中進行預渲染,修改 build/webpack.prod.conf.js,在配置插件的地方加入以下代碼。
var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')
{
// ...
plugins: [
// ...
new PrerenderSpaPlugin(
// 輸出目錄的絕對路徑
path.join(__dirname, '../dist'),
// 預渲染的路由
[ '/new', '/hot' ]
)
]
}複製代碼
實例化 PrerenderSpaPlugin 須要至少兩個參數,第一個參數是單頁應用的輸出目錄,第二個參數指定預渲染的路由,這裏執行了兩個路由 /new
和 /hot
。執行構建 npm run build
。
訪問 /new
路由。
一樣在 Fast 3G 網絡下,首屏可見時間縮短至 2.30s。事實上,只要加載 html 和 app.css 文件,頁面內容就能看到了。
dist
│ index.html
│
├─hot
│ index.html
│
├─new
│ index.html
│
└─static複製代碼
對比構建完成目錄,能夠發現預渲染的目錄多了兩個文件 new/index.html
, hot/index.html
。
查看 new/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vue-prerender-demo</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link href="/static/css/app.23611ac69a9fa48640e3bad8ceeab7bf.css" rel="stylesheet">
<script type="text/javascript" charset="utf-8" async="" src="/static/js/0.41194d76e86bbf547b16.js"></script>
</head>
<body>
<div id="app">
<div>
<div class="mu-appbar mu-paper-1">
<div class="left">
<i class="mu-icon material-icons">home</i>
</div>
<div class="mu-appbar-title">
<span>新聞</span>
</div>
<div class="right"></div>
</div>
...
</div>
</div>
<script type="text/javascript" src="/static/js/manifest.4410c20c250c68dac5bc.js"></script>
<script type="text/javascript" src="/static/js/vendor.d55f477df6e96ccceb5c.js"></script>
<script type="text/javascript" src="/static/js/app.f199467bd568ee8a197a.js"></script>
</body>
</html>複製代碼
相比 index.html, new/index.html 中的 <div id="app"></div>
是有內容的,且 <head></head>
中多了當前路由分包的 js 文件。其他部分跟 index.html 同樣。雖然有多個 html,但從 /new 跳轉到其餘路由時,仍是單頁內跳轉的,不會有新的 html 請求。
根據上面配置的 nginx 規則,路由對應的返回文件分別是:
/ -> index.html
/new -> new/index.html
/hot -> hot/index.html
/article/:id -> index.html複製代碼
其中,/new 和 /hot 路由返回的 html 包含了對應路由的內容,從而實現預渲染。沒有配置預渲染的路由跟原來同樣,仍是訪問 /index.html,請求腳本,動態渲染。
預渲染達到了相似服務端渲染的效果。區別在於預渲染髮生在構建時,服務端渲染髮生在服務器處理請求時。
那麼 prerender-spa-plugin 是如何作到將運行時的 html 打包到文件中的呢?原理很簡單,就是在 webpack 構建階段的最後,在本地啓動一個 phantomjs,訪問配置了預渲染的路由,再將 phantomjs 中渲染的頁面輸出到 html 文件中,並創建路由對應的目錄。
查看 prerender-spa-plugin 源碼 prerender-spa-plugin/lib/phantom-page-render.js。
// 打開頁面
page.open(url, function (status) {
...
// 沒有設置捕獲鉤子時,在腳本執行完捕獲
if (
!options.captureAfterDocumentEvent &&
!options.captureAfterElementExists &&
!options.captureAfterTime
) {
// 拼接 html
var html = page.evaluate(function () {
var doctype = new window.XMLSerializer().serializeToString(document.doctype)
var outerHTML = document.documentElement.outerHTML
return doctype + outerHTML
})
returnResult(html) // 捕獲輸出
}
...
})複製代碼
默認狀況下 html 會在腳本執行完被捕獲並輸出。你也能夠指定一些鉤子,html 將會在特定時機被捕獲。
var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')
{
// ...
plugins: [
// ...
new PrerenderSpaPlugin(
path.join(__dirname, '../dist'),
[ '/new', '/hot' ],
{
// 監聽到自定事件時捕獲
// document.dispatchEvent(new Event('custom-post-render-event'))
captureAfterDocumentEvent: 'custom-post-render-event',
// 查詢到指定元素時捕獲
captureAfterElementExists: '#content',
// 定時捕獲
captureAfterTime: 5000
}
)
]
}複製代碼
本文實例中更可能是變化的數據,時效性要求比較高,不太適合預渲染的場景。若是想用預渲染來減小白屏時間,讓頁面反饋更及時的話,能夠預渲染骨架屏。
<template>
<div>
<new-list v-if="news.length > 0"></new-list>
<new-list-skeleton></new-list-skeleton>
</div>
</template>複製代碼
請求 news 數據須要必定時間,因此插件在腳本執行完捕獲的通常就是骨架屏。若是你想更靈活地指定捕獲時機,可使用自定義事件鉤子,在組件掛載且請求數據前捕獲。
{
mounted () {
document.dispatchEvent(new Event('sketelon-render-event'))
fetchNews()
}
}複製代碼
訪問頁面時,用戶首先看到預渲染的骨架屏(左圖),等待 js 加載完成後,再拉取數據渲染出正確的內容。
若是你配置了引用資源連接爲帶域名的完整路徑。
// config/index.js
module.exports = {
build: {
...
assetsPublicPath: '//www.example.com/'
},
...
}複製代碼
那麼構建時須要將域名代理到本地,不然 prerender-spa-plugin 捕獲的將會是線上的代碼。
127.0.0.1 www.example.com複製代碼
一般狀況下,動態路由如 /users/:id 不會配置預渲染,由於你無法枚舉出全部的 User ID。訪問動態路由時,服務器會返回根路由 / 的 html,因此根路由也不適合作預渲染。但根路由每每是一個網站的首頁,是訪問量最大的一個路由。經過一些 nginx 能夠解決這個問題。
location = / {
try_files /home/index.html /index.html;
}
location / {
try_files $uri $uri/ /index.html;
}複製代碼
用戶訪問 / 路由,其實是訪問了 /home/index.html,用 router 中配置的 /home 做爲首頁。/index.html 能夠做爲其餘沒有匹配到路由的響應。
預渲染是實現成本較低,效果提高明顯的性能優化方案。預渲染有它適合的場景,當你的頁面內容變化不大,又想讓它更快地呈現給用戶時,試試預渲染吧。