如何從 Create React App 遷移至 Next.js

本指南將協助您將現有的 Create React App (CRA) 網站遷移至 Next.js。

為什麼要遷移?

以下是您可能想從 Create React App 遷移至 Next.js 的幾個原因:

初始頁面載入時間緩慢

Create React App 僅使用客戶端 React。純客戶端應用程式(也稱為單頁應用程式 (SPA))通常會遇到初始頁面載入緩慢的問題。這有幾個原因:

  1. 瀏覽器需要等待 React 程式碼和整個應用程式套件下載並執行後,您的程式碼才能發送請求載入資料。
  2. 隨著新增功能和依賴項,您的應用程式程式碼會不斷增長。

沒有自動程式碼分割

上述載入時間緩慢的問題可以透過程式碼分割稍微緩解。但如果您嘗試手動進行程式碼分割,可能會無意中引入網路瀑布效應。Next.js 在其路由器和建置管道中內建了自動程式碼分割和 tree-shaking 功能。

網路瀑布效應

效能不佳的常見原因是應用程式為了獲取資料而進行連續的客戶端-伺服器請求。SPA 中獲取資料的一種模式是先渲染佔位符,然後在元件掛載後獲取資料。不幸的是,子元件只能在父元件完成載入自己的資料後才能開始獲取資料,從而導致請求的「瀑布」效應。

雖然 Next.js 支援客戶端資料獲取,但它也允許您將資料獲取移至伺服器端。這通常可以完全消除客戶端-伺服器的瀑布效應。

快速且可控的載入狀態

透過內建的透過 React Suspense 進行串流支援,您可以定義 UI 的哪些部分先載入以及載入順序,而不會產生網路瀑布效應。

這使您能夠構建載入速度更快的頁面,並消除版面偏移

選擇資料獲取策略

根據需求,Next.js 允許您在頁面或元件層級選擇資料獲取策略。例如,您可以從 CMS 獲取資料並在建置時 (SSG) 渲染部落格文章以實現快速載入,或在必要時在請求時 (SSR) 獲取資料。

中介軟體

Next.js 中介軟體允許您在請求完成前在伺服器上執行程式碼。例如,您可以透過在中介軟體中將使用者重定向到登入頁面來避免未驗證內容的閃爍。您還可以將其用於 A/B 測試、實驗和國際化等功能。

內建優化

圖片字型第三方腳本通常對應用程式效能有很大影響。Next.js 包含專門的元件和 API,可自動為您優化這些資源。

遷移步驟

我們的目標是盡快獲得一個可運行的 Next.js 應用程式,以便您可以逐步採用 Next.js 的功能。首先,我們將您的應用程式視為純客戶端應用程式 (SPA),而不立即替換現有的路由器。這減少了複雜性和合併衝突。

注意:如果您使用進階的 CRA 配置,例如 package.json 中的自訂 homepage 欄位、自訂 service worker 或特定的 Babel/webpack 調整,請參閱本指南末尾的其他注意事項部分,以獲取在 Next.js 中複製或調整這些功能的提示。

步驟 1:安裝 Next.js 依賴項

在現有專案中安裝 Next.js:

終端機
npm install next@latest

步驟 2:建立 Next.js 配置檔案

在專案根目錄(與 package.json 同級)建立 next.config.ts。此檔案包含您的 Next.js 配置選項

next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export', // 輸出單頁應用程式 (SPA)
  distDir: 'build', // 將建置輸出目錄更改為 `build`
}

export default nextConfig

注意:使用 output: 'export' 表示您正在進行靜態匯出。您將無法存取伺服器端功能,如 SSR 或 API。您可以移除此行以利用 Next.js 的伺服器功能。

步驟 3:建立根佈局

Next.js App Router 應用程式必須包含一個 根佈局檔案,這是一個 React 伺服器元件,將包裹所有頁面。

在 CRA 應用程式中,最接近根佈局檔案的是 public/index.html,其中包含您的 <html><head><body> 標籤。

  1. src 資料夾內建立一個新的 app 目錄(或者如果您希望 app 位於根目錄,則在專案根目錄建立)。
  2. app 目錄內建立 layout.tsx(或 layout.js)檔案:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}
