如果你只習慣使用 Vue 單純做客戶端渲染 (CSR),對於 Nuxt 的伺服器端渲染 (SSR) 模式還不是那麼的熟悉,你可能會在開發過程有些時候明明記得這段程式碼或第三方元件,在以前 Vue 的專案都可以直接使用,怎麼遷移到 Nuxt 就出現了 window is not defined
或 document is not defined
,那麼你可能就需要更深入了解伺服器端與客戶端渲染的流程,以及在 Nuxt 如何控制元件或程式碼僅在 Server 或 Client 執行。
當 Nuxt 使用通用渲染模式時,使用者發送一個網頁的請求,伺服器會在後端依據頁面程式碼來進行渲染 HTML 原始碼,這個過程是在後端伺服器執行的,伺服器端渲染完的 HTML 回傳至使用者瀏覽器進行繪製顯示。
當瀏覽器繪製畫面的同時,瀏覽器也會同步的開始下載相關的 Vue 程式碼準備執行,再執行之前,畫面雖然出現了,但其實是沒有交互性的,白話來說就是沒辦法進行操作及響應。
無法交互性的時間將一直持續至 Vue 程式碼準備與執行完成,相同的頁面元件與流程,都會再執行一次,讓客戶端與伺服器預先渲染出來的 DOM 進行綁定與響應,這個過程稱之為「Hydration」,至此,整個網站才算載入完成得以進行互動。
流程看似複雜,不過因為網路環境與電腦配備等級,這個 Hydration 與渲染綁定過程可能轉眼就完成了,但也可能因為網路差,下載資源時間過長,導致整個無法互動的時間拉長,甚至發生畫面閃一下的情況。
如果你有在 Nuxt 遇到過 window is not defined
或 document is not defined
這類型的錯誤訊息,多半與渲染時期有關係。前面我們提到了通用渲染,再首次請求 Nuxt 會在伺服器端依據請求的頁面進行渲染,這個渲染執行 JavaScript 的過程是在後端透過 Node.js 的引擎來執行,所以當有使用到 window
或 document
,自然就會出現沒有定義,因為這是瀏覽器才有的物件屬性,用來控制瀏覽器的 DOM、API 等行為,如果你有開發過 Node.js 的後端程式,可能也能夠理解與預料到這類型的情況。
Nuxt 中會發生上述的情況,多數是在使用第三方的元件或不熟悉執行環境而導致的,因為第三方元件你可能不清楚實際的實作為何,可能是需要依賴一些瀏覽器的 API 才能執行,例如元件是需要依賴瀏覽器視窗的寬度,但是在伺服器端可能無法透過相關 API 即時得知。
Nuxt 3 提供了一個 <ClientOnly>
元件,亦可以寫為 <client-only>
,這個元件,顧名思義可以控制被包裹的元件僅在客戶端進行渲染。
舉例來說,今天有一個第三方的日曆元件 <MyCalendar>
,因為元件的實作關係有使用到瀏覽器相關 AP,所以只能在客戶端進行使用,那麼我們可以就可以使用 <ClientOnly>
元件來包裹著。
<template>
<div>
<ClientOnly>
<MyCalendar />
</ClientOnly>
</div>
</template>
這樣就可以將 <MyCalendar>
元件設定為僅在客戶端進行渲染,首次請求頁面時伺服器端將不會渲染出包含這個元件的 HTML。
<ClientOnly>
元件中提供了一個名為 fallback 的插槽 (Slot),可以用作於在伺服器渲染的預設內容,等到客戶端載入完成才接手渲染被包裹的 <MyCalendar>
元件。
<template>
<div>
<ClientOnly>
<MyCalendar />
<template #fallback>
<p>[MyCalendar] 元件載入中...</p>
</template>
</ClientOnly>
</div>
</template>
預設情況下,我們在專案目錄 components,可以建立自訂的元件,而在元件的檔案名稱命名上,Nuxt 3 提供我們在副檔名前可以使用 .client
與 .server
,來控制元件在客戶端或伺服器端進行載入。
舉例來說,我們有一個 MyComponent.client.vue 與 MyComponent.server.vue 檔案,
./components/MyComponent.client.vue:
<template>
<div>
<p>[MyComponent]</p>
<p>這是從 Client 渲染出來的元件</p>
</div>
</template>
./components/MyComponent.client.vue:
<template>
<div>
<p>[MyComponent]</p>
<p>這是從 Server 渲染出來的元件</p>
</div>
</template>
當我們在使用 <MyComponent>
元件時,Nuxt 3 將會根據檔案名稱中的 .client
與 .server
來決定,客戶端與伺服器端應該選染哪一個元件檔案內容,以此我們就能來封裝一些僅能在客戶端使用的元件,而在伺服器端使用的元件則是渲染一些空間或提示字來等待客戶端完成載入。
更深入一點,我們在專案內所撰寫的 JavaScript 程式,也可能會遇到一些情況僅需要在伺服器端或客戶端執行,或是執行不同的程式碼,那麼我們 可以使用 process.client
或 process.server
屬性來做判斷,這在中間件或插件的使用上能輔助你撰寫不同環境的商業或控制邏輯。
在 SFC 的 script 中使用:
<script setup>
if (process.server) {
console.log('伺服器端執行的區塊!')
}
if (process.client) {
console.log('客戶端執行的區塊!')
}
</script>
當然在路由中間件也可以這樣使用:
export default defineNuxtRouteMiddleware(to => {
if (process.server) {
console.log('伺服器端跳過此路由中間件')
return
}
if (process.client) {
console.log('客戶端端跳過此路由中間件')
return
}
})
Nuxt 也提供了執行期間可以存取上下文的方法,內部的屬性更能用來判斷是否處於已經完成伺服器端的渲染或 Hydration 階段。
已經完成伺服器端的渲染:
const nuxtApp = useNuxtApp()
console.log(nuxtApp.payload.serverRendered)
是否處於 Hydration 階段:
const nuxtApp = useNuxtApp()
console.log(nuxtApp.isHydrating)
當這些屬性狀態一起組合就可以達到判斷客戶端已完成伺服器端渲染,正處於 Hydration 階段。
const nuxtApp = useNuxtApp()
if (process.client && nuxtApp.payload.serverRendered && nuxtApp.isHydrating) {
return
}
在開發 Nuxt 3 的專案時,你多少還是得了解一下預設的通用渲染 (Universal Rendering) 模式,在伺服器端或客戶端進行渲染時,都可能會面臨到一些伺服器端與客戶端無法執行相同的程式等問題,Nuxt 3 在自訂元件、插件等都支援透過檔案名稱來在不同環境選擇執行或載入不同的檔案與邏輯,該怎麼處理這些狀況或針對伺服器端或客戶端調整流程,就需要仰賴這些伺服器端或客戶端的判斷與約定。
感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱
接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。
參考資料