跳至內容
返回部落格

2023 年 5 月 4 日,星期四

Next.js 13.4

發佈者

Next.js 13.4 是一個奠基性的版本,標誌著 App Router 的穩定性

自六個月前 Next.js 13 發布以來,我們一直專注於為 Next.js 的未來(App Router)構建基礎,並以漸進式的方式採用,避免不必要的重大變更。

今天,隨著 13.4 版本的發布,您現在可以開始在生產環境中採用 App Router 了。

終端機
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

Next.js App Router

我們於 2016 年發布了 Next.js,以提供一種輕鬆進行伺服器端渲染 React 應用程式的方式,目標是創造更動態、更個人化且更全球化的網路。

在最初的公告文章中,我們分享了一些 Next.js 的設計原則

  • 零設定。將檔案系統作為 API 使用
  • 僅使用 JavaScript。一切皆為函式
  • 自動伺服器端渲染和程式碼分割
  • 資料擷取由開發者決定

Next.js 現在已經六年了。我們最初的設計原則仍然保留著,隨著越來越多的開發者和公司採用 Next.js,我們一直在努力對框架進行基礎升級,以更好地實現這些原則。

我們一直在開發下一代的 Next.js,而今天隨著 13.4 版本的推出,這個下一代已經穩定,可以正式採用。這篇文章將分享更多關於我們對於 App Router 的設計決策和選擇。

零設定。將檔案系統作為 API 使用

基於檔案系統的路由 一直是 Next.js 的核心功能。在我們最初的文章中,我們展示了如何從單個 React 元件建立路由的範例

pages/about.js
// Pages Router
 
import React from 'react';
export default () => <h1>About us</h1>;

無需額外設定。只要將檔案放入 pages/ 資料夾中,Next.js 路由器就會處理剩下的工作。我們仍然喜愛這種路由的簡潔性。但隨著框架使用量的增長,開發者希望用它構建的介面類型也越來越多。

開發者們一直希望能更好地支援定義佈局、巢狀 UI 片段作為佈局,以及在定義載入和錯誤狀態時有更大的彈性。將這些功能改進到現有的 Next.js 路由器中並不容易。

框架的每個部分都必須圍繞路由器進行設計。頁面轉場、資料擷取、快取、變更和重新驗證資料、串流、樣式內容等等。

為了使我們的路由器與串流相容,並滿足這些對於增強佈局支援的需求,我們著手構建新版本的路由器。

這是我們在最初發布 佈局 RFC 後的成果。

app/layout.js
// New: App Router ✨
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
app/page.js
export default function Page() {
  return <h1>Hello, Next.js!</h1>;
}

比你看到的更重要的是你*沒看到*的。這個新的路由器(可以透過 app/ 目錄逐步採用)有著完全不同的架構,建立在 React 伺服器組件Suspense 的基礎之上。

這個基礎讓我們得以移除最初為了擴展 React 原始功能而開發的 Next.js 特定 API。例如,你不再需要使用自定義的 _app 檔案來自定義全域共享的佈局。

pages/_app.js
// Pages Router
 
// This "global layout" wraps all routes. There's no way to
// compose other layout components, and you cannot fetch global
// data from this file.
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

使用 Pages Router 時,佈局無法組合,資料擷取也無法與組件放在一起。新的 App Router 現在支援這些功能。

app/layout.js
// New: App Router ✨
// The root layout is shared for the entire application
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
app/dashboard/layout.js
// Layouts can be nested and composed
export default function DashboardLayout({ children }) {
  return (
    <section>
      <h1>Dashboard</h1>
      {children}
    </section>
  );
}

使用 Pages Router 時,_document 用於自定義伺服器的初始負載。

pages/_document.js
// Pages Router
 
// This file allows you to customize the <html> and <body> tags
// for the server request, but adds framework-specific features
// rather than writing HTML elements.
import { Html, Head, Main, NextScript } from 'next/document';
 
