jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Vue.js 3 · Phần 7 — Vue Router

Định tuyến client-side với Vue Router 4: cấu hình route, route động và param, RouterLink/RouterView, điều hướng programmatic, nested route & layout, lazy-load để chia bundle, và navigation guard bảo vệ trang.

App thật có nhiều trang. Vue Router là router chính thức của Vue: nó ánh xạ URL ↔ component, không reload trang (Single-Page App). Phần này đi từ cấu hình cơ bản đến những thứ production cần: nested layout, lazy-load chia bundle, và guard bảo vệ route.


1. Cài đặt & cấu hình

npm install vue-router@4
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/pages/Home.vue';

const router = createRouter({
  history: createWebHistory(), // HTML5 history (URL sạch, không #)
  routes: [
    { path: '/', name: 'home', component: Home },
    { path: '/about', name: 'about', component: () => import('@/pages/About.vue') },
    { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/pages/NotFound.vue') },
  ],
});

export default router;
// main.ts
import router from './router';
createApp(App).use(router).mount('#app');

createWebHistory cho URL đẹp (/about) nhưng cần server fallback về index.html. Route bắt-tất-cả /:pathMatch(.*)* xử lý 404.


RouterView là chỗ component của route hiện tại render; RouterLink điều hướng không reload:

<template>
  <nav>
    <RouterLink to="/">Trang chủ</RouterLink>
    <RouterLink :to="{ name: 'about' }">Giới thiệu</RouterLink>
  </nav>
  <RouterView />  <!-- route khớp render ở đây -->
</template>

RouterLink tự thêm class router-link-active / router-link-exact-active để style link đang chọn. Ưu tiên :to="{ name: 'about' }" (theo tên) hơn chuỗi path cứng — đổi path sau này không vỡ link.


3. Route động & param

{ path: '/users/:id', name: 'user', component: () => import('@/pages/User.vue') }

Đọc param trong component bằng useRoute:

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

const route = useRoute();
const router = useRouter();

console.log(route.params.id);   // param từ URL
console.log(route.query.tab);   // ?tab=... query string
</script>

Bẫy kinh điển: khi điều hướng /users/1/users/2, cùng component được tái dùng, onMounted không chạy lại. Phản ứng đổi param bằng watch:

watch(() => route.params.id, (id) => loadUser(id), { immediate: true });

4. Điều hướng programmatic

Dùng useRouter để điều hướng từ logic (sau khi submit, đăng nhập…):

router.push('/users/1');
router.push({ name: 'user', params: { id: 1 }, query: { tab: 'posts' } });
router.replace('/login'); // không thêm vào history (không back lại được)
router.back();

push thêm vào lịch sử (back được); replace thay thế (dùng sau redirect đăng nhập để không back về form login).


5. Nested route & layout

Route lồng nhau cho UI có layout chung (sidebar, tab) bọc nội dung con:

{
  path: '/dashboard',
  component: () => import('@/layouts/DashboardLayout.vue'),
  children: [
    { path: '', name: 'dash', component: () => import('@/pages/Overview.vue') },
    { path: 'settings', component: () => import('@/pages/Settings.vue') },
  ],
}
<!-- DashboardLayout.vue -->
<template>
  <aside><!-- sidebar chung --></aside>
  <main><RouterView /></main>  <!-- route con render ở đây -->
</template>

URL /dashboard/settings render DashboardLayout chứa Settings. Layout viết một lần, dùng cho mọi trang con.


6. Lazy-load & chia bundle

Để ý component: () => import('...') ở trên — đó là dynamic import. Vite tách mỗi route thành chunk riêng, chỉ tải khi vào route đó. Đây là tối ưu hiệu năng quan trọng nhất của SPA: trang chủ không phải tải code của mọi trang.

// eager (vào bundle chính) — chỉ cho route luôn cần ngay như Home
import Home from '@/pages/Home.vue';

// lazy (chunk riêng) — mặc định cho phần còn lại
component: () => import('@/pages/Heavy.vue');

7. Navigation guard — bảo vệ route

Guard chặn/đổi hướng điều hướng — dùng cho auth, xác nhận rời trang:

// guard toàn cục — chạy trước mỗi điều hướng
router.beforeEach((to) => {
  const auth = useAuthStore(); // Pinia, Phần 8
  if (to.meta.requiresAuth && !auth.isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }; // chuyển hướng
  }
  // return true / không return → cho qua
});
// đánh dấu route cần đăng nhập qua meta
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }

Có cả guard cấp component (onBeforeRouteLeave để chặn rời form chưa lưu) và cấp route (beforeEnter). Trả về một location = redirect; trả false = hủy điều hướng.


8. Bài tập

1. Vì sao chuyển /users/1/users/2 không kích hoạt lại onMounted?

Lời giải

Cùng component được tái dùng (chỉ param đổi) nên không bị hủy/tạo lại. Phải watch(() => route.params.id, ...) để tải dữ liệu mới.

2. Khi nào dùng router.replace thay vì router.push?

Lời giải

Khi không muốn thêm bản ghi lịch sử — ví dụ sau khi đăng nhập thành công, replace /login bằng /dashboard để nút Back không quay lại form login.

3. Làm sao chuyển hướng người chưa đăng nhập về /login và quay lại đúng trang sau khi đăng nhập?

Lời giải

beforeEach: nếu to.meta.requiresAuth && !loggedInreturn { name: 'login', query: { redirect: to.fullPath } }. Sau khi login đọc route.query.redirect để push về.


Điểm chính

  • createRouter + createWebHistory; map URL ↔ component; RouterView/RouterLink.
  • Route động :id đọc qua useRoute().params; watch param vì component bị tái dùng.
  • Điều hướng programmatic bằng useRouter (push/replace/back).
  • Nested route cho layout chung; lazy-load (() => import) chia bundle theo route.
  • Navigation guard (beforeEach + meta.requiresAuth) bảo vệ trang cần đăng nhập.

Phần tiếp theo

Route guard vừa rồi tham chiếu một auth store. Phần 8 — Pinia là cách quản lý state dùng chung toàn app một cách chuẩn mực: định nghĩa store với Composition API, state/getters/actions, dùng store trong component và router, và vì sao Pinia thay thế Vuex.