export default function RootLayout({ children }) {
  return '...'
}

現在將舊的 index.html 內容複製到此 <RootLayout> 元件中。將 body div#root(和 body noscript)替換為 <div id="root">{children}</div>

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

小提示:Next.js 預設會忽略 CRA 的 public/manifest.json、額外的圖示和測試配置。如果您需要這些,Next.js 透過其Metadata API測試設定提供支援。

步驟 4:元資料

Next.js 自動包含 <meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /> 標籤,因此您可以從 <head> 中移除它們:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

任何元資料檔案,如 favicon.icoicon.pngrobots.txt,只要將它們放置在 app 目錄的頂層,就會自動添加到應用程式的 <head> 標籤中。將所有支援的檔案移至 app 目錄後,您可以安全地刪除它們的 <link> 標籤:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

最後,Next.js 可以透過 Metadata API 管理您的最後一個 <head> 標籤。將您的最終元資料資訊移至匯出的 metadata 物件

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

透過上述變更,您從在 index.html 中宣告所有內容轉變為使用 Next.js 內建的基於慣例的方法(Metadata API)。這種方法使您能夠更輕鬆地改善頁面的 SEO 和網路分享性。

步驟 5:樣式

與 CRA 一樣,Next.js 開箱即用支援 CSS Modules。它還支援全域 CSS 導入

如果您有全域 CSS 檔案,請將其導入到 app/layout.tsx

import '../index.css'

export const metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

如果您使用 Tailwind CSS,請參閱我們的安裝文件

步驟 6:建立入口點頁面

Create React App 使用 src/index.tsx(或 index.js)作為入口點。在 Next.js (App Router) 中,app 目錄內的每個資料夾對應一個路由,每個資料夾應有一個 page.tsx

由於我們希望暫時將應用程式保持為 SPA 並攔截所有路由,我們將使用可選的 catch-all 路由

  1. app 內建立 [[...slug]] 目錄。
app
 [[...slug]]
 page.tsx
 layout.tsx
  1. 將以下內容添加到 page.tsx
export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // 我們稍後會更新此處
}
export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // 我們稍後會更新此處
}

這告訴 Next.js 為空 slug (/) 生成單一路由,有效地將所有路由映射到同一頁面。此頁面是一個伺服器元件,預先渲染為靜態 HTML。

步驟 7:添加僅客戶端入口點

接下來,我們將把 CRA 的根 App 元件嵌入到客戶端元件中,以便所有邏輯保留在客戶端。如果您是第一次使用 Next.js,值得知道的是客戶端元件(預設情況下)仍然會在伺服器上預先渲染。您可以將它們視為具有執行客戶端 JavaScript 的額外能力。

app/[[...slug]]/ 中建立 client.tsx(或 client.js):

'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
  • 'use client' 指令使此檔案成為客戶端元件
  • 帶有 ssr: falsedynamic 導入會禁用 <App /> 元件的伺服器端渲染,使其成為真正的僅客戶端 (SPA)。

現在更新您的 page.tsx(或 page.js)以使用新元件:

import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

步驟 8:更新靜態圖片匯入方式

在 CRA 中,匯入圖片檔案會回傳其公開 URL 作為字串:

import image from './img.png'

export default function App() {
  return <img src={image} />
}

在 Next.js 中,靜態圖片匯入會回傳一個物件。這個物件可以直接用於 Next.js 的 <Image> 元件,或者你也可以使用物件的 src 屬性搭配現有的 <img> 標籤。

<Image> 元件具有自動圖片最佳化的額外優勢。<Image> 元件會根據圖片的尺寸自動設定結果 <img>widthheight 屬性,這可以防止圖片載入時的版面位移。然而,如果你的應用程式中的圖片只有其中一個維度被設定樣式,而另一個維度沒有設定為 auto,這可能會導致問題。當沒有設定為 auto 時,該維度會預設為 <img> 維度屬性的值,這可能導致圖片顯示變形。

保留 <img> 標籤可以減少應用程式中的變更量,並避免上述問題。之後你可以選擇性地遷移到 <Image> 元件,透過設定 loader 來利用圖片最佳化的優勢,或者轉移到預設的 Next.js 伺服器,該伺服器具有自動圖片最佳化功能。