export default function Document() {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

使用 App Router,你不再需要從 Next.js 導入 <Html><Head><Body>。你只需要使用 React 即可。

app/layout.js
// New: App Router ✨
// The root layout is shared for the entire application
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

構建新的檔案系統路由器的機會也是解決許多其他與路由系統相關的功能請求的適當時機。例如:

  • 以前,你只能從 _app.js 中的外部 npm 套件(例如組件庫)導入全域樣式表。這不是理想的開發體驗。使用 App Router,你可以在任何組件中導入(並與組件放在一起)任何 CSS 檔案。
  • 以前,使用 Next.js 選擇伺服器端渲染(透過 getServerSideProps)意味著與應用程式的互動會被阻塞,直到整個頁面都完成水合作用。使用 App Router,我們重構了架構,使其與 React Suspense 深度整合,這意味著我們可以選擇性地水合頁面的部分內容,而不會阻塞使用者介面中其他組件的互動。內容可以從伺服器即時串流,從而改善頁面的感知載入效能。

路由器 是 Next.js 運作的核心。但重點不在於路由器本身,而在於它如何整合框架的其他部分,例如 資料擷取

僅限 JavaScript。一切皆為函式

Next.js 和 React 開發人員希望編寫 JavaScript 和 TypeScript 程式碼,並將應用程式組件組合在一起。從我們的原始文章來看:

pages/index.js
import React from 'react';
import Head from 'next/head';
 
export default () => (
  <div>
    <Head>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
    </Head>
    <h1>Hi. I'm mobile-ready!</h1>
  </div>
);

在未來版本的 Next.js 中,我們添加了一項 DX 改進,可自動為您導入 React。

此組件封裝了可以在應用程式中的任何位置重複使用和組合的邏輯。搭配檔案系統路由,這意味著可以輕鬆開始構建感覺像在編寫 JavaScript 和 HTML 的 React 應用程式。

例如,如果您想擷取一些資料,原始版本的 Next.js 看起來像這樣:

pages/index.js
import React from 'react';
import 'isomorphic-fetch';
 
export default class extends React.Component {
  static async getInitialProps() {
    const res = await fetch('https://api.company.com/user/123');
    const data = await res.json();
    return { username: data.profile.username };
  }
}

在未來版本的 Next.js 中,我們添加了一項 DX 改進,它填充了 fetch,因此您不需要導入 isomorphic-fetchnode-fetch,並且可以在客戶端和伺服器上使用 Web fetch API

隨著採用率的提高和框架的成熟,我們探索了新的資料擷取模式。

getInitialProps 在伺服器*和*客戶端上運行。此 API 擴展了 React 組件,允許您建立 Promise 並將結果轉發到組件的 props

雖然 getInitialProps 至今仍然有效,但基於客戶的回饋,我們接著迭代開發了下一代的資料擷取 API:getServerSidePropsgetStaticProps

pages/index.js
// Generate a static version of the route
export async function getStaticProps(context) {
  return { props: {} };
}
// Or dynamically server-render the route
export async function getServerSideProps(context) {
  return { props: {} };
}

這些 API 更清楚地表明了程式碼的執行位置,無論是在客戶端還是伺服器端,並且允許 Next.js 應用程式進行自動靜態優化。此外,它還允許靜態匯出,讓 Next.js 可以部署到不支援伺服器的環境(例如 AWS S3 儲存桶)。

然而,這並非「純粹的 JavaScript」,我們希望更貼近我們最初的設計原則。

自 Next.js 創建以來,我們一直與 Meta 的 React 核心團隊緊密合作,在 React 基礎元件之上建構框架功能。我們的合作關係,加上 React 核心團隊多年的研究和開發,讓 Next.js 有機會透過最新版本的 React 架構(包含伺服器元件)來實現我們的目標。

使用 App Router,您可以使用熟悉的 asyncawait 語法來擷取資料。無需學習新的 API。預設情況下,所有元件都是 React 伺服器元件,因此資料擷取會安全地在伺服器上進行。例如:

app/page.js
export default async function Page() {
  const res = await fetch('https://api.example.com/...');
  // The return value is *not* serialized
  // You can use Date, Map, Set, etc.
  const data = res.json();
 
  return '...';
}

關鍵的是,「資料擷取由開發者決定」的原則得以實現。您可以擷取資料並組合*任何*元件。不僅僅是第一方元件,而是伺服器元件生態系統中的*任何*元件,例如已設計用於與伺服器元件整合並完全在伺服器上執行的Twitter 嵌入 react-tweet

app/page.js
import { Tweet } from 'react-tweet';
 
export default async function Page() {
  return <Tweet id="790942692909916160" />;
}

由於路由器與React Suspense整合,您可以在部分內容載入時更流暢地顯示 fallback 內容,並根據需要逐步顯示內容。

app/page.js
import { Suspense } from 'react';
import { PostFeed, Weather } from './components';
 
export default function Page() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  );
}

此外,路由器將頁面導覽標記為轉場,讓路由轉場可以被中斷。

自動伺服器渲染和程式碼分割

在我們創建 Next.js 時,開發者手動設定 webpack、babel 和其他工具來執行 React 應用程式仍然很常見。新增伺服器渲染或程式碼分割等進一步的優化通常不會在客製化解決方案中實作。Next.js 以及其他 React 框架建立了一個抽象層來實作並強制執行這些最佳實務。

