如何从 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 应用,后续再逐步采用更多特性。初始阶段我们将保持纯客户端应用(SPA)模式,不迁移现有路由,以降低迁移风险和合并冲突。

第一步:安装 Next.js 依赖

首先安装最新版 Next.js:

终端
npm install next@latest

第二步:创建 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 扩展名。

第三步:更新 TypeScript 配置(如适用)

TypeScript 用户需更新 tsconfig.json

  1. 移除对 tsconfig.node.json项目引用
  2. include 数组 添加 ./dist/types/**/*.ts./next-env.d.ts
  3. exclude 数组 添加 ./node_modules
  4. compilerOptions.plugins 添加 { "name": "next" }
  5. 启用 esModuleInteroptrue
  6. 设置 jsxpreserve
  7. 启用 allowJstrue
  8. 启用 forceConsistentCasingInFileNamestrue
  9. 启用 incrementaltrue

示例配置:

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 文档

第四步:创建根布局

Next.js 应用路由 必须包含 根布局 文件(一个 React 服务端组件),用于包裹所有页面。该文件位于 app 目录顶层。

Vite 中与之对应的是 index.html,包含 <html><head><body> 标签。现在将其转换为根布局:

  1. src 下新建 app 目录
  2. 创建 layout.tsx 文件:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  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="zh-CN">
      <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>我的应用</title>
        <meta name="description" content="我的应用是..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 移除 Next.js 已内置的 charsetviewport meta 标签:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>我的应用</title>
        <meta name="description" content="我的应用是..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. favicon.icoicon.pngrobots.txt元数据文件 移至 app 目录后,可删除对应的 <link> 标签:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <head>
        <title>我的应用</title>
        <meta name="description" content="我的应用是..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 最后使用 元数据 API 管理剩余 <head> 标签:
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '我的应用',
  description: '我的应用是...',
}

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

通过以上变更,您已从静态 index.html 迁移到 Next.js 基于约定的 元数据 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 '...' // 我们稍后会更新这里
}

小知识:页面文件可以使用 .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 />
}

这个文件是一个 客户端组件 (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 />
}

步骤 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 支持 .env 环境变量,与 Vite 类似。主要区别在于在客户端暴露环境变量时使用的前缀。

  • 将所有以 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。但在那之前,您需要更新 package.json 中的 scripts,添加与 Next.js 相关的命令,并将 .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 的大部分优势,但现在可以开始逐步进行更改以获得所有好处。以下是您可能想要进行的下一步操作: