使用 Vue + Nuxt 打造簡單的預先渲染網頁應用程式
原文由 Michael Lynch 于 發布,訂閱此部落格
在這篇文章中,我會示範如何使用 Vue 和 Nuxt 預先渲染頁面。這個方法結合了 Vue 便利的開發體驗,同時又不會犧牲社群分享或搜尋引擎最佳化等關鍵功能。
這篇教學假設你完全沒有 Vue 或 Nuxt 的經驗,過程中我會一一解說。
Vue 的問題
跟 Angular 和 React 一樣,Vue 是一個用來打造單頁式應用程式(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>因為是單頁式應用程式,這個 HTML 空殼在你網站上的每個頁面都是一樣的。換句話說,不管使用者造訪的是 yoursite.com/about 還是 yoursite.com/contact,伺服器傳給他們的都是同一個 HTML 空殼。JavaScript 負責在使用者瀏覽器中執行後,判斷路徑並繪製出對應的頁面。
動態頁面渲染是一項讓網站導覽變快的巧妙創新,但當你把網站串接到社群網路或搜尋引擎時,就會產生問題。
SPA 問題一:社群分享
當我在 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 就是用來解決這個問題的框架。它在 Vue 之上多加了一層,把一部分原本由瀏覽器處理的工作搬回伺服器端。Nuxt 不會只傳下一個空的 HTML 空殼、再等待客戶端的 JavaScript 來渲染所有內容,而是會在伺服器端先預先處理頁面,產生更完整的 HTML。
伺服器端渲染的問題
大多數人都是在網頁伺服器上執行 Nuxt,這就叫做「伺服器端渲染」。當使用者向伺服器請求頁面時,Nuxt 會在伺服器端即時建立好頁面,再傳送到使用者的瀏覽器。
伺服器端渲染可以減少應用程式初始載入的時間,因為你的伺服器分擔了一部分瀏覽器的工作。但如果你只是想為了社群分享和 SEO 多填幾個 HTML 標籤,就為了這樣在技術架構中加入 Nuxt 和一整台 Node.js 伺服器,實在有點小題大作。
SPA 最大的優勢之一,就是它只是靜態的 HTML、CSS 和 JavaScript,完全不需要應用程式伺服器。像 Google Cloud Storage 和 Amazon S3 這類簡單的檔案託管服務就能託管標準的 SPA。如果你使用伺服器端渲染,就必須從靜態檔案託管升級到完整的應用程式伺服器,成本更高、也更複雜。
幸好,除了伺服器端渲染之外還有另一個選擇:預先渲染。Nuxt 不會在收到 HTTP 請求時才即時渲染頁面,而是會事先把你網站上的每個頁面都渲染好。這個過程會產生靜態檔案,所以你依然可以把應用程式託管在任何能託管標準 SPA 的地方。
你該使用預先渲染嗎?
預先渲染並不適合所有情況。你需要判斷你的應用程式需要什麼,到底是需要預先渲染、伺服器端渲染,還是維持使用單純的 Vue 就好。下面我列出了一些優缺點,幫助你決定何時該採用預先渲染。
預先渲染的優點
- 讓你能為網站上的每個頁面設定獨立的社群分享卡片
- 提升頁面載入速度
- 標準的 SPA 必須等到瀏覽器下載並執行完 JavaScript 後才開始渲染頁面。採用預先渲染時,使用者在瀏覽器執行任何 JavaScript 之前就能看到你的頁面。
預先渲染的缺點
- 比標準的 Vue 更複雜
- 雖然預先渲染比在 Node 伺服器上執行 Nuxt 來得單純,但仍比執行完全在客戶端運行的 Vue 應用程式複雜。
- 使用預先渲染時,你必須在腦中釐清程式碼是在伺服器端還是客戶端執行,以及執行當下有哪些可用的執行環境。
- 無法支援使用者產生的頁面
- 預先渲染要求你在建置頁面時就知道所有的頁面路由。它明確不支援動態路由。
- 如果你的網站包含使用者產生的內容,例如你希望使用者加入後能擁有自己的網址(例如
yoursite.com/users/michael123),用預先渲染就無法做到。 - 你可以把部分路由改成 URL 查詢參數來變通(例如
yoursite.com/users?id=michael123),然後在客戶端再抓取動態資料,但你仍然無法為這些頁面產生各自獨立的社群分享標籤。
預先渲染的「Hello, world」
為了示範預先渲染,我會用僅僅三個檔案來展示一個基本的、預先渲染的「Hello, world!」應用程式。
唯一的先決條件是 Node.js。我使用的是 Node v12.13.1,也就是撰寫本文當時的最新穩定版本。
pages/index.vue
第一個檔案定義了網頁應用程式中的一個頁面。pages/ 資料夾對 Nuxt 有特殊的意義。它會為在 pages/ 資料夾中找到的每個 .vue 檔案預先渲染成各自獨立的頁面。名稱 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 上執行。
預先渲染你的應用程式
當你執行 npm run dev 時,你使用的是伺服器端渲染。Node 會執行一個本地開發伺服器,並在你請求頁面時即時產生頁面。
但我答應要給你的是預先渲染的頁面。對於預先渲染的頁面,你甚至不需要網頁伺服器,因為它就是一組靜態檔案。
要預先渲染你的應用程式,請執行以下指令:
npm run generate如果你查看 dist/ 資料夾,會看到 Nuxt 已經為你預先渲染好頁面:
$ 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 如何協作來建立預先渲染的頁面。根據你瀏覽網站的方式,你會看到兩種不同版本的頁面。

About 頁面會根據你的到達方式顯示不同的資訊。
如果你直接從 /about 頁面開始瀏覽,應該會看到左邊的版本。如果你從根頁面開始,再點擊「about page」連結,應該會看到右邊的版本。
為什麼會看到兩種不同版本的頁面?答案就在 asyncData hook 裡。這個函式會在兩個時間點執行:
- (伺服器端)當 Nuxt 預先渲染頁面時
- (客戶端)當瀏覽器從站內其他地方導覽到這個頁面時
定義如下:
asyncData() {
// Don't re-evaluate buildTime when the client loads this page in the
// browser.
if (!process.client) {
return {
buildTime: new Date().toUTCString(),
};
}
},當 Nuxt 預先渲染網站時,伺服器會執行 asyncData 方法。在伺服器環境中,process.client 是 null,因此它會把 buildTime 設為當下的時間,並在預先渲染頁面的 HTML 時使用這個變數。
當你從站內不同頁面導覽到 /about 路徑時,瀏覽器會在載入頁面時執行 asyncData 方法。此時 process.client 因為程式碼是在客戶端執行而不再是 null,所以這個方法不會定義 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>預先渲染只針對第一個頁面
About 頁面展示了預先渲染的一個細微之處:Nuxt 只會預先渲染使用者造訪的第一個頁面。之後,Vue 就會像一般的 SPA 那樣,每當使用者在站內導覽時都在客戶端重新繪製頁面。這其實是好事,代表你的應用程式既能保留 Vue 瞬間切換頁面的導覽體驗,又不會犧牲與需要伺服器端渲染的服務的相容性。
在本機執行 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 是使用伺服器端渲染來即時建立頁面的。

npm run dev 會在使用者請求時才渲染頁面,因此建置時間和載入時間會一致。
跟只產生一次頁面並持續提供同一份頁面的預先渲染不同,伺服器端渲染會在使用者每次造訪時都產生一份全新的頁面。
發布你的應用程式
使用預先渲染時,你不需要 Node.js 伺服器來託管應用程式。你只需要一個支援靜態檔案託管的託管服務即可。
以下是幾個熱門平台發布靜態檔案的操作說明:
原始碼
這個範例的所有程式碼都已在 GitHub 上以 MIT 授權釋出:
功能更豐富的範例
如果你要用 Vue 和 Nuxt 打造實際可用的應用程式,你會需要比兩個預先渲染頁面更多的功能。我建立了一個樣板專案 pre-vue,裡面包含了 SEO 和社群分享所需的所有樣板程式碼:
它具備以下功能:
- 產生
robots.txt檔案 - 產生 sitemap
- 為每個頁面支援獨立的
<title>標籤及其他與 SEO 相關的<meta>標籤 - 為每個頁面加入獨立的 Open Graph 標籤
- 加入 Google Analytics 支援
- 加入 favicon
- 處理 404 錯誤
我使用 pre-vue 樣板重寫了 Zestful 展示網站,它原本是一個 Angular SPA。README 中說明了如何使用 pre-vue,如果大家有興趣,我會再發布一篇詳細解說細節的部落格文章。
隨機一篇部落格
留言
登入後參與討論