如何從 Vite 遷移至 Next.js

本指南將協助您將現有的 Vite 應用程式遷移至 Next.js。

為什麼要遷移?

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

初始頁面載入時間緩慢

如果您使用 Vite 預設的 React 插件 建置應用程式,您的應用程式將是一個純客戶端應用程式。純客戶端應用程式(也稱為單頁應用程式 SPA)通常會遇到初始頁面載入時間緩慢的問題,原因如下:

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

缺乏自動程式碼分割

初始載入時間緩慢的問題可以透過程式碼分割來部分解決。但手動進行程式碼分割往往會使效能更差,容易無意中引入網路瀑布效應。Next.js 的路由器內建了自動程式碼分割功能。

網路瀑布效應

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

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

快速且可控的載入狀態

透過內建的 React Suspense 串流支援,您可以更精確地控制 UI 各部分的載入順序,同時避免網路瀑布效應。

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

選擇資料獲取策略

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

中介軟體

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

須知: 您可以使用 .js.mjs 作為 Next.js 設定檔的副檔名。

步驟 3:更新 TypeScript 設定

如果您使用 TypeScript,需要更新 tsconfig.json 檔案以使其與 Next.js 相容。如果不使用 TypeScript,可以跳過此步驟。

  1. 移除對 tsconfig.node.json專案參考
  2. ./dist/types/**/*.ts./next-env.d.ts 加入 include 陣列
  3. ./node_modules 加入 exclude 陣列
  4. compilerOptionsplugins 陣列 中加入 { "name": "next" }"plugins": [{ "name": "next" }]
  5. esModuleInterop 設為 true"esModuleInterop": true
  6. jsx 設為 preserve"jsx": "preserve"
  7. allowJs 設為 true"allowJs": true
  8. forceConsistentCasingInFileNames 設為 true"forceConsistentCasingInFileNames": true
  9. incremental 設為 true"incremental": true

以下是包含這些變更的有效 tsconfig.json 範例:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

您可以在 Next.js 文件 中找到更多關於設定 TypeScript 的資訊。

步驟 4:建立根佈局

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

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

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

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

須知:佈局檔案可以使用 .js.jsx.tsx 副檔名。

  1. 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" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </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" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js 已預設包含 meta charsetmeta viewport 標籤,因此您可以安全地從 <head> 中移除這些:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 任何 中繼資料檔案favicon.icoicon.pngrobots.txt 只要放在 app 目錄的頂層,就會自動加入應用程式的 <head> 標籤。將 所有支援的檔案 移至 app 目錄後,您可以安全地刪除它們的 <link> 標籤:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 最後,Next.js 可以透過 Metadata API 管理您的最後幾個 <head> 標籤。將您的最終中繼資料資訊移至匯出的 metadata 物件
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'My App',
  description: 'My App is a...',
}

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:建立入口頁面

在 Next.js 中,你可以透過建立 page.tsx 檔案來宣告應用程式的入口點。這與 Vite 中的 main.tsx 檔案最為相似。在這個步驟中,你將設定應用程式的入口點。

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

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

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

  1. app/[[...slug]] 目錄中建立一個新的 page.tsx 檔案,內容如下:
import '../../index.css'

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

export default function Page() {
  return '...' // 我們稍後會更新這裡
}
import '../../index.css'

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

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

小知識:頁面檔案可以使用 .js.jsx.tsx 副檔名。

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

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

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

'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'

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

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

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

這個檔案是一個客戶端元件 (Client Component),由 '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 />
}
import '../../index.css'
import { ClientOnly } from './client'

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

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

步驟 6:更新靜態圖片匯入

Next.js 處理靜態圖片匯入的方式與 Vite 略有不同。在 Vite 中,匯入圖片檔案會返回其公共 URL 作為字串:

App.tsx
import image from './img.png' // `image` 在生產環境中會是 '/assets/img.2d8efhg.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 伺服器,該伺服器具有自動圖片優化功能。

  1. 將從 /public 匯入的絕對路徑圖片轉換為相對匯入:
// 之前
import logo from '/logo.png'

// 之後
import logo from '../public/logo.png'
  1. 將圖片的 src 屬性而非整個圖片物件傳遞給你的 <img> 標籤:
// 之前
<img src={logo} />

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

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

警告:如果你使用 TypeScript,在存取 src 屬性時可能會遇到類型錯誤。你可以暫時安全地忽略這些錯誤。它們將在本指南結束時修復。

步驟 7:遷移環境變數

Next.js 支援類似 Vite 的 .env 環境變數。主要區別在於用於在客戶端公開環境變數的前綴。

  • 將所有帶有 VITE_ 前綴的環境變數更改為 NEXT_PUBLIC_

Vite 在特殊的 import.meta.env 物件上公開了一些內建的環境變數,這些變數不受 Next.js 支援。你需要按以下方式更新它們的使用:

  • import.meta.env.MODEprocess.env.NODE_ENV
  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'
  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'
  • import.meta.env.SSRtypeof window !== 'undefined'

Next.js 也沒有提供內建的 BASE_URL 環境變數。然而,如果你需要,仍然可以配置一個:

  1. 將以下內容添加到你的 .env 檔案中:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. 在你的 next.config.mjs 檔案中將 basePath 設定為 process.env.NEXT_PUBLIC_BASE_PATH
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // 輸出一個單頁應用程式 (SPA)。
  distDir: './dist', // 將建置輸出目錄更改為 `./dist/`。
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // 將基礎路徑設定為 `/some-base-path`。
}

export default nextConfig
  1. import.meta.env.BASE_URL 的使用更新為 process.env.NEXT_PUBLIC_BASE_PATH

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

你現在應該能夠執行你的應用程式來測試是否成功遷移到 Next.js。但在那之前,你需要使用 Next.js 相關命令更新 package.json 中的 scripts,並將 .nextnext-env.d.ts 添加到你的 .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 上運行。

範例:查看這個拉取請求以獲取一個從 Vite 遷移到 Next.js 的工作範例。

步驟 9:清理

你現在可以從程式碼庫中清理與 Vite 相關的檔案:

  • 刪除 main.tsx
  • 刪除 index.html
  • 刪除 vite-env.d.ts
  • 刪除 tsconfig.node.json
  • 刪除 vite.config.ts
  • 解除安裝 Vite 依賴項

下一步

如果一切按計劃進行,你現在有一個運作正常的 Next.js 應用程式,作為單頁應用程式運行。然而,你還沒有利用 Next.js 的大部分優勢,但你現在可以開始進行增量變更以獲得所有好處。以下是你接下來可能想做的事情: