ปลดล็อกพลังของ Next.js App Router ด้วยคู่มือเชิงลึกเกี่ยวกับ file-based routing เรียนรู้วิธีการจัดโครงสร้างแอปพลิเคชัน สร้าง dynamic routes จัดการ layouts และอื่น ๆ
Next.js App Router: คู่มือฉบับสมบูรณ์เกี่ยวกับ File-Based Routing
Next.js App Router ซึ่งเปิดตัวใน Next.js 13 และกลายเป็นมาตรฐานในเวอร์ชันต่อๆ มา ได้ปฏิวัติวิธีการที่เราจัดโครงสร้างและนำทางในแอปพลิเคชัน โดยนำเสนอระบบการกำหนดเส้นทางตามไฟล์ (file-based routing) ที่ทรงพลังและใช้งานง่าย ซึ่งช่วยให้การพัฒนาง่ายขึ้น ปรับปรุงประสิทธิภาพ และยกระดับประสบการณ์ของนักพัฒนาโดยรวม คู่มือฉบับสมบูรณ์นี้จะเจาะลึกเกี่ยวกับการกำหนดเส้นทางตามไฟล์ของ App Router เพื่อให้คุณมีความรู้และทักษะในการสร้างแอปพลิเคชัน Next.js ที่แข็งแกร่งและขยายขนาดได้
File-Based Routing คืออะไร?
File-based routing คือระบบการกำหนดเส้นทางที่โครงสร้างของ routes ในแอปพลิเคชันของคุณถูกกำหนดโดยตรงจากการจัดระเบียบไฟล์และไดเรกทอรีของคุณ ใน Next.js App Router คุณจะกำหนด routes โดยการสร้างไฟล์ภายในไดเรกทอรี `app` แต่ละโฟลเดอร์จะแสดงถึงส่วนของ route (route segment) และไฟล์พิเศษภายในโฟลเดอร์เหล่านั้นจะกำหนดว่า route segment นั้นจะถูกจัดการอย่างไร แนวทางนี้มีข้อดีหลายประการ:
- โครงสร้างที่เข้าใจง่าย: ระบบไฟล์จะสะท้อนโครงสร้าง route ของแอปพลิเคชัน ทำให้ง่ายต่อการทำความเข้าใจและนำทาง
- การกำหนดเส้นทางอัตโนมัติ: Next.js จะสร้าง routes โดยอัตโนมัติตามโครงสร้างไฟล์ของคุณ ทำให้ไม่จำเป็นต้องกำหนดค่าด้วยตนเอง
- การจัดวางโค้ดร่วมกัน (Code Collocation): Route handlers และ UI components จะอยู่ด้วยกัน ช่วยปรับปรุงการจัดระเบียบโค้ดและการบำรุงรักษา
- ฟีเจอร์ในตัว: App Router รองรับ layouts, dynamic routes, data fetching และอื่นๆ ในตัว ทำให้การจัดการ routing ที่ซับซ้อนง่ายขึ้น
การเริ่มต้นใช้งาน App Router
ในการใช้ App Router คุณต้องสร้างโปรเจกต์ Next.js ใหม่ หรือย้ายโปรเจกต์ที่มีอยู่แล้ว ตรวจสอบให้แน่ใจว่าคุณใช้ Next.js เวอร์ชัน 13 หรือสูงกว่า
การสร้างโปรเจกต์ใหม่:
คุณสามารถสร้างโปรเจกต์ Next.js ใหม่ด้วย App Router โดยใช้คำสั่งต่อไปนี้:
npx create-next-app@latest my-app --example with-app
การย้ายโปรเจกต์ที่มีอยู่แล้ว:
หากต้องการย้ายโปรเจกต์ที่มีอยู่ คุณต้องย้ายหน้าต่างๆ ของคุณจากไดเรกทอรี `pages` ไปยังไดเรกทอรี `app` คุณอาจต้องปรับตรรกะการกำหนดเส้นทางของคุณตามไปด้วย Next.js มี คู่มือการย้ายระบบ เพื่อช่วยคุณในกระบวนการนี้
แนวคิดหลักของ File-Based Routing
App Router นำเสนอไฟล์พิเศษและข้อตกลงหลายอย่างที่กำหนดวิธีการจัดการ routes ของคุณ:
1. ไดเรกทอรี `app`
ไดเรกทอรี `app` คือรากของ routes ในแอปพลิเคชันของคุณ ไฟล์และโฟลเดอร์ทั้งหมดภายในไดเรกทอรีนี้จะถูกใช้เพื่อสร้าง routes สิ่งใดก็ตามที่อยู่นอกไดเรกทอรี `app` (เช่น ไดเรกทอรี `pages` หากคุณกำลังย้ายระบบ) จะถูกละเว้นโดย App Router
2. ไฟล์ `page.js`
ไฟล์ `page.js` (หรือ `page.jsx`, `page.ts`, `page.tsx`) เป็นส่วนพื้นฐานที่สุดของ App Router ใช้สำหรับกำหนด UI component ที่จะแสดงผลสำหรับ route segment ที่เฉพาะเจาะจง เป็นไฟล์ที่ **จำเป็นต้องมี** สำหรับทุก route segment ที่คุณต้องการให้เข้าถึงได้โดยตรง
ตัวอย่าง:
หากคุณมีโครงสร้างไฟล์ดังนี้:
app/
about/
page.js
component ที่ export จาก `app/about/page.js` จะถูกแสดงผลเมื่อผู้ใช้ไปยัง `/about`
// app/about/page.js
import React from 'react';
export default function AboutPage() {
return (
<div>
<h1>เกี่ยวกับเรา</h1>
<p>เรียนรู้เพิ่มเติมเกี่ยวกับบริษัทของเรา</p>
</div>
);
}
3. ไฟล์ `layout.js`
ไฟล์ `layout.js` (หรือ `layout.jsx`, `layout.ts`, `layout.tsx`) กำหนด UI ที่ใช้ร่วมกันในหลายๆ หน้าภายใน route segment เดียวกัน Layouts มีประโยชน์สำหรับการสร้างส่วนหัว (headers), ส่วนท้าย (footers), แถบด้านข้าง (sidebars) และองค์ประกอบอื่นๆ ที่ควรมีอยู่บนหลายหน้า
ตัวอย่าง:
สมมติว่าคุณต้องการเพิ่มส่วนหัวให้กับทั้งหน้า `/about` และหน้าที่สมมติขึ้นมาคือ `/about/team` คุณสามารถสร้างไฟล์ `layout.js` ในไดเรกทอรี `app/about`:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>เกี่ยวกับบริษัทของเรา</h1>
</header>
<main>{children}</main>
</div>
);
}
prop `children` จะถูกแทนที่ด้วย UI ที่แสดงผลโดยไฟล์ `page.js` ในไดเรกทอรีเดียวกันหรือในไดเรกทอรีย่อยใดๆ
4. ไฟล์ `template.js`
ไฟล์ `template.js` คล้ายกับ `layout.js` แต่จะสร้างอินสแตนซ์ใหม่ของ component สำหรับแต่ละ child route ซึ่งมีประโยชน์สำหรับสถานการณ์ที่คุณต้องการรักษาสถานะของ component หรือป้องกันการ re-render เมื่อนำทางระหว่าง child routes ซึ่งแตกต่างจาก layouts ตรงที่ templates จะ re-render ใหม่เมื่อมีการนำทาง การใช้ templates เหมาะอย่างยิ่งสำหรับการทำแอนิเมชันองค์ประกอบต่างๆ ขณะนำทาง
ตัวอย่าง:
// app/template.js
'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<main>
<p>Template: {count}</p>
<button onClick={() => setCount(count + 1)}>อัปเดต Template</button>
{children}
</main>
)
}
5. ไฟล์ `loading.js`
ไฟล์ `loading.js` (หรือ `loading.jsx`, `loading.ts`, `loading.tsx`) ช่วยให้คุณสร้าง UI สำหรับการโหลดที่จะแสดงในขณะที่ route segment กำลังโหลดข้อมูล ซึ่งมีประโยชน์ในการมอบประสบการณ์ผู้ใช้ที่ดีขึ้นเมื่อมีการดึงข้อมูลหรือดำเนินการแบบอะซิงโครนัสอื่นๆ
ตัวอย่าง:
// app/about/loading.js
import React from 'react';
export default function Loading() {
return <p>กำลังโหลดข้อมูลเกี่ยวกับเรา...</p>;
}
เมื่อผู้ใช้ไปยัง `/about` component `Loading` จะถูกแสดงขึ้นจนกว่า component `page.js` จะแสดงผลเสร็จสมบูรณ์
6. ไฟล์ `error.js`
ไฟล์ `error.js` (หรือ `error.jsx`, `error.ts`, `error.tsx`) ช่วยให้คุณสร้าง UI ข้อผิดพลาดแบบกำหนดเองที่จะแสดงเมื่อเกิดข้อผิดพลาดภายใน route segment ซึ่งมีประโยชน์ในการให้ข้อความแสดงข้อผิดพลาดที่เป็นมิตรต่อผู้ใช้มากขึ้นและป้องกันไม่ให้แอปพลิเคชันทั้งหมดล่ม
ตัวอย่าง:
// app/about/error.js
'use client'
import React from 'react';
export default function Error({ error, reset }) {
return (
<div>
<h2>เกิดข้อผิดพลาด!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>ลองอีกครั้ง</button>
</div>
);
}
หากเกิดข้อผิดพลาดขณะแสดงผลหน้า `/about` component `Error` จะถูกแสดงขึ้น prop `error` จะมีข้อมูลเกี่ยวกับข้อผิดพลาด และฟังก์ชัน `reset` จะช่วยให้ผู้ใช้สามารถพยายามโหลดหน้านั้นใหม่ได้
7. Route Groups
Route Groups `(groupName)` ช่วยให้คุณสามารถจัดระเบียบ routes ของคุณได้โดยไม่ส่งผลกระทบต่อโครงสร้าง URL สามารถสร้างได้โดยการครอบชื่อโฟลเดอร์ด้วยวงเล็บ ซึ่งมีประโยชน์อย่างยิ่งสำหรับการจัดระเบียบ layouts และ components ที่ใช้ร่วมกัน
ตัวอย่าง:
app/
(marketing)/
about/
page.js
contact/
page.js
(shop)/
products/
page.js
ในตัวอย่างนี้ หน้า `about` และ `contact` ถูกจัดกลุ่มภายใต้กลุ่ม `marketing` และหน้า `products` อยู่ภายใต้กลุ่ม `shop` โดยที่ URL ยังคงเป็น `/about`, `/contact`, และ `/products` ตามลำดับ
8. Dynamic Routes
Dynamic routes ช่วยให้คุณสร้าง routes ที่มี segments ที่เปลี่ยนแปลงได้ ซึ่งมีประโยชน์สำหรับการแสดงเนื้อหาตามข้อมูลที่ดึงมาจากฐานข้อมูลหรือ API Dynamic route segments ถูกกำหนดโดยการครอบชื่อ segment ด้วยวงเล็บเหลี่ยม (เช่น `[id]`)
ตัวอย่าง:
สมมติว่าคุณต้องการสร้าง route สำหรับแสดงโพสต์บล็อกแต่ละรายการตาม ID ของมัน คุณสามารถสร้างโครงสร้างไฟล์ได้ดังนี้:
app/
blog/
[id]/
page.js
segment `[id]` เป็น dynamic segment component ที่ export จาก `app/blog/[id]/page.js` จะถูกแสดงผลเมื่อผู้ใช้ไปยัง URL เช่น `/blog/123` หรือ `/blog/456` ค่าของพารามิเตอร์ `id` จะมีอยู่ใน prop `params` ของ component นั้น
// app/blog/[id]/page.js
import React from 'react';
export default async function BlogPost({ params }) {
const { id } = params;
// ดึงข้อมูลสำหรับโพสต์บล็อกที่มี ID ที่กำหนด
const post = await fetchBlogPost(id);
if (!post) {
return <p>ไม่พบโพสต์บล็อก</p>;
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
async function fetchBlogPost(id) {
// จำลองการดึงข้อมูลจากฐานข้อมูลหรือ API
return new Promise((resolve) => {
setTimeout(() => {
const posts = {
'123': { title: 'โพสต์บล็อกแรกของฉัน', content: 'นี่คือเนื้อหาของโพสต์บล็อกแรกของฉัน' },
'456': { title: 'โพสต์บล็อกอีกอัน', content: 'นี่คือเนื้อหาที่น่าตื่นเต้นเพิ่มเติม' },
};
resolve(posts[id] || null);
}, 500);
});
}
คุณยังสามารถใช้ dynamic segments หลายตัวใน route เดียวได้ ตัวอย่างเช่น คุณอาจมี route อย่าง `/blog/[category]/[id]`
9. Catch-all Segments
Catch-all segments ช่วยให้คุณสร้าง routes ที่ตรงกับ segments จำนวนเท่าใดก็ได้ ซึ่งมีประโยชน์สำหรับสถานการณ์เช่นการสร้าง CMS ที่โครงสร้าง URL ถูกกำหนดโดยผู้ใช้ Catch-all segments ถูกกำหนดโดยการเพิ่มจุดสามจุดหน้าชื่อ segment (เช่น `[...slug]`)
ตัวอย่าง:
app/
docs/
[...slug]/
page.js
segment `[...slug]` จะตรงกับ segments จำนวนเท่าใดก็ได้ที่ตามหลัง `/docs` ตัวอย่างเช่น มันจะตรงกับ `/docs/getting-started`, `/docs/api/users`, และ `/docs/advanced/configuration` ค่าของพารามิเตอร์ `slug` จะเป็นอาร์เรย์ที่ประกอบด้วย segments ที่ตรงกัน
// app/docs/[...slug]/page.js
import React from 'react';
export default function DocsPage({ params }) {
const { slug } = params;
return (
<div>
<h1>เอกสาร</h1>
<p>Slug: {slug ? slug.join('/') : 'ไม่มี slug'}</p>
</div>
);
}
Optional catch-all segments สามารถสร้างได้โดยการเพิ่มชื่อ segment ในวงเล็บเหลี่ยมสองชั้น `[[...slug]]` ซึ่งจะทำให้ route segment นั้นเป็นทางเลือก ตัวอย่าง:
app/
blog/
[[...slug]]/
page.js
การตั้งค่านี้จะแสดงผล component page.js ทั้งที่ `/blog` และที่ `/blog/any/number/of/segments`
10. Parallel Routes
Parallel Routes ช่วยให้คุณสามารถแสดงผลหน้าเว็บหนึ่งหน้าหรือมากกว่าพร้อมกันใน layout เดียวกันได้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับ layout ที่ซับซ้อน เช่น แดชบอร์ด ที่ส่วนต่างๆ ของหน้าสามารถโหลดได้อย่างอิสระ Parallel Routes ถูกกำหนดโดยใช้สัญลักษณ์ `@` ตามด้วยชื่อ slot (เช่น `@sidebar`, `@main`)
ตัวอย่าง:
app/
@sidebar/
page.js // เนื้อหาสำหรับ sidebar
@main/
page.js // เนื้อหาสำหรับส่วนหลัก
default.js // จำเป็น: กำหนด layout เริ่มต้นสำหรับ parallel routes
ไฟล์ `default.js` เป็นสิ่งจำเป็นเมื่อใช้ parallel routes ใช้สำหรับกำหนดว่า slot ต่างๆ จะถูกรวมกันเพื่อสร้าง layout สุดท้ายอย่างไร
// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
{sidebar}
</aside>
<main style={{ flex: 1, padding: '20px' }}>
{main}
</main>
</div>
);
}
11. Intercepting Routes
Intercepting Routes ช่วยให้คุณสามารถโหลด route จากส่วนอื่นของแอปพลิเคชันของคุณภายใน layout ปัจจุบันได้ ซึ่งมีประโยชน์สำหรับการสร้าง modals, แกลเลอรีรูปภาพ และองค์ประกอบ UI อื่นๆ ที่ควรปรากฏทับเนื้อหาหน้าที่มีอยู่ Intercepting Routes ถูกกำหนดโดยใช้ синтаксис `(..)` ซึ่งบ่งชี้ว่าต้องย้อนกลับไปกี่ระดับในโครงสร้างไดเรกทอรีเพื่อค้นหา route ที่จะดักจับ
ตัวอย่าง:
app/
(.)photos/
[id]/
page.js // route ที่ถูกดักจับ
feed/
page.js // หน้าที่แสดง modal รูปภาพ
ในตัวอย่างนี้ เมื่อผู้ใช้คลิกที่รูปภาพในหน้า `/feed` route `app/(.)photos/[id]/page.js` จะถูกดักจับและแสดงเป็น modal ทับหน้า `/feed` ไวยากรณ์ `(.)` บอกให้ Next.js มองขึ้นไปหนึ่งระดับ (ไปยังไดเรกทอรี `app`) เพื่อค้นหา route `photos/[id]`
การดึงข้อมูลด้วย App Router
App Router รองรับการดึงข้อมูลในตัวโดยใช้ Server Components และ Client Components Server Components จะถูกเรนเดอร์บนเซิร์ฟเวอร์ ในขณะที่ Client Components จะถูกเรนเดอร์บนไคลเอนต์ ซึ่งช่วยให้คุณสามารถเลือกแนวทางที่ดีที่สุดสำหรับแต่ละ component ตามความต้องการได้
Server Components
Server Components เป็นค่าเริ่มต้นใน App Router ช่วยให้คุณสามารถดึงข้อมูลได้โดยตรงใน components ของคุณโดยไม่จำเป็นต้องมี API routes แยกต่างหาก ซึ่งสามารถปรับปรุงประสิทธิภาพและทำให้โค้ดของคุณง่ายขึ้น
ตัวอย่าง:
// app/products/page.js
import React from 'react';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div>
<h1>สินค้า</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
async function fetchProducts() {
// จำลองการดึงข้อมูลจากฐานข้อมูลหรือ API
return new Promise((resolve) => {
setTimeout(() => {
const products = [
{ id: 1, name: 'สินค้า A' },
{ id: 2, name: 'สินค้า B' },
{ id: 3, name: 'สินค้า C' },
];
resolve(products);
}, 500);
});
}
ในตัวอย่างนี้ ฟังก์ชัน `fetchProducts` ถูกเรียกโดยตรงภายใน component `ProductsPage` component จะถูกเรนเดอร์บนเซิร์ฟเวอร์ และข้อมูลจะถูกดึงมาก่อนที่ HTML จะถูกส่งไปยังไคลเอนต์
Client Components
Client Components จะถูกเรนเดอร์บนไคลเอนต์และช่วยให้คุณสามารถใช้ฟีเจอร์ฝั่งไคลเอนต์ได้ เช่น event listeners, state และ browser APIs หากต้องการใช้ Client Component คุณต้องเพิ่มคำสั่ง `'use client'` ที่ด้านบนของไฟล์
ตัวอย่าง:
// app/counter/page.js
'use client'
import React, { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>ตัวนับ</h1>
<p>จำนวน: {count}</p>
<button onClick={() => setCount(count + 1)}>เพิ่มค่า</button>
</div>
);
}
ในตัวอย่างนี้ `CounterPage` component เป็น Client Component เพราะใช้ hook `useState` คำสั่ง `'use client'` บอกให้ Next.js เรนเดอร์ component นี้บนฝั่งไคลเอนต์
เทคนิคการกำหนดเส้นทางขั้นสูง
App Router มีเทคนิคการกำหนดเส้นทางขั้นสูงหลายอย่างที่สามารถใช้เพื่อสร้างแอปพลิเคชันที่ซับซ้อนและมีประสิทธิภาพ
1. Route Handlers
Route Handlers ช่วยให้คุณสามารถสร้าง API endpoints ภายในไดเรกทอรี `app` ของคุณได้ ซึ่งทำให้ไม่จำเป็นต้องมีไดเรกทอรี `pages/api` แยกต่างหาก Route Handlers ถูกกำหนดในไฟล์ชื่อ `route.js` (หรือ `route.ts`) และส่งออกฟังก์ชันที่จัดการกับเมธอด HTTP ต่างๆ (เช่น `GET`, `POST`, `PUT`, `DELETE`)
ตัวอย่าง:
// app/api/users/route.js
import { NextResponse } from 'next/server'
export async function GET(request) {
// จำลองการดึงผู้ใช้จากฐานข้อมูล
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json()
console.log('ข้อมูลที่ได้รับ:', body)
return NextResponse.json({ message: 'สร้างผู้ใช้สำเร็จ' }, { status: 201 })
}
ตัวอย่างนี้กำหนด route handler ที่ `/api/users` ซึ่งจัดการทั้งคำขอ `GET` และ `POST` ฟังก์ชัน `GET` จะส่งคืนรายชื่อผู้ใช้ และฟังก์ชัน `POST` จะสร้างผู้ใช้ใหม่
2. Route Groups กับ Multiple Layouts
คุณสามารถรวม route groups กับ layouts เพื่อสร้าง layouts ที่แตกต่างกันสำหรับส่วนต่างๆ ของแอปพลิเคชันของคุณได้ ซึ่งมีประโยชน์สำหรับสถานการณ์ที่คุณต้องการมีส่วนหัวหรือแถบด้านข้างที่แตกต่างกันสำหรับส่วนต่างๆ ของเว็บไซต์
ตัวอย่าง:
app/
(marketing)/
layout.js // Layout สำหรับการตลาด
about/
page.js
contact/
page.js
(admin)/
layout.js // Layout สำหรับผู้ดูแลระบบ
dashboard/
page.js
ในตัวอย่างนี้ หน้า `about` และ `contact` จะใช้ layout `marketing` ในขณะที่หน้า `dashboard` จะใช้ layout `admin`
3. Middleware
Middleware ช่วยให้คุณสามารถรันโค้ดก่อนที่คำขอจะถูกจัดการโดยแอปพลิเคชันของคุณ ซึ่งมีประโยชน์สำหรับงานต่างๆ เช่น การยืนยันตัวตน, การให้สิทธิ์, การบันทึกข้อมูล (logging) และการเปลี่ยนเส้นทางผู้ใช้ตามตำแหน่งที่ตั้งหรืออุปกรณ์ของพวกเขา
Middleware ถูกกำหนดในไฟล์ชื่อ `middleware.js` (หรือ `middleware.ts`) ที่รากของโปรเจกต์ของคุณ
ตัวอย่าง:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// ตรวจสอบว่าผู้ใช้ได้รับการยืนยันตัวตนหรือไม่
const isAuthenticated = false; // แทนที่ด้วยตรรกะการยืนยันตัวตนของคุณ
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// ดู "Matching Paths" ด้านล่างเพื่อเรียนรู้เพิ่มเติม
export const config = {
matcher: '/admin/:path*',
}
ตัวอย่างนี้กำหนด middleware ที่ตรวจสอบว่าผู้ใช้ได้รับการยืนยันตัวตนหรือไม่ก่อนที่จะอนุญาตให้เข้าถึง route ใดๆ ภายใต้ `/admin` หากผู้ใช้ไม่ได้รับการยืนยันตัวตน พวกเขาจะถูกเปลี่ยนเส้นทางไปยังหน้า `/login`
แนวทางปฏิบัติที่ดีที่สุดสำหรับ File-Based Routing
เพื่อให้ได้ประโยชน์สูงสุดจากระบบ file-based routing ของ App Router ให้พิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:
- จัดระเบียบโครงสร้างไฟล์ของคุณให้ดี: ใช้ชื่อโฟลเดอร์ที่มีความหมายและจัดกลุ่มไฟล์ที่เกี่ยวข้องไว้ด้วยกัน
- ใช้ layouts สำหรับ UI ที่ใช้ร่วมกัน: สร้าง layouts สำหรับส่วนหัว, ส่วนท้าย, แถบด้านข้าง และองค์ประกอบอื่นๆ ที่ใช้ร่วมกันในหลายหน้า
- ใช้ loading UIs: จัดเตรียม UI สำหรับการโหลดสำหรับ routes ที่มีการดึงข้อมูลหรือดำเนินการแบบอะซิงโครนัสอื่นๆ
- จัดการข้อผิดพลาดอย่างเหมาะสม: สร้าง UI ข้อผิดพลาดแบบกำหนดเองเพื่อให้ประสบการณ์ผู้ใช้ที่ดีขึ้นเมื่อเกิดข้อผิดพลาด
- ใช้ route groups สำหรับการจัดระเบียบ: ใช้ route groups เพื่อจัดระเบียบ routes ของคุณโดยไม่ส่งผลกระทบต่อโครงสร้าง URL
- ใช้ประโยชน์จาก server components เพื่อประสิทธิภาพ: ใช้ server components เพื่อดึงข้อมูลและเรนเดอร์ UI บนเซิร์ฟเวอร์ เพื่อปรับปรุงประสิทธิภาพและ SEO
- ใช้ client components เมื่อจำเป็น: ใช้ client components เมื่อคุณต้องการใช้ฟีเจอร์ฝั่งไคลเอนต์ เช่น event listeners, state และ browser APIs
ตัวอย่างการทำ Internationalization ด้วย Next.js App Router
Next.js App Router ทำให้การทำ Internationalization (i18n) ง่ายขึ้นผ่าน file-based routing นี่คือวิธีที่คุณสามารถนำ i18n ไปใช้อย่างมีประสิทธิภาพ:
1. Sub-path Routing
จัดระเบียบ routes ของคุณตาม locale โดยใช้ sub-paths ตัวอย่างเช่น:
app/
[locale]/
page.tsx // หน้าแรกสำหรับ locale นั้น
about/
page.tsx // หน้า About สำหรับ locale นั้น
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';
export default async function HomePage({ params: { locale } }) {
const t = await getTranslations(locale);
return (<h1>{t.home.title}</h1>);
}
// dictionaries.js
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
es: () => import('./dictionaries/es.json').then((module) => module.default),
};
export const getTranslations = async (locale) => {
try {
return dictionaries[locale]() ?? dictionaries.en();
} catch (error) {
console.error(`ไม่สามารถโหลดคำแปลสำหรับ locale ${locale}`, error);
return dictionaries.en();
}
};
ในการตั้งค่านี้ dynamic route segment `[locale]` จะจัดการกับ locales ที่แตกต่างกัน (เช่น `/en`, `/es`) คำแปลจะถูกโหลดแบบไดนามิกตาม locale
2. Domain Routing
สำหรับแนวทางที่สูงขึ้น คุณสามารถใช้โดเมนหรือซับโดเมนที่แตกต่างกันสำหรับแต่ละ locale ซึ่งมักจะต้องมีการกำหนดค่าเพิ่มเติมกับผู้ให้บริการโฮสติ้งของคุณ
3. Middleware สำหรับการตรวจจับ Locale
ใช้ middleware เพื่อตรวจจับ locale ที่ผู้ใช้ต้องการโดยอัตโนมัติและเปลี่ยนเส้นทางพวกเขาไปยังที่ที่เหมาะสม
// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
let locales = ['en', 'es', 'fr'];
function getLocale(request) {
const negotiatorHeaders = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(languages, locales, 'en'); // ใช้ "en" เป็น locale เริ่มต้น
} catch (error) {
console.error("เกิดข้อผิดพลาดในการจับคู่ locale:", error);
return 'en'; // กลับไปใช้ภาษาอังกฤษหากการจับคู่ล้มเหลว
}
}
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
);
}
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
middleware นี้จะตรวจสอบว่าพาธที่ร้องขอมีคำนำหน้า locale หรือไม่ หากไม่มี มันจะตรวจจับ locale ที่ผู้ใช้ต้องการโดยใช้ header `Accept-Language` และเปลี่ยนเส้นทางพวกเขาไปยังพาธเฉพาะ locale ที่เหมาะสม ไลบรารีเช่น `@formatjs/intl-localematcher` และ `negotiator` ถูกใช้เพื่อจัดการการเจรจาต่อรอง locale
Next.js App Router และการเข้าถึงได้ทั่วถึง (Global Accessibility)
การสร้างเว็บแอปพลิเคชันที่เข้าถึงได้ทั่วโลกต้องการการพิจารณาหลักการของการเข้าถึงได้ (accessibility - a11y) อย่างรอบคอบ Next.js App Router เป็นพื้นฐานที่มั่นคงสำหรับการสร้างประสบการณ์ที่เข้าถึงได้ แต่สิ่งสำคัญคือการนำแนวทางปฏิบัติที่ดีที่สุดมาใช้เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณสามารถใช้งานได้โดยทุกคน โดยไม่คำนึงถึงความสามารถของพวกเขา
ข้อควรพิจารณาที่สำคัญด้านการเข้าถึงได้
- Semantic HTML: ใช้องค์ประกอบ HTML เชิงความหมาย (เช่น `<article>`, `<nav>`, `<aside>`, `<main>`) เพื่อจัดโครงสร้างเนื้อหาของคุณ สิ่งนี้ให้ความหมายแก่เทคโนโลยีช่วยเหลือและช่วยให้ผู้ใช้นำทางไซต์ของคุณได้ง่ายขึ้น
- ARIA Attributes: ใช้แอตทริบิวต์ ARIA (Accessible Rich Internet Applications) เพื่อเพิ่มการเข้าถึงได้ของ components และ widgets ที่กำหนดเอง แอตทริบิวต์ ARIA ให้ข้อมูลเพิ่มเติมเกี่ยวกับบทบาท สถานะ และคุณสมบัติขององค์ประกอบแก่เทคโนโลยีช่วยเหลือ
- การนำทางด้วยคีย์บอร์ด: ตรวจสอบให้แน่ใจว่าองค์ประกอบแบบโต้ตอบทั้งหมดสามารถเข้าถึงได้ผ่านคีย์บอร์ด ผู้ใช้ควรสามารถนำทางผ่านแอปพลิเคชันของคุณโดยใช้ปุ่ม `Tab` และโต้ตอบกับองค์ประกอบโดยใช้ปุ่ม `Enter` หรือ `Space`
- ความคมชัดของสี: ใช้ความคมชัดของสีที่เพียงพอระหว่างข้อความและพื้นหลังเพื่อให้แน่ใจว่าผู้ใช้ที่มีความบกพร่องทางการมองเห็นสามารถอ่านได้ Web Content Accessibility Guidelines (WCAG) แนะนำอัตราส่วนความคมชัดอย่างน้อย 4.5:1 สำหรับข้อความปกติ และ 3:1 สำหรับข้อความขนาดใหญ่
- ข้อความ Alt สำหรับรูปภาพ: จัดเตรียมข้อความ alt ที่สื่อความหมายสำหรับรูปภาพทั้งหมด ข้อความ alt เป็นข้อความทางเลือกสำหรับรูปภาพที่โปรแกรมอ่านหน้าจอสามารถอ่านได้
- ป้ายกำกับฟอร์ม: เชื่อมโยงป้ายกำกับฟอร์มกับช่องป้อนข้อมูลที่สอดคล้องกันโดยใช้องค์ประกอบ `<label>` สิ่งนี้ทำให้ผู้ใช้เข้าใจได้ชัดเจนว่าคาดหวังข้อมูลอะไรในแต่ละช่อง
- การทดสอบด้วยโปรแกรมอ่านหน้าจอ: ทดสอบแอปพลิเคชันของคุณด้วยโปรแกรมอ่านหน้าจอเพื่อให้แน่ใจว่าผู้ใช้ที่มีความบกพร่องทางการมองเห็นสามารถเข้าถึงได้ โปรแกรมอ่านหน้าจอยอดนิยม ได้แก่ NVDA, JAWS และ VoiceOver
การนำการเข้าถึงได้มาใช้ใน Next.js App Router
- ใช้ Next.js Link Component: ใช้ `<Link>` component สำหรับการนำทาง มันมีฟีเจอร์การเข้าถึงได้ในตัว เช่น prefetching และการจัดการ focus
- การจัดการ Focus: เมื่อนำทางระหว่างหน้าหรือเปิด modals ตรวจสอบให้แน่ใจว่ามีการจัดการ focus อย่างเหมาะสม focus ควรถูกตั้งค่าไปยังองค์ประกอบที่สมเหตุสมผลที่สุดบนหน้าใหม่หรือ modal
- Custom Components ที่เข้าถึงได้: เมื่อสร้าง custom components ตรวจสอบให้แน่ใจว่าสามารถเข้าถึงได้โดยปฏิบัติตามหลักการที่ระบุไว้ข้างต้น ใช้ Semantic HTML, ARIA attributes และการนำทางด้วยคีย์บอร์ดเพื่อให้ components ของคุณใช้งานได้โดยทุกคน
- การ Linting และการทดสอบ: ใช้เครื่องมือ linting เช่น ESLint พร้อมปลั๊กอินการเข้าถึงได้เพื่อระบุปัญหาการเข้าถึงที่อาจเกิดขึ้นในโค้ดของคุณ นอกจากนี้ ให้ใช้เครื่องมือทดสอบอัตโนมัติเพื่อทดสอบแอปพลิเคชันของคุณว่ามีการละเมิดการเข้าถึงได้หรือไม่
สรุป
ระบบ file-based routing ของ Next.js App Router นำเสนอวิธีการที่ทรงพลังและใช้งานง่ายในการจัดโครงสร้างและนำทางแอปพลิเคชันของคุณ ด้วยการทำความเข้าใจแนวคิดหลักและแนวทางปฏิบัติที่ดีที่สุดที่สรุปไว้ในคู่มือนี้ คุณสามารถสร้างแอปพลิเคชัน Next.js ที่แข็งแกร่ง ขยายขนาดได้ และบำรุงรักษาง่าย ลองทดลองกับฟีเจอร์ต่างๆ ของ App Router และค้นพบว่ามันสามารถทำให้เวิร์กโฟลว์การพัฒนาของคุณง่ายขึ้นและปรับปรุงประสบการณ์ผู้ใช้ได้อย่างไร