[Lv3] SSR 實作難題與解決方案
在 SSR 實作過程中,常見的難題與解決方案:Hydration Mismatch、環境變數處理、第三方套件相容性、效能優化等。
📋 面試情境題
Q: 在實作 SSR 時, 有遇到哪些難題?如何解決?
這是面試中經常會被問到的問題,面試官想了解:
- 實際經驗:是否真的實作過 SSR
- 問題解決能力:遇到問題時的思考過程
- 技術深度:對 SSR 原理的理解程度
- 最佳實踐:是否有採用業界標準做法
難題 1:Hydration Mismatch 錯誤
問題描述
錯誤訊息:
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content.
發生原因:
- Server Side 渲染的 HTML 與 Client Side 渲染的 HTML 不一致
- 常見於使用瀏覽器專用 API(
window、document、localStorage等) - 時間相關的內容(如:
new Date())在 Server 和 Client 執行時間不同
解決方案
方案 1: 使用 ClientOnly 組件
適用場景: 組件只在客戶端渲染
<template>
<div>
<h1>主要內容(SSR)</h1>
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<div>載入中...</div>
</template>
</ClientOnly>
</div>
</template>
優點:
- ✅ 簡單直接
- ✅ Nuxt 內建支援
缺點:
- ⚠️ 該部分內容不會在 SSR 中渲染
- ⚠️ 可能影響 SEO
方案 2: 使用 process.client 檢查
適用場景: 條件式渲染客戶端專用內容
<script setup lang="ts">
const userAgent = ref('');
onMounted(() => {
// 只在客戶端執行
if (process.client) {
userAgent.value = navigator.userAgent;
}
});
</script>
<template>
<div>
<p v-if="userAgent">User Agent: {{ userAgent }}</p>
</div>
</template>
關鍵點:
- ✅ 使用
process.client檢查執行環境 - ✅ 避免在 Server Side 存取瀏覽器 API
方案 3: 使用 onMounted Hook
適用場景: 需要在客戶端執行的初始化邏輯
<script setup lang="ts">
const isClient = ref(false);
onMounted(() => {
isClient.value = true;
// 客戶端專用的初始化邏輯
initializeClientOnlyFeature();
});
</script>
<template>
<div>
<div v-if="isClient">客戶端內容</div>
<div v-else>伺服器端內容(或載入中)</div>
</div>
</template>
方案 4: 統一 Server 和 Client 的資料來源
適用場景: 時間、隨機數等會導致不一致的內容
<script setup lang="ts">
// ❌ 錯誤:Server 和 Client 時間不同
const currentTime = new Date().toISOString();
// ✅ 正確:從 API 取得統一時間
const { data: serverTime } = await useFetch('/api/time');
const currentTime = serverTime.value;
</script>
面試回答範例
在實作 SSR 時,最常遇到的是 Hydration Mismatch 錯誤。這通常發生在使用瀏覽器專用 API 時,例如
window、localStorage等。我的解決方式是:首先,使用ClientOnly組件包裹只在客戶端渲染的內容;其次,使用process.client檢查執行環境,避免在 Server Side 存取瀏覽器 API;最後,對於時間、隨機數等會導致不一致的內容,統一從 Server Side API 取得,確保 Server 和 Client 的資料一致。
難題 2:環境變數處理
問題描述
問題情境:
- Server Side 和 Client Side 需要不同的環境變數
- 敏感資訊(如:API Key)不應該暴露到客戶端
- 需要區分開發、測試、生產環境
解決方案
方案 1: 使用 Nuxt 的環境變數
Server Side 環境變數:
// .env
NUXT_API_SECRET_KEY=secret_key_123
NUXT_DATABASE_URL=postgresql://...
// server/api/example.ts
export default defineEventHandler(async (event) => {
// 只在 Server Side 可用
const secretKey = process.env.NUXT_API_SECRET_KEY;
// ...
});
Client Side 環境變數:
// .env
NUXT_PUBLIC_API_URL=https://api.example.com
// 客戶端可以使用
const apiUrl = useRuntimeConfig().public.apiUrl;
關鍵點:
- ✅
NUXT_開頭的變數在 Server Side 可用 - ✅
NUXT_PUBLIC_開頭的變數在 Client Side 也可用 - ✅ 敏感資訊使用
NUXT_(不帶PUBLIC)
方案 2: 使用 useRuntimeConfig
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 私有變數(只在 Server Side 可用)
apiSecret: process.env.API_SECRET,
// 公開變數(Client Side 也可用)
public: {
apiUrl: process.env.PUBLIC_API_URL || 'https://api.example.com',
},
},
});
// 使用方式
const config = useRuntimeConfig();
// Server Side: config.apiSecret 可用
// Client Side: config.public.apiUrl 可用
面試回答範例
在 SSR 專案中,環境變數的處理很重要。我使用 Nuxt 的環境變數機制:
NUXT_開頭的變數只在 Server Side 可用,用於存放敏感資訊如 API Key、資料庫連線等;NUXT_PUBLIC_開頭的變數在 Client Side 也可用,用於存放公開資訊如 API URL。同時使用useRuntimeConfig統一管理,確保環境變數的正確使用。
難題 3:第三方套件不支援 SSR
問題描述
問題情境:
- 某些第三方套件( 如:圖表庫、動畫庫)不支援 SSR
- 直接使用會導致 Hydration Mismatch
- 需要找到 SSR 相容的替代方案
解決方案
方案 1: 使用 ClientOnly 包裹
<template>
<ClientOnly>
<ChartComponent :data="chartData" />
<template #fallback>
<div class="chart-skeleton">載入中...</div>
</template>
</ClientOnly>
</template>
方案 2: 動態導入(Dynamic Import)
<script setup lang="ts">
const ChartComponent = ref(null);
onMounted(async () => {
// 只在客戶端動態導入
if (process.client) {
const module = await import('chart-library');
ChartComponent.value = module.default;
}
});
</script>
<template>
<component v-if="ChartComponent" :is="ChartComponent" :data="chartData" />
</template>
方案 3: 使用 SSR 相容的替代方案
範例:圖表庫選擇
// ❌ 不支援 SSR
import Chart from 'chart.js';
// ✅ 支援 SSR
import { Chart } from 'vue-chartjs';
// 或使用其他 SSR 相容的圖表庫
面試回答範例
在實作 SSR 時,遇到某些第三方套件不支援 SSR 的問題。我的解決方式是:首先,使用
ClientOnly組件包裹不支援 SSR 的組件,並提供 fallback 內容;其次,對於複雜的套件,使用動態導入(Dynamic Import)在客戶端載入;最後,優先選擇 SSR 相容的替代方案,例如使用vue-chartjs而非直接使用chart.js。
難題 4:Cookie 和 Header 處理
問題描述
問題情境:
- Server Side 需要讀取 Cookie 進行身份驗證
- 需要將 Cookie 傳遞給 API 請求
- Client Side 和 Server Side 的 Cookie 處理方式不同
解決方案
方案 1: 使用 useCookie
// 讀取 Cookie
const token = useCookie('auth-token');
// 設定 Cookie
token.value = 'new-token-value';
// 設定 Cookie 選項
const token = useCookie('auth-token', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 天
});
關鍵點:
- ✅
useCookie在 Server Side 和 Client Side 都可使用 - ✅ 自動處理 Cookie 的讀寫
- ✅ 支援 Cookie 選項設定
方案 2: 在 Server API 中讀取 Cookie
// server/api/user.ts
export default defineEventHandler(async (event) => {
// 讀取 Cookie
const token = getCookie(event, 'auth-token');
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized',
});
}
// 使用 token 驗證使用者
const user = await verifyToken(token);
return user;
});