A Simple Pre-Rendered Web App Using Vue + Nuxt

Michael Lynch

使用 Vue + Nuxt 打造簡單的預渲染網頁應用程式

在這篇文章中,我將示範如何使用 Vue 與 Nuxt 進行 Pre-rendering(預先渲染)頁面。這種做法結合了 Vue 便利的開發體驗,同時又不會犧牲社群分享或 Search Engine Optimization(搜尋引擎最佳化)等關鍵功能。

本教學不需要具備 Vue 或 Nuxt 的經驗,我會在過程中詳細說明一切。

Vue 的問題

如同 Angular 與 React,Vue 是一個用於打造 Single-Page App(單頁應用程式)(SPA)的框架。傳統網站會在使用者點擊站內連結時,迫使瀏覽器重新下載整個新頁面,而 SPA 則將所有內容保留在單一頁面上。當使用者在你的網站中瀏覽時,JavaScript 只需重新繪製新頁面,而不需要再次從伺服器下載所有內容。這省去了使用者瀏覽器與網頁伺服器之間緩慢的網路請求,帶來快速、流暢的使用者體驗。

Vue 帶來的高反應速度,其代價是你對頁面初始 HTML 的掌控較少。當瀏覽器從伺服器擷取 SPA 時,會收到類似以下的 HTML:

<html>
<head>
  <title>My Awesome Website</title>
</head>
<body>
  <div id="app"></div>
  <!-- app.js populates the rest of the page after the browser executes the script. -->
  <script type="text/javascript" src="app.js">
</body>
</html>

因為它是 Single-Page App,網站上每個頁面的 HTML 存根都相同。換句話說,無論使用者造訪 yoursite.com/about 還是 yoursite.com/contact,伺服器傳送給他們的都是同一份 HTML 存根。JavaScript 會在瀏覽器中執行後,負責判斷路徑並繪製對應的頁面。

動態頁面渲染是一項讓網站導覽更快速的巧妙創新,但當你將網站與社群網路或搜尋引擎串接時,就會產生問題。

SPA 問題一:社群分享

當我在 Twitter 上分享部落格文章時,呈現的樣子是這樣:

豐富的 Twitter 卡片範例

使用 Open Graph 標籤,讓 Twitter 為我的文章產生豐富卡片。

Twitter 是根據頁面中遵循 Open Graph 標準的 HTML 標籤來產生該卡片。例如,若要指定卡片中的圖片,我會加入如下的標籤:

<meta property="og:image" content="https://mtlynch.io/post-42/cover.jpg" />

如果你的網站是 SPA,那麼所有頁面都會共用相同的 HTML 骨架,也因此共用相同的 Open Graph 標籤。像 Twitter 和 Facebook 這類主流社群網路,都要求 Open Graph 標籤在任何 JavaScript 執行之前就已存在。結果就是,你無法為網站上不同的頁面建立各自獨立的 Twitter 或 Facebook 卡片。

SPA 問題二:搜尋引擎最佳化(SEO)

與社群網站不同,搜尋引擎確實會使用 JavaScript 來渲染網站,問題在於它們無法完美地做到

許多網站會使用 JavaScript 在使用者瀏覽時持續更新頁面內容。從 Google 的角度來看,頁面何時才算「渲染完成」並可供索引?對於一般的 SPA,Google 會嘗試索引你的頁面,但你無法保證它會正確索引。

Nuxt 來救援

在現代的網路環境中,社群網路與 SEO 都相當重要,因此如果使用 Vue 就意味著你的應用程式無法與這些服務完整整合,那將會非常可惜。

Nuxt.js 標誌

Nuxt 是一個解決此問題的框架。它在 Vue 之上增加了一層,將原本由瀏覽器處理的部分工作移回伺服器端。Nuxt 不再是先傳送空的 HTML 存根、再等待客戶端 JavaScript 渲染全部內容,而是會在伺服器端預先處理頁面,產生更完整的 HTML。

Server-Side Rendering(伺服器端渲染)的問題