基於路由的程式碼分割意味著 pages/ 目錄中的每個檔案都會被程式碼分割成自己的 JavaScript bundle,有助於減少檔案大小並提升初始頁面載入效能。

這對伺服器渲染應用程式和使用 Next.js 的單頁應用程式都有好處,因為後者通常在應用程式啟動時載入一個大型的 JavaScript 捆綁包。然而,要實現元件級別的程式碼分割,開發人員需要使用 next/dynamic 來動態導入元件。

app/page.tsx
import dynamic from 'next/dynamic';
 
const DynamicHeader = dynamic(() => import('../components/header'), {
  loading: () => <p>Loading...</p>,
});
 
export default function Home() {
  return <DynamicHeader />;
}

使用 App Router,伺服器元件不會包含在瀏覽器的 JavaScript 捆綁包中。客戶端元件 預設會自動進行程式碼分割(在 Next.js 中使用 webpack 或 Turbopack)。此外,由於整個路由器架構都支援串流和 Suspense,您可以逐步將 UI 的各個部分從伺服器發送到客戶端。

例如,您可以使用條件邏輯對整個程式碼路徑進行程式碼分割。在此範例中,您不需要為已登出的使用者載入儀表板的客戶端 JavaScript。

app/layout.tsx
import { getUser } from './auth';
import { Dashboard, Landing } from './components';
 
export default async function Layout() {
  const isLoggedIn = await getUser();
  return isLoggedIn ? <Dashboard /> : <Landing />;
}

Turbopack (測試版)

Turbopack,我們正在透過 Next.js 測試和穩定化的新一代打包工具,有助於在開發 Next.js 應用程式時加快本地迭代速度(透過 next dev --turbo),並且很快就能加快您的正式版建置速度(next build --turbo)。

自 Next.js 13 中的 Alpha 版本發布以來,隨著我們不斷修復錯誤並新增對缺失功能的支援,採用率穩步增長。我們一直在 Vercel.com 上以及與許多經營大型 Next.js 網站的 Vercel 客戶一起進行 Turbopack 的實際應用測試,以收集回饋並提高穩定性。我們非常感謝社群在測試和向我們的團隊回報錯誤方面的支持。

現在,六個月後,我們準備進入 Beta 測試階段。

Turbopack 尚未與 webpack 和 Next.js 完全功能對等。我們正在這個 issue 中追蹤這些功能的支援進度。不過,大多數的使用案例現在應該都已支援。我們在此 Beta 測試階段的目標是繼續解決因採用率提高而產生的剩餘錯誤,並為未來版本的穩定性做好準備。

我們對 Turbopack 增量引擎和快取層的改進投入,不僅將加快本地開發速度,也將很快加快正式版建置速度。敬請期待未來的 Next.js 版本,屆時您將能夠執行 next build --turbo 來進行即時建置。

立即在 Next.js 13.4 中使用 next dev --turbo 試用 Turbopack Beta 測試版。

伺服器動作 (Alpha)

React 生態圈在表單、管理表單狀態、資料快取和重新驗證等方面,出現了許多創新和探索。隨著時間推移,React 對其中一些模式的看法也更加明確。例如,推薦使用「非受控元件」(uncontrolled components) 來管理表單狀態。

目前的解決方案生態系統,要不是可重複使用的客戶端解決方案,要不就是框架內建的基礎元件。到目前為止,還沒有辦法組合伺服器端異動和資料基本元素。React 團隊一直在努力開發第一方異動解決方案。

我們很高興地宣布,Next.js 現在實驗性地支援伺服器動作,讓您可以在伺服器上直接呼叫函式來異動資料,而不需要建立中間的 API 層。

app/post/[id]/page.tsx (伺服器元件)
import kv from './kv';
 
export default function Page({ params }) {
  async function increment() {
    'use server';
    await kv.incr(`post:id:${params.id}`);
  }
 
  return (
    <form action={increment}>
      <button type="submit">Like</button>
    </form>
  );
}

使用伺服器動作,您可以擁有強大的伺服器優先資料異動功能、更少的客戶端 JavaScript 程式碼,以及漸進式增強的表單。

app/post/new/page.tsx (伺服器元件)
import db from './db';
import { redirect } from 'next/navigation';
 