將從 /public 匯入的圖片的絕對路徑轉換為相對路徑:

// 之前
import logo from '/logo.png'

// 之後
import logo from '../public/logo.png'

將圖片的 src 屬性傳遞給 <img> 標籤,而不是整個圖片物件:

// 之前
<img src={logo} />

// 之後
<img src={logo.src} />

或者,你也可以根據檔案名稱參考圖片資源的公開 URL。例如,public/logo.png 將會在你的應用程式中以 /logo.png 提供圖片,這將是 src 的值。

警告: 如果你使用 TypeScript,在存取 src 屬性時可能會遇到型別錯誤。要解決這些問題,你需要將 next-env.d.ts 加入到 tsconfig.json 檔案的 include 陣列中。Next.js 會在步驟 9 中執行應用程式時自動產生這個檔案。

步驟 9:遷移環境變數

Next.js 支援環境變數,方式與 CRA 類似,但需要為任何你想在瀏覽器中公開的變數加上 NEXT_PUBLIC_ 前綴。

主要的差異在於用於在客戶端公開環境變數的前綴。將所有帶有 REACT_APP_ 前綴的環境變數更改為 NEXT_PUBLIC_

步驟 10:更新 package.json 中的腳本

更新你的 package.json 腳本以使用 Next.js 指令。同時,將 .nextnext-env.d.ts 加入到你的 .gitignore

package.json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "npx serve@latest ./build"
  }
}
.gitignore
# ...
.next
next-env.d.ts

現在你可以執行:

npm run dev

開啟 http://localhost:3000。你應該會看到你的應用程式現在在 Next.js 上執行(以 SPA 模式)。

步驟 11:清理

你現在可以移除特定於 Create React App 的檔案:

  • public/index.html
  • src/index.tsx
  • src/react-app-env.d.ts
  • reportWebVitals 設定
  • react-scripts 依賴(從 package.json 中解除安裝)

其他考量

在 CRA 中使用自訂 homepage

如果你在 CRA 的 package.json 中使用 homepage 欄位來在特定子路徑下提供應用程式,你可以在 Next.js 中使用 basePath 設定來複製這個功能:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  basePath: '/my-subpath',
  // ...
}

export default nextConfig

處理自訂 Service Worker

如果你使用了 CRA 的 service worker(例如來自 create-react-appserviceWorker.js),你可以學習如何在 Next.js 中建立漸進式網路應用程式 (PWA)

代理 API 請求

如果你的 CRA 應用程式使用 package.json 中的 proxy 欄位來轉發請求到後端伺服器,你可以在 next.config.ts 中使用 Next.js rewrites 來複製這個功能:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://your-backend.com/:path*',
      },
    ]
  },
}

自訂 Webpack / Babel 設定

如果你在 CRA 中有自訂的 webpack 或 Babel 設定,你可以在 next.config.ts 中擴展 Next.js 的設定:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (config, { isServer }) => {
    // 在這裡修改 webpack 設定
    return config
  },
}

export default nextConfig

注意: 這需要透過從 dev 腳本中移除 --turbopack 來停用 Turbopack。

TypeScript 設定

如果你有 tsconfig.json,Next.js 會自動設定 TypeScript。確保 next-env.d.ts 列在你的 tsconfig.jsoninclude 陣列中:

{
  "include": ["next-env.d.ts", "app/**/*", "src/**/*"]
}

打包工具相容性

Create React App 和 Next.js 預設都使用 webpack 進行打包。Next.js 還提供 Turbopack 以獲得更快的本地開發:

next dev --turbopack

如果你需要從 CRA 遷移進階的 webpack 設定,你仍然可以提供自訂的 webpack 設定

下一步

如果一切順利,你現在有一個運作正常的 Next.js 應用程式,以單頁應用程式模式執行。你還沒有利用 Next.js 的功能,如伺服器端渲染或基於檔案的路由,但你現在可以逐步進行:

注意: 使用靜態匯出 (output: 'export') 目前不支援 useParams 鉤子或其他伺服器功能。要使用所有 Next.js 功能,請從 next.config.ts 中移除 output: 'export'