大多數人是在網頁伺服器上執行 Nuxt,這種方式稱為「Server-Side Rendering」。當使用者向伺服器請求頁面時,Nuxt 會在伺服器端即時組建頁面,再傳送給使用者的瀏覽器。

Server-Side Rendering 減少了應用程式初始頁面的載入時間,因為伺服器分擔了部分瀏覽器的工作。但如果你只是想為社群分享與 SEO 填入幾個 HTML 標籤,卻要為此在技術堆疊中加入 Nuxt 與整個 Node.js 伺服器,就顯得有點小題大作了。

SPA 最大的優勢之一在於它們僅由靜態的 HTML、CSS 與 JavaScript 組成,完全不需要應用程式伺服器。像 Google Cloud Storage 與 Amazon S3 這類簡易的檔案代管服務就能託管標準的 SPA。如果你使用 Server-Side Rendering,就必須從靜態檔案代管升級到完整的應用程式伺服器,成本更高、也更複雜。

幸好,除了 Server-Side Rendering 之外,還有另一種選擇:Pre-rendering。Nuxt 不會在收到 HTTP 請求時才即時渲染頁面,而是預先將網站上的每個頁面都渲染好。這個過程會產生靜態檔案,因此你仍然可以在任何能託管標準 SPA 的地方託管你的應用程式。

你該使用 Pre-rendering 嗎?

Pre-rendering 並不適用於所有情境。你需要評估應用程式的需求,判斷究竟需要 Pre-rendering、Server-Side Rendering,還是維持使用單純的 Vue 就好。以下我列出了一些優缺點,幫助你決定何時採用 Pre-rendering。

Pre-rendering 的優點

  • 讓網站上的每個頁面都能擁有獨立的社群分享卡片
  • 提升頁面載入速度
    • 標準的 SPA 必須等到瀏覽器下載並執行 JavaScript 後才開始渲染頁面。使用 Pre-rendering 時,使用者在瀏覽器執行任何 JavaScript 之前就能看到你的頁面。

Pre-rendering 的缺點

  • 比起標準的 Vue 增加了複雜度
    • 雖然 Pre-rendering 比在 Node 伺服器上執行 Nuxt 來得簡單,但仍比執行完全在客戶端運行的 Vue 應用程式複雜。
    • 使用 Pre-rendering 時,你必須在腦中釐清程式碼是在伺服器端還是客戶端執行,以及在程式碼執行當下有哪些可用的執行環境。
  • 無法支援使用者產生的頁面
    • Pre-rendering 要求你在建置頁面時就必須知道所有的頁面路由,它明確地不支援動態路由
    • 如果你的網站包含使用者產生的內容,例如希望使用者加入後能擁有自己的網址(例如 yoursite.com/users/michael123),使用 Pre-rendering 就無法實現。
    • 你可以將部分路由改為 URL 查詢參數來迴避這個限制(例如 yoursite.com/users?id=michael123),再於客戶端下載動態資料,但仍無法為這些頁面產生各自獨立的社群分享標籤。

預先渲染的「Hello, world」

為了示範 Pre-rendering,我將用僅僅三個檔案來展示一個基本的預先渲染「Hello, world!」應用程式。

唯一的先決條件是 Node.js。我使用的是 Node v12.13.1,這是撰寫本文當時的最新穩定版本。

pages/index.vue

第一個檔案定義了網頁應用程式中的一個頁面。pages/ 資料夾對 Nuxt 具有特殊意義,它會為在 pages/ 資料夾中找到的每個 .vue 檔案 Pre-rendering 各自獨立的頁面。index.vue 這個名稱表示這是根頁面,也就是使用者未指定任何路徑時所看到的頁面。

index.vue 會產生一個簡單的「Hello, world!」頁面,顯示歡迎訊息與一個按鈕。為了展示 Vue 的部分客戶端功能,按鈕會在使用者每次點擊時更新其文字。

<template>
  <div>
    <h1>Hello, world!</h1>
    <p>I'm an example of a pre-rendered Vue webpage.</p>
    <button v-on:click="count++">I have been clicked {{ count }} times</button>
  </div>
