跳至主要內容
Skip to content

Vue Router + TypeScript 路由類型

Vue Router 4 原生支援 TypeScript。本篇將介紹路由參數、導航守衛等類型定義。


一、 基本設定

1.1 安裝

bash
npm install vue-router

1.2 定義路由

typescript
// router/index.ts
import {
  createRouter,
  createWebHistory,
  type RouteRecordRaw,
} from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: Home,
  },
  {
    path: '/about',
    name: 'about',
    component: About,
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

1.3 使用

typescript
// main.ts
import { createApp } from 'vue';
import router from './router';
import App from './App.vue';

createApp(App).use(router).mount('#app');

二、 路由參數類型

2.1 基本參數

typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'),
  },
];
vue
<!-- User.vue -->
<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();

// params 類型是 Record<string, string | string[]>
const userId = route.params.id as string;
</script>

2.2 強類型路由參數

typescript
// 定義路由名稱和參數的映射
interface RouteParams {
  home: undefined;
  user: { id: string };
  post: { id: string; slug?: string };
}

// 使用泛型函式
function useTypedRoute<T extends keyof RouteParams>() {
  const route = useRoute();
  return route as typeof route & { params: RouteParams[T] };
}

// 在元件中使用
const route = useTypedRoute<'user'>();
const userId = route.params.id; // string

三、 Query 參數

3.1 基本用法

vue
<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();

// query 類型是 Record<string, string | string[] | undefined>
const page = route.query.page as string | undefined;
const tags = route.query.tags as string[] | undefined;
</script>

3.2 類型安全的 Query

typescript
interface SearchQuery {
  keyword?: string;
  page?: string;
  sort?: 'asc' | 'desc';
}

function useSearchQuery(): SearchQuery {
  const route = useRoute();

  return {
    keyword: route.query.keyword as string | undefined,
    page: route.query.page as string | undefined,
    sort: route.query.sort as 'asc' | 'desc' | undefined,
  };
}

四、 導航

4.1 useRouter

vue
<script setup lang="ts">
import { useRouter } from "vue-router";

const router = useRouter();

function goToUser(id: number) {
  router.push({ name: "user", params: { id: String(id) } });
}

function goToSearch(keyword: string) {
  router.push({ path: "/search", query: { keyword } });
}

function goBack() {
  router.back();
}
</script>

4.2 類型安全的導航

typescript
import type { RouteLocationRaw } from 'vue-router';

// 定義應用路由
type AppRoutes =
  | { name: 'home' }
  | { name: 'user'; params: { id: string } }
  | { name: 'post'; params: { id: string }; query?: { preview?: string } };

function navigateTo(route: AppRoutes) {
  const router = useRouter();
  router.push(route as RouteLocationRaw);
}

// 使用
navigateTo({ name: 'user', params: { id: '123' } });

五、 導航守衛

5.1 全域守衛

typescript
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';

router.beforeEach(
  (
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    next: NavigationGuardNext
  ) => {
    const isAuthenticated = checkAuth();

    if (to.meta.requiresAuth && !isAuthenticated) {
      next({ name: 'login' });
    } else {
      next();
    }
  }
);

5.2 路由 Meta 類型

typescript
// 擴展 RouteMeta
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
    roles?: string[];
  }
}

// 使用
const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    name: 'dashboard',
    component: Dashboard,
    meta: {
      requiresAuth: true,
      title: 'Dashboard',
      roles: ['admin', 'user'],
    },
  },
];

// 存取 meta
router.beforeEach((to) => {
  document.title = to.meta.title ?? 'My App';

  if (to.meta.requiresAuth) {
    // 檢查認證
  }

  if (to.meta.roles) {
    // 檢查角色
  }
});

5.3 組件內守衛

vue
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";
import type { RouteLocationNormalized } from "vue-router";

onBeforeRouteLeave((to, from) => {
  const answer = window.confirm("確定要離開嗎?");
  if (!answer) return false;
});

onBeforeRouteUpdate((to: RouteLocationNormalized) => {
  // 路由參數變化時
  console.log("Route updated:", to.params);
});
</script>

六、 巢狀路由

6.1 定義巢狀路由

typescript
const routes: RouteRecordRaw[] = [
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'),
    children: [
      {
        path: '',
        name: 'user-profile',
        component: () => import('@/views/UserProfile.vue'),
      },
      {
        path: 'posts',
        name: 'user-posts',
        component: () => import('@/views/UserPosts.vue'),
      },
      {
        path: 'settings',
        name: 'user-settings',
        component: () => import('@/views/UserSettings.vue'),
        meta: { requiresAuth: true },
      },
    ],
  },
];

七、 路由組合函式

7.1 基本封裝

typescript
// composables/useRouteParams.ts
import { computed } from 'vue';
import { useRoute } from 'vue-router';

export function useRouteParams() {
  const route = useRoute();

  const id = computed(() => route.params.id as string);
  const slug = computed(() => route.params.slug as string | undefined);

  return { id, slug };
}

7.2 完整範例

typescript
// composables/useRouteMeta.ts
import { computed } from 'vue';
import { useRoute } from 'vue-router';

export function useRouteMeta() {
  const route = useRoute();

  const title = computed(() => route.meta.title as string | undefined);
  const requiresAuth = computed(() => route.meta.requiresAuth ?? false);
  const roles = computed(() => route.meta.roles as string[] | undefined);

  return { title, requiresAuth, roles };
}

八、 實用範例

8.1 完整路由配置

typescript
// router/index.ts
import {
  createRouter,
  createWebHistory,
  type RouteRecordRaw,
} from 'vue-router';

// 擴展 Meta
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
    layout?: 'default' | 'blank';
  }
}

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue'),
    meta: { title: 'Home', layout: 'default' },
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/Login.vue'),
    meta: { title: 'Login', layout: 'blank' },
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: 'Dashboard', requiresAuth: true },
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue'),
    meta: { title: '404' },
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// 全域守衛
router.beforeEach((to, from, next) => {
  document.title = to.meta.title ?? 'My App';

  if (to.meta.requiresAuth && !isAuthenticated()) {
    next({ name: 'login', query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

function isAuthenticated(): boolean {
  return !!localStorage.getItem('token');
}

export default router;

總結

類型說明
RouteRecordRaw路由配置
RouteLocationNormalized標準化路由
RouteLocationRaw導航目標
NavigationGuardNextnext 函式
RouteMeta路由 meta

> **推薦做法**:

  • 擴展 RouteMeta 介面
  • 封裝路由邏輯為組合函式
  • 使用懶加載元件

進階挑戰

  1. 實作一個路由權限控制系統
  2. 建立動態路由配置(根據使用者角色)
  3. 實作路由切換動畫

延伸閱讀與資源