從 Create React App 遷移

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

為什麼要遷移?

從 Create React App 遷移至 Next.js 有以下幾個主要原因:

初始頁面載入時間過長

Create React App 僅使用客戶端 (client-side) 的 React。純客戶端應用程式(又稱單頁應用程式 SPA)通常會遇到初始頁面載入時間過長的問題,原因如下:

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

缺乏自動程式碼分割

雖然可以透過手動程式碼分割來緩解載入時間過長的問題,但手動操作往往會導致效能更差。手動程式碼分割容易無意中引入網路瀑布式請求。Next.js 的路由器內建了自動程式碼分割功能。

網路瀑布式請求

應用程式在獲取資料時,若採用客戶端-伺服器順序請求的方式,通常會導致效能不佳。SPA 中常見的資料獲取模式是先渲染佔位內容,然後在元件掛載後再獲取資料。這意味著子元件必須等待父元件完成資料載入後,才能開始獲取自己的資料。

雖然 Next.js 支援在客戶端獲取資料,但它也提供了將資料獲取移至伺服器的選項,從而消除客戶端-伺服器的瀑布式請求。

快速且可控的載入狀態

透過內建的 React Suspense 串流支援,您可以更精確地控制 UI 的哪些部分應優先載入及其順序,同時避免網路瀑布式請求。

這讓您能夠建立載入速度更快的頁面,並消除 版面位移

選擇資料獲取策略

Next.js 允許您根據頁面和元件的需求選擇資料獲取策略。您可以選擇在建置時、伺服器請求時或在客戶端獲取資料。例如,您可以從 CMS 獲取資料並在建置時渲染部落格文章,然後將其高效緩存在 CDN 上。

中介軟體 (Middleware)

Next.js 中介軟體 允許您在請求完成前在伺服器上執行程式碼。這對於避免用戶訪問需驗證的頁面時出現未驗證內容的閃爍非常有用,因為可以將用戶重定向至登入頁面。中介軟體也適用於實驗和 國際化

內建優化

圖片字型第三方腳本 通常對應用程式效能有顯著影響。Next.js 提供了內建元件,可自動為您優化這些資源。

遷移步驟

本次遷移的目標是盡快建立一個可運行的 Next.js 應用程式,以便後續逐步採用 Next.js 的功能。首先,我們將保持其為純客戶端應用程式 (SPA),不遷移現有的路由。這有助於減少遷移過程中遇到問題的可能性,並降低合併衝突。

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

首先,安裝 next 作為依賴項:

Terminal
npm install next@latest

步驟 2:建立 Next.js 設定檔

在專案根目錄建立 next.config.mjs 檔案。此檔案將包含 Next.js 設定選項

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // 輸出單頁應用程式 (SPA)。
  distDir: './dist', // 將建置輸出目錄更改為 `./dist/`。
}

export default nextConfig

步驟 3:更新 TypeScript 設定

如果使用 TypeScript,您需要更新 tsconfig.json 檔案以使其與 Next.js 相容:

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "strictNullChecks": true
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "./dist/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

更多關於設定 TypeScript 的資訊,請參閱 Next.js 文件

步驟 4:建立根佈局 (Root Layout)

Next.js App Router 應用程式必須包含一個 根佈局 檔案,這是一個 React 伺服器元件,將包裹應用程式中的所有頁面。此檔案定義在 app 目錄的頂層。

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

在此步驟中,您將把 index.html 檔案轉換為根佈局檔案:

  1. src 目錄中建立新的 app 目錄。
  2. app 目錄中建立新的 layout.tsx 檔案:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}

小提示:佈局檔案可以使用 .js.jsx.tsx 副檔名。

index.html 檔案的內容複製到先前建立的 <RootLayout> 元件中,並將 body.div#rootbody.script 標籤替換為 <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>
  )
}

小提示:我們將忽略 manifest 檔案、favicon 以外的其他圖示以及 測試設定,但如果需要,Next.js 也支援這些選項。

步驟 5:元資料 (Metadata)

Next.js 預設已包含 meta charsetmeta viewport 標籤,因此可以安全地從 <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>
  )
}

任何 元資料檔案(如 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>
  )
}

最後,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>
  )
}

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