</template>

<script>
  export default {
    data: function () {
      return {
        count: 0,
      };
    },
  };
</script>

package.json

package.json 檔案會告訴 Node.js 如何建置這個應用程式:

{
  "name": "hello-world-vue-pre-rendered",
  "dependencies": {
    "nuxt": "latest"
  },
  "scripts": {
    "dev": "nuxt --port 3600",
    "generate": "nuxt generate"
  }
}

nuxt.config.js

最後,Nuxt 需要一個設定檔,即使它是空的:

// Even though we have no Nuxt settings, this file is required.

執行「Hello, world」

你可以在 Codesandbox 上執行此應用程式:

或者,你也可以在本地端機器上透過以下指令執行應用程式:

git clone https://github.com/mtlynch/hello-world-vue-pre-rendered.git
cd hello-world-vue-pre-rendered
git checkout step-1

npm install
npm run dev

應用程式將在 http://localhost:3600 上執行。

Pre-rendering 你的應用程式

當你執行 npm run dev 時,你使用的是 Server-Side Rendering。Node 會執行一個本地開發伺服器,並在你請求時即時產生頁面。

但我答應過要給你預先渲染的頁面。使用預先渲染的頁面時,你甚至不需要網頁伺服器,因為它就是一組靜態檔案。

若要 Pre-rendering 你的應用程式,請執行以下指令:

npm run generate

如果你查看 dist/ 資料夾,會發現 Nuxt 已經為你的頁面完成 Pre-rendering:

$ find ./dist/ -type f
./dist/.nojekyll
./dist/200.html
./dist/index.html
./dist/_nuxt/7cef7880379068a94897.js
./dist/_nuxt/b10d0692e6306468ee9f.js
./dist/_nuxt/cae55ee8b1125819f113.js
./dist/_nuxt/ee10340617a3beab9da2.js
./dist/_nuxt/LICENSES

你可以透過簡易的 HTTP 伺服器來檢視這些檔案,例如 Python2 的 SimpleHTTPServer:

cd dist
python -m SimpleHTTPServer 8123

接著 Python 會啟動一個網頁伺服器,讓你可以在 http://localhost:8123 檢視預先渲染的應用程式。稍後我會說明如何發布此應用程式至靜態檔案代管服務。

新增 About 頁面

為了讓範例更有趣,我將為這個應用程式新增第二個頁面。

pages/about.vue

這個頁面使用 Vue 的 Hook 來顯示頁面渲染方式的相關資訊。我會在下方更詳細地解釋程式碼。

<template>
  <div>
    <h1>About this Build</h1>
    <p v-if="buildTime">
      Nuxt pre-rendered this page at
      <b>{{ buildTime }}</b> (before the browser ever saw it).
    </p>
    <template v-else>
      <p>
        Vue generated this page client-side because you navigated here from
        another route on the same site.
      </p>
      <p>
        <a href="/about">Refresh the page</a> to see the pre-rendered version.
      </p>
    </template>
    <p>
      The browser loaded this page at
      <b>{{ loadTime }}</b>.
    </p>
    <p><nuxt-link to="/">Home</nuxt-link></p>
  </div>
</template>

<script>
  export default {
    asyncData() {
      // Don't re-evaluate buildTime when the client loads this page in the
      // browser.
      if (!process.client) {
        return {
          buildTime: new Date().toUTCString(),
        };
      }
    },
    // Vue evaluates data variables at page render time and again every time the
    // browser loads this page.
    data: function () {
      return {
        loadTime: new Date().toUTCString(),
      };
    },
  };
</script>

以下是 About 頁面的即時版本

了解 About 頁面的兩種版本

About 頁面展示了 Nuxt 與 Vue 如何協作來建立 Pre-rendered 頁面。根據你在網站中的瀏覽方式,你會看到兩種不同版本的頁面。

About 頁面不同版本的螢幕截圖

About 頁面會根據你進入頁面的方式顯示不同的資訊。

