單頁應用多路由預渲染指南總結

Prerender SPA Plugin

prerender-spa-plugin 是一個 webpack 插件用於在單頁應用中預渲染靜態 html 內容。因此,該插件限定了你的單頁應用必須使用 webpack 構建,且它是框架無關的,無論你是使用 React 或 Vue 甚至不使用框架,都能用來進行預渲染。本文示例基於 Vue.js 2.0 + vue-router。
下文會從生成項目講起,然後看下沒有配置預渲染前的樣子,再配置預渲染進行構建,對比前後的差別。

生成項目

首先生成一個項目並安裝依賴。

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,頁面至少在加載下面文件後才能被看到。

html
app.css - 樣式
manifest.js - webpack manifest
vendor.js - 第三方庫
app.js - 業務邏輯
0.js - 路由分包文件

其中 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 中的

是有內容的,且 中多了當前路由分包的 js 文件。其餘部分跟 index.html 一樣。雖然有多個 html,但從 /new 跳轉到其他路由時,還是單頁內跳轉的,不會有新的 html 請求。
根據上面配置的 nginx 規則,路由對應的返回文件分別是:

/ -> index.html
/new -> new/index.html
/hot -> hot/index.html
/article/:id -> index.htm

其中,/new 和 /hot 路由返回的 html 包含了對應路由的內容,從而實現預渲染。沒有配置預渲染的路由跟原來一樣,還是訪問 /index.html,請求腳本,動態渲染。
預渲染達到了類似服務端渲染的效果。區別在於預渲染髮生在構建時,服務端渲染髮生在服務器處理請求時。

prerender-spa-plugin 原理

那麼 prerender-spa-plugin 是如何做到將運行時的 html 打包到文件中的呢?原理很簡單,就是在 webpack 構建階段的最後,在本地啓動一個 phantomjs,訪問配置了預渲染的路由,再將 phantomjs 中渲染的頁面輸出到 html 文件中,並建立路由對應的目錄。
加粗樣式
查看 prerender-spa-plugin源碼

// 打開頁面
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) // 捕獲輸出
  }
  ...
})

預渲染骨架屏

本文實例中更多是變化的數據,時效性要求比較高,不太適合預渲染的場景。如果想用預渲染來減少白屏時間,讓頁面反饋更及時的話,可以預渲染骨架屏。

<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 加載完成後,再拉取數據渲染出正確的內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章