步驟 6:樣式

與 Create React App 一樣,Next.js 內建支援 CSS Modules

如果使用全域 CSS 檔案,請將其導入 app/layout.tsx 檔案:

import '../index.css'

// ...

如果使用 Tailwind,則需要安裝 postcssautoprefixer

Terminal
npm install postcss autoprefixer

然後,在專案根目錄建立 postcss.config.js 檔案:

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

步驟 7:建立入口頁面

在 Next.js 中,您可以透過建立 page.tsx 檔案來宣告應用程式的入口點。在 CRA 中最接近此檔案的是 src/index.tsx 檔案。在此步驟中,您將設定應用程式的入口點。

app 目錄中建立 [[...slug]] 目錄。

由於本指南旨在首先將 Next.js 設定為 SPA(單頁應用程式),因此需要讓頁面入口點捕獲應用程式的所有可能路由。為此,請在 app 目錄中建立新的 [[...slug]] 目錄。

此目錄稱為 可選的 catch-all 路由段。Next.js 使用基於檔案系統的路由器,其中 目錄用於定義路由。此特殊目錄將確保應用程式的所有路由都會指向其包含的 page.tsx 檔案。

app/[[...slug]] 目錄中建立新的 page.tsx 檔案,內容如下:

import '../../index.css'

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

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

此檔案是一個 伺服器元件。當執行 next build 時,此檔案會被預渲染為靜態資源。它不需要任何動態程式碼。

此檔案導入全域 CSS 並告訴 generateStaticParams 我們只會生成一個路由,即位於 / 的索引路由。

現在,讓我們遷移 CRA 應用程式的其餘部分,這些部分將僅在客戶端執行。

'use client'

import React from 'react'
import dynamic from 'next/dynamic'

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

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

此檔案是一個 客戶端元件,由 'use client' 指令定義。客戶端元件仍會在發送至客戶端前 預渲染為 HTML

由於我們希望一開始建立純客戶端應用程式,可以設定 Next.js 從 App 元件開始禁用預渲染。

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

現在,更新入口頁面以使用新元件:

import '../../index.css'
import { ClientOnly } from './client'

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

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

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

Next.js 處理靜態圖片導入的方式與 CRA 略有不同。在 CRA 中,導入圖片檔案會直接返回其公開 URL 字串:

App.tsx
import image from './img.png'

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

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

使用 <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 屬性時遇到型別錯誤。目前可以安全忽略這些錯誤,本指南後續會解決這些問題。

步驟 9:遷移環境變數

Next.js 支援與 CRA 類似的 .env 環境變數

主要差異在於暴露給客戶端環境變數的前綴。將所有使用 REACT_APP_ 前綴的環境變數改為 NEXT_PUBLIC_

步驟 10:更新 package.json 中的指令

現在你應該可以執行應用程式來測試是否成功遷移到 Next.js。但在這之前,你需要更新 package.json 中的 scripts,加入 Next.js 相關指令,並將 .nextnext-env.d.tsdist 加入 .gitignore 檔案:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

現在執行 npm run dev,並開啟 http://localhost:3000。你應該會看到應用程式已在 Next.js 上運行。

步驟 11:清理專案

現在你可以從程式碼庫中刪除與 Create React App 相關的檔案:

  • 刪除 src/index.tsx
  • 刪除 public/index.html
  • 刪除 reportWebVitals 設定
  • 解除安裝 CRA 的相依套件(react-scripts

打包工具相容性

Create React App 和 Next.js 預設都使用 webpack 進行打包。

將 CRA 應用程式遷移到 Next.js 時,你可能需要遷移自訂的 webpack 設定。Next.js 支援提供自訂 webpack 設定

此外,Next.js 透過 next dev --turbo 支援 Turbopack,可提升本地開發效能。Turbopack 也支援部分 webpack loader,以確保相容性和逐步採用。

後續步驟

如果一切順利,你現在已經有一個運作正常的 Next.js 應用程式,以單頁應用程式模式運行。不過,你尚未充分利用 Next.js 的大部分優勢,但現在可以開始逐步進行變更以獲得所有好處。以下是接下來可能的步驟:

小提示: 使用靜態匯出目前不支援 useParams 鉤子。