如果你直接從 /about 頁面開始,會看到左側的版本;如果你從根頁面開始,再點擊「about page」連結,則會看到右側的版本。

為什麼會看到兩種不同版本的頁面?答案在於 asyncData Hook。這個函式會在兩個時機點執行:

  1. (伺服器端)當 Nuxt Pre-rendering 頁面時
  2. (客戶端)當瀏覽器從站內其他位置導覽至此頁面時

以下是定義的內容:

asyncData() {
  // Don't re-evaluate buildTime when the client loads this page in the
  // browser.
  if (!process.client) {
    return {
      buildTime: new Date().toUTCString(),
    };
  }
},

當 Nuxt Pre-rendering 整個網站時,伺服器會執行 asyncData 方法。在伺服器環境中,process.client 為 null,因此它會將 buildTime 設為當下時間,並在 Pre-rendering 頁面的 HTML 時使用該變數。

當你從網站上的其他頁面導覽至 /about 路徑時,瀏覽器會在頁面載入時執行 asyncData 方法。此時 process.client 因為程式碼是在客戶端執行而為非空值,因此該方法不會定義 buildTime,Vue 則會渲染 buildTime 為未定義時的頁面樣板:

<p v-if="buildTime">...</p>
<template v-else>
  <p>
    Vue generated this page client-side because you navigated here from another
    route on the same site.
  </p>
  <p><a href="/about">Refresh the page</a> to see the pre-rendered version.</p>
</template>

Pre-rendering 僅適用於第一個頁面

About 頁面展示了 Pre-rendering 的一個細微之處:Nuxt 只會 Pre-rendering 使用者造訪的第一個頁面。之後,Vue 就會像一般的 SPA 那樣,在使用者於站內導覽時於客戶端重新繪製頁面。這是件好事,代表你的應用程式仍保有 Vue 即時的頁面切換體驗,同時又不犧牲與需要 Server-Side Rendering 的服務之相容性。

在本地端執行 About 頁面

若要試驗 About 頁面,請執行以下指令

git clone https://github.com/mtlynch/hello-world-vue-pre-rendered.git
cd hello-world-vue-pre-rendered

npm install
npm run dev

你會注意到,當你瀏覽至 https://localhost:3600/about 時,建置時間與載入時間大致相同。這是因為當你執行 npm run dev 時,Nuxt 會使用 Server-Side Rendering 即時建立頁面。

使用 Server-Side Rendering 渲染的 About 頁面螢幕截圖

npm run dev 會在使用者請求時才渲染頁面,因此建置時間與載入時間會一致。

與 Pre-rendering 產生一次頁面後就持續提供同一份頁面不同,Server-Side Rendering 會在使用者每次造訪時都產生全新版本的頁面。

發布你的應用程式

使用 Pre-rendering 時,你不需要 Node.js 伺服器來託管應用程式,你只需要支援靜態檔案代管的服務即可。

以下是在幾個熱門服務上發布靜態檔案的說明:

原始碼

本範例的所有程式碼皆已在 GitHub 上以 MIT 授權條款釋出:

功能更豐富的範例

如果你正在使用 Vue 與 Nuxt 打造實際的應用程式,你會需要比兩個 Pre-rendered 頁面更多的功能。我建立了一個樣板專案 pre-vue,其中包含了 SEO 與社群分享所需的所有樣板程式碼:

它具備以下功能:

  • 產生 robots.txt 檔案
  • 產生網站地圖
  • 為每個頁面支援獨立的 <title> 標籤與其他與 SEO 相關的 <meta> 標籤
  • 為每個頁面加入獨立的 Open Graph 標籤
  • 加入 Google Analytics 支援
  • 加入網站圖示
  • 處理 404 錯誤

我使用 pre-vue 樣板重寫了 Zestful 展示網站,該網站原本是一個 Angular SPA。README 說明了如何使用 pre-vue,如果大家有興趣,我會再發布一篇詳細解說細節的部落格文章。

原文由 Michael Lynch 發布

本文章由 muse-spark-1.2-contributor 進行翻譯