async function create(formData: FormData) {
  'use server';
  const post = await db.post.insert({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  redirect(`/blog/${post.slug}`);
}
 
export default function Page() {
  return (
    <form action={create}>
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">Submit</button>
    </form>
  );
}

Next.js 中的伺服器動作旨在與其餘的資料生命週期深度整合,包括 Next.js 快取、增量靜態再生 (ISR) 和客戶端路由器。

透過新的 API revalidatePathrevalidateTag 重新驗證資料,意味著異動、重新渲染頁面或重新導向都可以在一次網路往返中完成,即使上游提供者速度緩慢,也能確保在客戶端顯示正確的資料。

app/dashboard/posts/page.tsx (伺服器元件)
import db from './db';
import { revalidateTag } from 'next/cache';
 
async function update(formData: FormData) {
  'use server';
  await db.post.update({
    title: formData.get('title'),
  });
  revalidateTag('posts');
}
 
export default async function Page() {
  const res = await fetch('https://...', { next: { tags: ['posts'] } });
  const data = await res.json();
  // ...
}

伺服器動作的設計是可組合的。React 社群中的任何人都可以建構和發佈伺服器動作,並在生態系統中發佈它們。就像伺服器元件一樣,我們對客戶端和伺服器端可組合基本元素的新時代感到興奮。

Next.js 13.4 版已提供 Alpha 階段的伺服器動作。選擇使用伺服器動作,Next.js 將使用 React 的實驗版本。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
};
 
module.exports = nextConfig;

其他改進 草稿模式:從您的Headless CMS擷取並渲染草稿內容。草稿模式在pagesapp目錄中皆可運作。我們強化並簡化了現有的預覽模式 API,其仍繼續適用於pages目錄。預覽模式在app目錄中*無法*運作—您應該使用草稿模式。

常見問題

App Router 的穩定性代表什麼意思?

今天將 App Router 標記為穩定版並不代表我們的工作已經完成。穩定性意味著 App Router 的核心已可用于正式環境,並且已經過我們內部測試以及許多 Next.js 早期採用者的驗證。

未來我們仍會進行其他優化,包括使伺服器動作達到完全穩定。對我們來說,推動核心穩定性非常重要,這有助於社群清楚了解他們現在應該從哪裡開始學習和建構應用程式。

App Router 建構在 React canary 版本之上,現在已準備好讓框架採用伺服器元件等功能。了解更多

這對 Next.js Beta 版文件有什麼影響?

從今天開始,我們建議使用 App Router 建構新的應用程式。用於說明 App Router 並從頭改寫的 Next.js Beta 版文件,現已合併回Next.js 正式版文件。您現在可以輕鬆地在 App Router 或 Pages Router 之間切換。

我們建議閱讀App Router 漸進式採用指南,以了解如何採用 App Router。

Pages Router 會被淘汰嗎?

不會。我們承諾會持續支援 pages/ 的開發,包含錯誤修復、效能提升以及安全性更新,並在未來支援多個主要版本。我們希望確保開發者有足夠的時間,在準備就緒後逐步採用 App Router。

在正式環境中同時使用 pages/app/ 是受到支援且鼓勵的。App Router 可以逐個路由逐步採用。

這是否意味著伺服器元件(Server Components)已經「完成」了?

Next.js 是一個選擇基於 React 架構(包含伺服器元件)建構的框架。我們希望 App Router 提供的體驗也能鼓勵其他框架(或新的框架)考慮使用此架構。

在這個生態系統中,仍然有一些模式尚未被定義,例如處理無限滾動。目前,我們建議針對這些模式使用客戶端解決方案,同時等待生態系統的發展以及函式庫的創建或更新。

社群

Next.js 是超過 2,600 位個別開發者、Google 和 Meta 等產業合作夥伴,以及我們在 Vercel 的核心團隊共同努力的成果。歡迎加入我們的社群,在 GitHub 討論區RedditDiscord 上與我們交流。

此版本由以下人員/團隊貢獻:

以及以下貢獻者的付出:@shuding、@huozhi、@wyattfry、@styfle、@sreetamdas、@afonsojramos、@timneutkens、@alexkirsz、@chriswdmr、@jankaifer、@pn-code、@kdy1、@sokra、@kwonoj、@martin-wahlberg、@Kikobeats、@JTaylor0196、@sebmarkbage、@ijjk、@gnoff、@jridgewell、@sagarpreet-xflowpay、@balazsorban44、@cprussin、@ForsakenHarmony、@li-jia-nan、@dciug、@albertothedev、@DuCanhGH、@feedthejim、@patrick91、@padmaia、@sophiebits、@eps1lon、@reconbot、@acdlite、@cjmling、@nabsul、@motopods、@hanneslund、@tunamagur0、@devknoll、@apeltop、@maranomynet、@y-tsubuku、@EndangeredMassa、@ykzts、@AviAvinav、@adilansari、@wyattjoh、@charkour、@delbaoliveira、@agadzik、@Just-Moh-it、@rodrigofeijao、@leerob、@juliusmarminge、@koba04、@Phiction、@jessewarren-aa、@ryo-manba、@Yovach 以及 @dylanjha。