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.
2. RouterLink & RouterView
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,onMountedkhông chạy lại. Phản ứng đổi param bằngwatch:
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 && !loggedIn → return { 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 quauseRoute().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.