ไทย

ปลดล็อกพลังของ 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 นั้นจะถูกจัดการอย่างไร แนวทางนี้มีข้อดีหลายประการ:

การเริ่มต้นใช้งาน 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 ให้พิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:

ตัวอย่างการทำ 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 เป็นพื้นฐานที่มั่นคงสำหรับการสร้างประสบการณ์ที่เข้าถึงได้ แต่สิ่งสำคัญคือการนำแนวทางปฏิบัติที่ดีที่สุดมาใช้เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณสามารถใช้งานได้โดยทุกคน โดยไม่คำนึงถึงความสามารถของพวกเขา

ข้อควรพิจารณาที่สำคัญด้านการเข้าถึงได้

  1. Semantic HTML: ใช้องค์ประกอบ HTML เชิงความหมาย (เช่น `<article>`, `<nav>`, `<aside>`, `<main>`) เพื่อจัดโครงสร้างเนื้อหาของคุณ สิ่งนี้ให้ความหมายแก่เทคโนโลยีช่วยเหลือและช่วยให้ผู้ใช้นำทางไซต์ของคุณได้ง่ายขึ้น
  2. ARIA Attributes: ใช้แอตทริบิวต์ ARIA (Accessible Rich Internet Applications) เพื่อเพิ่มการเข้าถึงได้ของ components และ widgets ที่กำหนดเอง แอตทริบิวต์ ARIA ให้ข้อมูลเพิ่มเติมเกี่ยวกับบทบาท สถานะ และคุณสมบัติขององค์ประกอบแก่เทคโนโลยีช่วยเหลือ
  3. การนำทางด้วยคีย์บอร์ด: ตรวจสอบให้แน่ใจว่าองค์ประกอบแบบโต้ตอบทั้งหมดสามารถเข้าถึงได้ผ่านคีย์บอร์ด ผู้ใช้ควรสามารถนำทางผ่านแอปพลิเคชันของคุณโดยใช้ปุ่ม `Tab` และโต้ตอบกับองค์ประกอบโดยใช้ปุ่ม `Enter` หรือ `Space`
  4. ความคมชัดของสี: ใช้ความคมชัดของสีที่เพียงพอระหว่างข้อความและพื้นหลังเพื่อให้แน่ใจว่าผู้ใช้ที่มีความบกพร่องทางการมองเห็นสามารถอ่านได้ Web Content Accessibility Guidelines (WCAG) แนะนำอัตราส่วนความคมชัดอย่างน้อย 4.5:1 สำหรับข้อความปกติ และ 3:1 สำหรับข้อความขนาดใหญ่
  5. ข้อความ Alt สำหรับรูปภาพ: จัดเตรียมข้อความ alt ที่สื่อความหมายสำหรับรูปภาพทั้งหมด ข้อความ alt เป็นข้อความทางเลือกสำหรับรูปภาพที่โปรแกรมอ่านหน้าจอสามารถอ่านได้
  6. ป้ายกำกับฟอร์ม: เชื่อมโยงป้ายกำกับฟอร์มกับช่องป้อนข้อมูลที่สอดคล้องกันโดยใช้องค์ประกอบ `<label>` สิ่งนี้ทำให้ผู้ใช้เข้าใจได้ชัดเจนว่าคาดหวังข้อมูลอะไรในแต่ละช่อง
  7. การทดสอบด้วยโปรแกรมอ่านหน้าจอ: ทดสอบแอปพลิเคชันของคุณด้วยโปรแกรมอ่านหน้าจอเพื่อให้แน่ใจว่าผู้ใช้ที่มีความบกพร่องทางการมองเห็นสามารถเข้าถึงได้ โปรแกรมอ่านหน้าจอยอดนิยม ได้แก่ NVDA, JAWS และ VoiceOver

การนำการเข้าถึงได้มาใช้ใน Next.js App Router

  1. ใช้ Next.js Link Component: ใช้ `<Link>` component สำหรับการนำทาง มันมีฟีเจอร์การเข้าถึงได้ในตัว เช่น prefetching และการจัดการ focus
  2. การจัดการ Focus: เมื่อนำทางระหว่างหน้าหรือเปิด modals ตรวจสอบให้แน่ใจว่ามีการจัดการ focus อย่างเหมาะสม focus ควรถูกตั้งค่าไปยังองค์ประกอบที่สมเหตุสมผลที่สุดบนหน้าใหม่หรือ modal
  3. Custom Components ที่เข้าถึงได้: เมื่อสร้าง custom components ตรวจสอบให้แน่ใจว่าสามารถเข้าถึงได้โดยปฏิบัติตามหลักการที่ระบุไว้ข้างต้น ใช้ Semantic HTML, ARIA attributes และการนำทางด้วยคีย์บอร์ดเพื่อให้ components ของคุณใช้งานได้โดยทุกคน
  4. การ Linting และการทดสอบ: ใช้เครื่องมือ linting เช่น ESLint พร้อมปลั๊กอินการเข้าถึงได้เพื่อระบุปัญหาการเข้าถึงที่อาจเกิดขึ้นในโค้ดของคุณ นอกจากนี้ ให้ใช้เครื่องมือทดสอบอัตโนมัติเพื่อทดสอบแอปพลิเคชันของคุณว่ามีการละเมิดการเข้าถึงได้หรือไม่

สรุป

ระบบ file-based routing ของ Next.js App Router นำเสนอวิธีการที่ทรงพลังและใช้งานง่ายในการจัดโครงสร้างและนำทางแอปพลิเคชันของคุณ ด้วยการทำความเข้าใจแนวคิดหลักและแนวทางปฏิบัติที่ดีที่สุดที่สรุปไว้ในคู่มือนี้ คุณสามารถสร้างแอปพลิเคชัน Next.js ที่แข็งแกร่ง ขยายขนาดได้ และบำรุงรักษาง่าย ลองทดลองกับฟีเจอร์ต่างๆ ของ App Router และค้นพบว่ามันสามารถทำให้เวิร์กโฟลว์การพัฒนาของคุณง่ายขึ้นและปรับปรุงประสบการณ์ผู้ใช้ได้อย่างไร

Next.js App Router: คู่มือฉบับสมบูรณ์เกี่ยวกับ File-Based Routing | MLOG