Mở khóa giao diện người dùng linh hoạt và có khả năng mở rộng trong Next.js. Hướng dẫn toàn diện của chúng tôi bao gồm Route Groups để tổ chức và Parallel Routes cho các dashboard phức tạp. Nâng cao kỹ năng ngay!
Làm chủ Next.js App Router: Phân tích chuyên sâu về kiến trúc Route Groups và Parallel Routes
Sự ra mắt của Next.js App Router đã đánh dấu một sự thay đổi mô hình trong cách các nhà phát triển xây dựng ứng dụng web với framework React nổi tiếng. Chuyển đổi khỏi các quy ước dựa trên tệp của Pages Router, App Router đã giới thiệu một mô hình mạnh mẽ, linh hoạt và lấy máy chủ làm trung tâm hơn. Sự phát triển này cho phép chúng ta tạo ra các giao diện người dùng có hiệu suất cao và cực kỳ phức tạp với khả năng kiểm soát và tổ chức tốt hơn. Trong số các tính năng mang tính chuyển đổi được giới thiệu, nổi bật nhất là Route Groups và Parallel Routes.
Đối với các nhà phát triển muốn xây dựng các ứng dụng cấp doanh nghiệp, việc thành thạo hai khái niệm này không chỉ có lợi—mà còn là điều cần thiết. Chúng giải quyết các thách thức kiến trúc phổ biến liên quan đến quản lý layout, tổ chức route, và việc tạo ra các giao diện động, đa bảng điều khiển như dashboard. Hướng dẫn này cung cấp một khám phá toàn diện về Route Groups và Parallel Routes, đi từ các khái niệm nền tảng đến các chiến lược triển khai nâng cao và các phương pháp hay nhất cho cộng đồng nhà phát triển toàn cầu.
Tìm hiểu về Next.js App Router: Ôn tập nhanh
Trước khi chúng ta đi sâu vào chi tiết, hãy cùng ôn lại ngắn gọn các nguyên tắc cốt lõi của App Router. Kiến trúc của nó được xây dựng trên một hệ thống dựa trên thư mục, nơi các thư mục xác định các phân đoạn URL. Các tệp đặc biệt trong các thư mục này xác định giao diện người dùng và hành vi cho phân đoạn đó:
page.js
: Thành phần UI chính cho một route, làm cho nó có thể truy cập công khai.layout.js
: Một thành phần UI bao bọc các layout hoặc page con. Nó rất quan trọng để chia sẻ UI qua nhiều route, như header và footer.loading.js
: Một UI tùy chọn để hiển thị trong khi nội dung trang đang tải, được xây dựng trên React Suspense.error.js
: Một UI tùy chọn để hiển thị trong trường hợp có lỗi, tạo ra các ranh giới lỗi (error boundaries) mạnh mẽ.
Cấu trúc này, kết hợp với việc sử dụng mặc định các React Server Components (RSC), khuyến khích một phương pháp tiếp cận ưu tiên máy chủ (server-first) có thể cải thiện đáng kể hiệu suất và các mẫu tìm nạp dữ liệu. Route Groups và Parallel Routes là các quy ước nâng cao được xây dựng trên nền tảng này.
Giải mã Route Groups: Tổ chức dự án của bạn để dễ quản lý và mở rộng
Khi một ứng dụng phát triển, số lượng route có thể trở nên khó quản lý. Bạn có thể có một bộ các trang cho marketing, một bộ khác cho xác thực người dùng, và một bộ thứ ba cho dashboard ứng dụng cốt lõi. Về mặt logic, đây là các phần riêng biệt, nhưng làm thế nào để bạn tổ chức chúng trong hệ thống tệp của mình mà không làm lộn xộn URL? Đây chính xác là vấn đề mà Route Groups giải quyết.
Route Groups là gì?
Một Route Group là một cơ chế để tổ chức các tệp và phân đoạn route của bạn thành các nhóm logic mà không ảnh hưởng đến cấu trúc URL. Bạn tạo một route group bằng cách đặt tên thư mục trong dấu ngoặc đơn, ví dụ: (marketing)
hoặc (app)
.
Tên thư mục trong dấu ngoặc đơn hoàn toàn chỉ dành cho mục đích tổ chức. Next.js hoàn toàn bỏ qua nó khi xác định đường dẫn URL. Ví dụ, tệp tại app/(marketing)/about/page.js
sẽ được phục vụ tại URL /about
, chứ không phải /(marketing)/about
.
Các trường hợp sử dụng chính và lợi ích của Route Groups
Mặc dù tổ chức đơn giản là một lợi ích, sức mạnh thực sự của Route Groups nằm ở khả năng phân chia ứng dụng của bạn thành các phần với các layout chung, riêng biệt.
1. Tạo các Layout khác nhau cho các phân đoạn Route
Đây là trường hợp sử dụng phổ biến và mạnh mẽ nhất. Hãy tưởng tượng một ứng dụng web có hai phần chính:
- Một trang web marketing công khai (Trang chủ, Giới thiệu, Bảng giá) với header và footer chung.
- Một dashboard người dùng riêng tư, đã được xác thực (Dashboard, Cài đặt, Hồ sơ) với một thanh bên, điều hướng dành riêng cho người dùng, và một cấu trúc tổng thể khác.
Nếu không có Route Groups, việc áp dụng các root layout khác nhau cho các phần này sẽ rất phức tạp. Với Route Groups, điều đó trở nên cực kỳ trực quan. Bạn có thể tạo một tệp layout.js
duy nhất bên trong mỗi nhóm.
Đây là một cấu trúc tệp điển hình cho kịch bản này:
app/
├── (marketing)/
│ ├── layout.js // Layout công khai với header/footer của marketing
│ ├── page.js // Hiển thị tại '/'
│ └── about/
│ └── page.js // Hiển thị tại '/about'
├── (app)/
│ ├── layout.js // Layout dashboard với thanh bên
│ ├── dashboard/
│ │ └── page.js // Hiển thị tại '/dashboard'
│ └── settings/
│ └── page.js // Hiển thị tại '/settings'
└── layout.js // Root layout (ví dụ, cho <html> và <body> tags)
Trong kiến trúc này:
- Bất kỳ route nào bên trong nhóm
(marketing)
sẽ được bao bọc bởi(marketing)/layout.js
. - Bất kỳ route nào bên trong nhóm
(app)
sẽ được bao bọc bởi(app)/layout.js
. - Cả hai nhóm đều chia sẻ root
app/layout.js
, điều này hoàn hảo để định nghĩa cấu trúc HTML toàn cục.
2. Chọn không tham gia một Layout chung cho một phân đoạn
Đôi khi, một trang hoặc một phần cụ thể cần thoát hoàn toàn khỏi layout cha. Một ví dụ phổ biến là quy trình thanh toán hoặc một trang đích đặc biệt không nên có thanh điều hướng của trang web chính. Bạn có thể đạt được điều này bằng cách đặt route vào một nhóm không chia sẻ layout cấp cao hơn. Mặc dù điều này nghe có vẻ phức tạp, nó chỉ đơn giản có nghĩa là cung cấp cho một route group một tệp layout.js
cấp cao nhất của riêng nó mà không render `children` từ root layout.
Ví dụ thực tế: Xây dựng một ứng dụng đa Layout
Hãy xây dựng một phiên bản tối thiểu của cấu trúc marketing/app đã mô tả ở trên.
1. Root Layout (app/layout.js
)
Layout này là tối thiểu và áp dụng cho mọi trang. Nó định nghĩa cấu trúc HTML thiết yếu.
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
2. Layout Marketing (app/(marketing)/layout.js
)
Layout này bao gồm một header và footer dành cho trang công khai.
// app/(marketing)/layout.js
export default function MarketingLayout({ children }) {
return (
<div>
<header>Marketing Header</header>
<main>{children}</main>
<footer>Marketing Footer</footer>
</div>
);
}
3. Layout Dashboard Ứng dụng (app/(app)/layout.js
)
Layout này có một cấu trúc khác, với một thanh bên cho người dùng đã xác thực.
// app/(app)/layout.js
export default function AppLayout({ children }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', borderRight: '1px solid #ccc' }}>
Dashboard Sidebar
</aside>
<main style={{ flex: 1, padding: '20px' }}>{children}</main>
</div>
);
}
Với cấu trúc này, việc điều hướng đến /about
sẽ render trang với `MarketingLayout`, trong khi điều hướng đến /dashboard
sẽ render nó với `AppLayout`. URL vẫn sạch sẽ và có ngữ nghĩa, trong khi cấu trúc tệp của dự án được tổ chức hoàn hảo và có khả năng mở rộng.
Mở khóa giao diện người dùng động với Parallel Routes
Trong khi Route Groups giúp tổ chức các phần riêng biệt của một ứng dụng, Parallel Routes giải quyết một thách thức khác: hiển thị nhiều chế độ xem trang độc lập trong một layout duy nhất. Đây là một yêu cầu phổ biến đối với các dashboard phức tạp, các trang tin tức mạng xã hội, hoặc bất kỳ giao diện người dùng nào mà các bảng điều khiển khác nhau cần được render và quản lý đồng thời.
Parallel Routes là gì?
Parallel Routes cho phép bạn render đồng thời một hoặc nhiều trang trong cùng một layout. Các route này được định nghĩa bằng cách sử dụng một quy ước thư mục đặc biệt gọi là slots. Slots được tạo bằng cú pháp @folderName
. Chúng không phải là một phần của cấu trúc URL; thay vào đó, chúng được tự động truyền dưới dạng props đến tệp `layout.js` cha chung gần nhất.
Ví dụ, nếu bạn có một layout cần hiển thị một bảng tin hoạt động của nhóm và một biểu đồ phân tích cạnh nhau, bạn có thể định nghĩa hai slot: `@team` và `@analytics`.
Ý tưởng cốt lõi: Slots
Hãy nghĩ về slots như các trình giữ chỗ (placeholder) được đặt tên trong layout của bạn. Tệp layout chấp nhận các slot này một cách tường minh dưới dạng props và quyết định nơi để render chúng.
Hãy xem xét thành phần layout này:
// Một layout chấp nhận hai slot: 'team' và 'analytics'
export default function DashboardLayout({ children, team, analytics }) {
return (
<div>
{children}
<div style={{ display: 'flex' }}>
{team}
{analytics}
</div>
</div>
);
}
Ở đây, `children`, `team`, và `analytics` đều là các slot. `children` là một slot ngầm định tương ứng với tệp `page.js` tiêu chuẩn trong thư mục. `team` và `analytics` là các slot tường minh phải được tạo với tiền tố `@` trong hệ thống tệp.
Các tính năng và lợi ích chính
- Xử lý Route độc lập: Mỗi parallel route (slot) có thể có trạng thái loading và error riêng. Điều này có nghĩa là bảng phân tích của bạn có thể hiển thị một biểu tượng tải trong khi bảng tin của nhóm đã được render, dẫn đến trải nghiệm người dùng tốt hơn nhiều.
- Render có điều kiện: Bạn có thể quyết định render slot nào một cách có lập trình dựa trên các điều kiện nhất định, chẳng hạn như trạng thái xác thực người dùng hoặc quyền hạn.
- Điều hướng phụ: Mỗi slot có thể được điều hướng độc lập mà không ảnh hưởng đến các slot khác. Điều này hoàn hảo cho các giao diện theo thẻ hoặc các dashboard nơi trạng thái của một bảng điều khiển hoàn toàn tách biệt với trạng thái của bảng điều khiển khác.
Kịch bản thực tế: Xây dựng một Dashboard phức tạp
Hãy thiết kế một dashboard tại URL /dashboard
. Nó sẽ có một khu vực nội dung chính, một bảng điều khiển hoạt động của nhóm, và một bảng điều khiển phân tích hiệu suất.
Cấu trúc tệp:
app/
└── dashboard/
├── @analytics/
│ ├── page.js // UI cho slot analytics
│ └── loading.js // UI tải dành riêng cho analytics
├── @team/
│ └── page.js // UI cho slot team
├── layout.js // Layout điều phối các slot
└── page.js // Slot 'children' ngầm định (nội dung chính)
1. Layout Dashboard (app/dashboard/layout.js
)
Layout này nhận và sắp xếp ba slot.
// app/dashboard/layout.js
export default function DashboardLayout({ children, analytics, team }) {
const isLoggedIn = true; // Thay thế bằng logic xác thực thực tế
return isLoggedIn ? (
<div>
<h1>Main Dashboard</h1>
{children}
<div style={{ marginTop: '20px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ border: '1px solid blue', padding: '10px' }}>
<h2>Team Activity</h2>
{team}
</div>
<div style={{ border: '1px solid green', padding: '10px' }}>
<h2>Performance Analytics</h2>
{analytics}
</div>
</div>
</div>
) : (
<div>Please log in to view the dashboard.</div>
);
}
2. Các trang Slot (ví dụ: app/dashboard/@analytics/page.js
)
Tệp `page.js` của mỗi slot chứa UI cho bảng điều khiển cụ thể đó.
// app/dashboard/@analytics/page.js
async function getAnalyticsData() {
// Mô phỏng một yêu cầu mạng
await new Promise(resolve => setTimeout(resolve, 3000));
return { views: '1.2M', revenue: '$50,000' };
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
return (
<div>
<p>Page Views: {data.views}</p>
<p>Revenue: {data.revenue}</p>
</div>
);
}
// app/dashboard/@analytics/loading.js
export default function Loading() {
return <p>Loading analytics data...</p>;
}
Với thiết lập này, khi người dùng điều hướng đến /dashboard
, Next.js sẽ render `DashboardLayout`. Layout sẽ nhận nội dung đã render từ dashboard/page.js
, dashboard/@team/page.js
, và dashboard/@analytics/page.js
dưới dạng props và đặt chúng vào vị trí tương ứng. Quan trọng là, bảng phân tích sẽ hiển thị trạng thái `loading.js` của riêng nó trong 3 giây mà không chặn việc render phần còn lại của dashboard.
Xử lý các Route không khớp với `default.js`
Một câu hỏi quan trọng nảy sinh: Điều gì xảy ra nếu Next.js không thể lấy trạng thái hoạt động của một slot cho URL hiện tại? Ví dụ, trong lần tải đầu tiên hoặc khi tải lại trang, URL có thể là /dashboard
, không cung cấp hướng dẫn cụ thể về những gì cần hiển thị bên trong các slot @team
hoặc `@analytics`. Theo mặc định, Next.js sẽ render lỗi 404.
Để ngăn chặn điều này, chúng ta có thể cung cấp một giao diện người dùng dự phòng (fallback UI) bằng cách tạo một tệp default.js
bên trong parallel route.
Ví dụ:
// app/dashboard/@analytics/default.js
export default function DefaultAnalyticsPage() {
return (
<div>
<p>No analytics data selected.</p>
</div>
);
}
Bây giờ, nếu slot analytics không khớp, Next.js sẽ render nội dung của `default.js` thay vì một trang 404. Điều này là cần thiết để tạo ra một trải nghiệm người dùng mượt mà, đặc biệt là khi tải lần đầu một thiết lập parallel route phức tạp.
Kết hợp Route Groups và Parallel Routes cho các kiến trúc nâng cao
Sức mạnh thực sự của App Router được nhận ra khi bạn kết hợp các tính năng của nó. Route Groups và Parallel Routes hoạt động tuyệt vời cùng nhau để tạo ra các kiến trúc ứng dụng tinh vi và được tổ chức cao.
Trường hợp sử dụng: Trình xem nội dung đa phương thức (Multi-Modal)
Hãy tưởng tượng một nền tảng như một thư viện media hoặc một trình xem tài liệu nơi người dùng có thể xem một mục nhưng cũng có thể mở một cửa sổ modal để xem chi tiết của nó mà không mất bối cảnh của trang nền. Điều này thường được gọi là "Intercepting Route" (Route chặn) và là một mẫu mạnh mẽ được xây dựng trên parallel routes.
Hãy tạo một thư viện ảnh. Khi bạn nhấp vào một bức ảnh, nó sẽ mở ra trong một modal. Nhưng nếu bạn làm mới trang hoặc điều hướng trực tiếp đến URL của bức ảnh, nó sẽ hiển thị một trang riêng cho bức ảnh đó.
Cấu trúc tệp:
app/
├── @modal/(..)(..)photos/[id]/page.js // Route bị chặn cho modal
├── photos/
│ └── [id]/
│ └── page.js // Trang ảnh chuyên dụng
├── layout.js // Root layout nhận slot @modal
└── page.js // Trang thư viện chính
Giải thích:
- Chúng ta tạo một parallel route slot tên là `@modal`.
- Đường dẫn trông lạ
(..)(..)photos/[id]
sử dụng một quy ước gọi là "catch-all segments" để khớp với routephotos/[id]
từ hai cấp độ trên (từ thư mục gốc). - Khi người dùng điều hướng từ trang thư viện chính (
/
) đến một bức ảnh, Next.js sẽ chặn điều hướng này và render trang của modal bên trong slot `@modal` thay vì thực hiện một điều hướng trang đầy đủ. - Trang thư viện chính vẫn hiển thị trong prop `children` của layout.
- Nếu người dùng truy cập trực tiếp
/photos/123
, việc chặn sẽ không được kích hoạt, và trang chuyên dụng tạiphotos/[id]/page.js
sẽ được render bình thường.
Mẫu này kết hợp parallel routes (slot `@modal`) với các quy ước định tuyến nâng cao để tạo ra một trải nghiệm người dùng liền mạch mà sẽ rất phức tạp để triển khai thủ công.
Các phương pháp hay nhất và những cạm bẫy thường gặp
Các phương pháp hay nhất cho Route Groups
- Sử dụng tên mô tả: Chọn các tên có ý nghĩa như
(auth)
,(marketing)
, hoặc(protected)
để làm cho cấu trúc dự án của bạn tự giải thích. - Giữ cấu trúc phẳng khi có thể: Tránh lồng ghép quá nhiều các route group. Một cấu trúc phẳng hơn thường dễ hiểu và bảo trì hơn.
- Nhớ mục đích của chúng: Sử dụng chúng để phân chia layout và tổ chức, không phải để tạo các phân đoạn URL.
Các phương pháp hay nhất cho Parallel Routes
- Luôn cung cấp một `default.js`: Đối với bất kỳ việc sử dụng parallel routes không tầm thường nào, hãy bao gồm một tệp `default.js` để xử lý các lần tải ban đầu và các trạng thái không khớp một cách mượt mà.
- Tận dụng các trạng thái tải chi tiết: Đặt một tệp `loading.js` bên trong thư mục của mỗi slot để cung cấp phản hồi tức thì cho người dùng và ngăn chặn các thác nước UI (UI waterfalls).
- Sử dụng cho UI độc lập: Parallel routes tỏa sáng khi nội dung của mỗi slot thực sự độc lập. Nếu các bảng điều khiển có mối liên kết sâu sắc, việc truyền props xuống qua một cây thành phần duy nhất có thể là một giải pháp đơn giản hơn.
Những cạm bẫy thường gặp cần tránh
- Quên các quy ước: Một lỗi phổ biến là quên dấu ngoặc đơn `()` cho route groups hoặc ký hiệu at `@` cho parallel route slots. Điều này sẽ khiến chúng bị coi như các phân đoạn URL thông thường.
- Thiếu `default.js`: Vấn đề thường gặp nhất với parallel routes là thấy các lỗi 404 không mong muốn vì không cung cấp một tệp `default.js` dự phòng cho các slot không khớp.
- Hiểu sai về `children`: Trong một layout sử dụng parallel routes, hãy nhớ rằng `children` chỉ là một trong các slot, được ánh xạ ngầm đến `page.js` hoặc layout lồng nhau trong cùng thư mục.
Kết luận: Xây dựng tương lai của ứng dụng web
Next.js App Router, với các tính năng như Route Groups và Parallel Routes, cung cấp một nền tảng vững chắc và có khả năng mở rộng cho phát triển web hiện đại. Route Groups cung cấp một giải pháp thanh lịch để tổ chức mã và áp dụng các layout riêng biệt mà không ảnh hưởng đến ngữ nghĩa của URL. Parallel Routes mở khóa khả năng xây dựng các giao diện động, đa bảng điều khiển với các trạng thái độc lập, điều mà trước đây chỉ có thể đạt được thông qua quản lý trạng thái phức tạp phía máy khách.
Bằng cách hiểu và kết hợp các mẫu kiến trúc mạnh mẽ này, bạn có thể vượt ra ngoài các trang web đơn giản và bắt đầu xây dựng các ứng dụng tinh vi, hiệu suất cao và dễ bảo trì, đáp ứng nhu cầu của người dùng ngày nay. Quá trình học hỏi có thể khó khăn hơn so với Pages Router cổ điển, nhưng lợi ích về mặt kiến trúc ứng dụng và trải nghiệm người dùng là vô cùng lớn. Hãy bắt đầu thử nghiệm với các khái niệm này trong dự án tiếp theo của bạn và mở khóa toàn bộ tiềm năng của Next.js.