Blog

/

Frontend

Next.js - App Router vs Pages Router

APP Router Pages Router

Posted at

2024年10月17日

Posted on

Frontend

如果你看過 Next.js 的文件,會看到兩套路由系統在並存:Pages Router 是老系統,App Router 是新系統。很多人還是在用 Pages Router,甚至不知道 App Router 是啥,今天我們聊聊這兩個東西。

特性

Pages Router

App Router

比較

多租戶隔離

需要手寫邏輯

Server Components 原生支援

App Router

資料獲取

getStaticProps/getServerSideProps

async/await 直接在 component

App Router

Metadata API

需要 Head 組件

原生支援

App Router

學習難度

簡單

比較難

Pages Router

API 路由

API Routes

Route Handlers

App Router

編譯速度

App Router

Pages Router

Pages Router 從 Next.js 早期就存在了。每個頁面都是一個 .tsx 檔案,資料獲取邏輯寫在 getStaticProps 或 getServerSideProps。

// pages/products/[id].tsx
export async function getStaticPaths() {
  const products = await fetch('api/products').then(r => r.json())
  return {
    paths: products.map(p => ({ params: { id: p.id.toString() } })),
    fallback: 'blocking'
  }
}

export async function getStaticProps({ params }) {
  const product = await fetch(`api/products/${params.id}`).then(r => r.json())
  return { props: { product }, revalidate: 3600 }
}

export default function Product({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </div>
  )
}

Pages Router 的特點是邏輯集中在一個地方。對初學者友好,但有個大問題:資料獲取和 UI 渲染混在一起,很難複用邏輯。而且 getStaticProps 這個概念對新手有點難理解。當項目變複雜,這個模式就顯得很累。

App Router 全新的思維

App Router 在 Next.js 13 引入,徹底改變了整個架構。它引入了 Server Components、Layouts、route segments 這些新概念。

最大的改進是 Layouts。Pages Router 沒有一個好的辦法共享 Layout,App Router 則把 Layout 變成了一等公民。

// app/layout.tsx (Root Layout)
export const metadata = {
  title: 'My Store',
  description: 'Product store'
}

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <nav>Navigation</nav>
        {children}
      </body>
    </html>
  )
}
// app/products/layout.tsx (Nested Layout)
export default function ProductLayout({ children }) {
  return (
    <div>
      <h1>Products</h1>
      <div className="product-sidebar">
        <h2>Filters</h2>
      </div>
      {children}
    </div>
  )
}

每個資料夾可以有自己的 layout.tsx,會自動套用到這個資料夾以下的所有頁面,這個設計真ㄉ是簡潔到不行。

Server Components: 核心差異

App Router 引入了 Server Components 這個概念。Default 情況下,App Router 裡的組件都在伺服器運行,不需要 JavaScript 傳到瀏覽器,最大的好處是可以直接查資料庫。

// app/products/[id]/page.tsx - Server Component
async function getProductDetails(id: string) {
  const product = await db.products.findById(id)
  return product
}

export async function generateStaticParams() {
  const products = await db.products.getAll()
  return products.map(p => ({ id: p.id.toString() }))
}

export const revalidate = 3600

export default async function Product({ params }: { params: { id: string } }) {
  const product = await getProductDetails(params.id)
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
      <AddToCartButton productId={params.id} />
    </div>
  )
}
// app/products/AddToCartButton.tsx - Client Component
'use client'

import { useState } from 'react'

export default function AddToCartButton({ productId }: { productId: string }) {
  const [count, setCount] = useState(1)

  const addToCart = async () => {
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, count })
    })
  }

  return (
    <div>
      <input type="number" value={count} onChange={e => setCount(+e.target.value)} />
      <button onClick={addToCart}>Add to Cart</button>
    </div>
  )
}

注意看。Server Component 裡可以直接調用 db.products.findById(),不用寫 API 路由。Client Component 只用來處理互動邏輯。這改變了一切。而 Pages Router 沒有這個概念,所以要查資料庫必須寫 API 路由,多一層複雜性。而且資料庫 credentials 會洩露到瀏覽器,這是個安全問題。

Route Handlers: API 路由的進化

如果你今天要調用 API,App Router 的 Route Handlers 比 Pages Router 的 API Routes 舒服得多。宋!

// app/api/cart/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { productId, count } = await request.json()
  
  const result = await db.cart.add(productId, count)
  
  return NextResponse.json({ success: true, cart: result })
}

export async function GET(request: NextRequest) {
  const userId = request.nextUrl.searchParams.get('userId')
  const cart = await db.cart.getByUserId(userId)
  
  return NextResponse.json(cart)
}

Request 和 Response 物件更接近標準的 Web API,邏輯清晰。Pages Router 的 API Routes 也能用,但語法更繁瑣。

不是 App Router 就一定更好

但 App Router 不是完美的,寫起來可能比較複雜一點。Server Components 這個概念對習慣了 React 的人來說很陌生。你要思考哪些組件該在伺服器,哪些該在客戶端,一開始會很累。

Pages Router 反而對初學者友好,邏輯都在頁面裡,想不了太多就能寫。但一旦項目變複雜,Pages Router 的限制就顯現出來了。

還有個實際問題:Pages Router 用不了 Metadata API,要用 Head 組件自己管。App Router 用 metadata 直接定義,Vercel 會自動生成對應的 HTML meta tags,小東西但很方便。

什麼時候用 Pages Router

其實現在已經沒什麼理由用 Pages Router 了,Vercel 官方已經推薦 App Router。唯一的例外是你在維護一個老專案,整個 codebase 都是 Pages Router,遷移成本太高。這時候繼續用 Pages Router 可以理解。但如果你在新建專案,就應該用 App Router。

遷移策略

如果要遷移,Vercel 其實設計得很聰明。你可以同時運行兩個路由系統。

新功能在 app 資料夾用 App Router 寫,老功能在 pages 資料夾繼續用 Pages Router。Next.js 會把兩套系統合併,按優先級處理衝突(App Router 優先級更高)。

遷移不需要一次搞完 (也應該比遷移 API簡單,最近幫 API 搬家搬到哭 murmur 一下)。先把最常改的頁面遷移,因為 App Router 編譯快一點,把 API 路由遷移成 Route Handlers,慢慢把 pages 清空。

實際遷移的工作量沒想像中多,主要是改 getStaticProps/getServerSideProps 成 async component,改 pages 資料夾結構成 app。最難的是理解 Server Components 的思維方式,但這個瓶頸只要突破一次,後面就很順。

我的建議

新專案用 App Router,老專案如果還在積極開發,可以用混合策略。如果老專案不維護了,Pages Router 也能用,就是沒那麼舒服。

App Router 現在已經很穩定了。性能、開發體驗都領先 Pages Router。唯一的成本是學習曲線陡,但值得。

下次來講講如何設計一個 Next.js 項目的架構,包括檔案夾結構、模組分割、共享邏輯。