
Nuxt3 提供了強大而靈活的路由系統,結合 TypeScript 的靜態類型檢查,可以大大提高開發效率和代碼質量。本文將深入探討 Nuxt3 中的路由管理和中間件(Middleware)的使用,並結合先前學習的概念,如 Pinia、Zod、Vee-Validate 等,構建一個健壯的路由系統。我們還將討論 Nuxt3 的生命週期、cookies 的應用,以及如何處理客戶端存儲和 hydration 問題。
Nuxt3 使用基於文件系統的路由,這意味著你在 pages 目錄中創建的 Vue 組件會自動映射到相應的路由。
在 pages 目錄下創建以下文件結構:
pages/
  index.vue
  about.vue
  users/
    [id].vue
或是使用指令創建
bunx nuxi add page index
bunx nuxi add page about
bunx nuxi add page users/[id]
這將自動生成以下路由:
/: 對應 index.vue
/about: 對應 about.vue
/users/:id: 對應 users/[id].vue
Nuxt3 支持兩種類型的中間件:全局中間件和路由中間件。
在 server/middleware 目錄下創建 auth.ts:
import { defineEventHandler, parseCookies } from 'h3'
import * as zod from 'zod'
const userSchema = zod.object({
  id: zod.number(),
  role: zod.enum(['user', 'admin'])
})
export default defineEventHandler((event) => {
  const cookies = parseCookies(event)
  const userCookie = cookies.user
  if (!userCookie) {
    return
  }
  try {
    const user = userSchema.parse(JSON.parse(userCookie))
    event.context.user = user
  } catch (error) {
    console.error('Invalid user data in cookie')
  }
})
在 middleware 目錄下創建 auth.ts:
import { useUserStore } from '~/stores/userStore'
import { storeToRefs } from 'pinia'
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  const { user } = storeToRefs(userStore)
  if (!user.value && to.path !== '/login') {
    return navigateTo('/login')
  }
})
在 pages 目錄下的 Vue 組件中使用中間件:
<script setup lang="ts">
definePageMeta({
  middleware: ['auth']
})
</script>
<template>
  <div>
    <h1>Protected Page</h1>
  </div>
</template>
在 stores 目錄下創建 userStore.ts:
import { defineStore } from 'pinia'
import * as zod from 'zod'
const userSchema = z.object({
  id: zod.number(),
  name: zod.string(),
  email: zod.string().email(),
  role: zod.enum(['user', 'admin'])
})
type User = zod.infer<typeof userSchema>
export const useUserStore = defineStore('user', () => {
  const user = ref<UserSchema | null>(null);
  // methods::
  const fetchUser = async (): Promise<void> => {
    const response = await $fetch('/api/user', {
      method: 'GET',
    });
    const validator = userSchema.safeParse(response);
    if (!validator.success) {
      throw new TypeError('validator error');
    }
    user.value = validator.data;
  };
  const logout = (): void => {
    user.value = null;
  };
  return {
    // state::
    user,
    // methods::
    fetchUser,
    logout,
  }
});
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot));
}
在 composables 目錄下創建 useAuth.ts:
import { useUserStore } from '~/stores/userStore'
import { useLocalStorage } from '@vueuse/core'
export function useAuth() {
  const userStore = useUserStore()
  const isAuthenticated = computed(() => !!userStore.user)
  const login = async (email: string, password: string) => {
    // 在實際應用中,這裡應該調用登錄 API
    await userStore.fetchUser()
    if (process.client) {
      useLocalStorage('auth_token', 'fake_token')
    }
  }
  const logout = () => {
    userStore.logout()
    if (process.client) {
      useLocalStorage('auth_token', null)
    }
  }
  return {
    isAuthenticated,
    login,
    logout,
  }
}
在 pages/profile.vue 中:
<template>
  <div>
    <h1>Welcome, {{ user?.name }}</h1>
    <p>Theme: {{ theme }}</p>
    <button @click="toggleTheme">Toggle Theme</button>
  </div>
</template>
<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
const theme = useState('theme', () => 'light')
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
const themeCookie = useCookie('theme')
watch(theme, (newTheme) => {
  themeCookie.value = newTheme
})
onMounted(() => {
  if (themeCookie.value) {
    theme.value = themeCookie.value
  }
})
</script>
在需要客戶端渲染的組件中使用 <ClientOnly> 包裹:
<template>
  <div>
    <h1>User Profile</h1>
    <ClientOnly>
      <pre>{{ userJson }}</pre>
      <template #fallback>
        <p>Loading user data...</p>
      </template>
    </ClientOnly>
  </div>
</template>
<script setup lang="ts">
import { useUserStore } from '~/stores/userStore'
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
const userJson = computed(() => JSON.stringify(user.value, null, 2))
</script>
Nuxt3 提供了幾個關鍵的生命週期鉤子:
useHead: 用於管理頁面的 head 信息useFetch: 用於在組件加載前獲取數據onMounted: 在組件掛載後執行onUnmounted: 在組件卸載前執行在 pages/users/[id].vue 中:
<script setup lang="ts">
const route = useRoute()
const userId = computed(() => route.params.id as string)
useHead({
  title: computed(() => `User ${userId.value}`),
})
const { data: user, pending, error } = await useFetch(`/api/users/${userId.value}`)
onMounted(() => {
  console.log('Component mounted')
})
onUnmounted(() => {
  console.log('Component will unmount')
})
</script>
<template>
  <div>
    <h1>User Details</h1>
    <p v-if="pending">Loading...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <div v-else>
      <p>Name: {{ user.name }}</p>
      <p>Email: {{ user.email }}</p>
    </div>
  </div>
</template>
在本文中,我們深入探討了 Nuxt3 中的路由管理和中間件的使用,並結合了 TypeScript、Pinia、等技術來構建一個強大而靈活的路由系統。我們還討論了如何處理 cookies、客戶端存儲和 hydration 問題,以及如何利用 Nuxt3 的生命週期鉤子來優化應用性能。
通過實施這些最佳實踐,開發者可以充分利用 Nuxt3 的強大功能,構建出類型安全、高效且易於維護的現代 Web 應用。同時,整合諸如 Pinia ,進一步增強了應用的可靠性和開發效率。
在處理客戶端特定的操作時,始終要考慮服務器端渲染(SSR)的環境,並適當使用 <ClientOnly> 組件和條件性的代碼執行來避免潛在的 hydration 問題。此外,利用 Nuxt3 提供的 useFetch 和 $fetch 方法進行數據獲取,可以確保更好的性能和一致性。
通過掌握這些概念和技術,你將能夠在 Nuxt3 中創建出富有表現力和高度互動的路由系統,為用戶提供流暢的導航體驗。