如何从 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 的路由器和构建管道内置了自动代码分割和摇树优化功能。

网络请求瀑布流

当应用需要连续发起客户端-服务器请求获取数据时,常会导致性能问题。在 SPA 中,常见的数据获取模式是先渲染占位内容,等组件挂载后再获取数据。这会导致子组件必须等待父组件完成数据加载后才能开始自己的请求,形成请求"瀑布"。

虽然 Next.js 支持客户端数据获取,但它也允许您将数据获取移至服务端,从而彻底消除客户端-服务端瀑布流问题。

快速可控的加载状态

通过内置的 React Suspense 流式渲染支持,您可以定义 UI 的加载优先级和顺序,避免网络瀑布流。

这能帮助您构建加载更快的页面,并消除布局偏移

灵活的数据获取策略

Next.js 允许您在页面或组件级别选择数据获取策略。例如,您可以从 CMS 获取数据并在构建时预渲染博客文章(SSG)以实现快速加载,或在必要时通过请求时获取数据(SSR)。

中间件功能

Next.js 中间件允许在请求完成前在服务端运行代码。例如,您可以在中间件中将未认证用户重定向至登录页面,避免出现未授权内容闪现。该功能还可用于 A/B 测试、实验和国际化

内置优化

图片字体第三方脚本通常对应用性能影响显著。Next.js 提供了专用组件和 API 来自动优化这些资源。

迁移步骤

我们的首要目标是快速获得可运行的 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 应用路由必须包含 根布局文件,这是一个 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 '...'
}

现在将旧 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>
  )
}

须知: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>
  )
}

任何元数据文件(如 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 和网络分享能力。

步骤 5:样式处理

与 CRA 类似,Next.js 开箱即用地支持 CSS 模块全局 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 目录内的每个文件夹对应一个路由,每个文件夹应包含 page.tsx

由于我们暂时希望保持应用为 SPA 并拦截所有路由,我们将使用可选的全捕获路由

  1. app 内创建 [[...slug]] 目录
app
 [[...slug]]
 page.tsx
 layout.tsx
  1. page.tsx 中添加以下内容
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' 指令使该文件成为客户端组件
  • 带有 ssr: falsedynamic 导入会禁用 <App /> 组件的服务端渲染,使其成为真正的纯客户端组件(SPA)

现在更新 page.tsx(或 page.js)以使用新组件:

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> 组件能获得自动图片优化的额外优势。该组件会根据图片尺寸自动设置 <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 访问。

警告: 使用 TypeScript 时访问 src 属性可能报类型错误。需将 next-env.d.ts 添加到 tsconfig.jsoninclude 数组中。执行步骤 9 时 Next.js 会自动生成该文件。

步骤 9:迁移环境变量

Next.js 的环境变量机制与 CRA 类似,但需为浏览器端暴露的变量添加 NEXT_PUBLIC_ 前缀。

将原有 REACT_APP_ 前缀的环境变量统一改为 NEXT_PUBLIC_ 前缀。

步骤 10:更新 package.json 脚本

更新 package.json 中的脚本命令,并将 .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,应用将以 SPA 模式运行在 Next.js 上。

步骤 11:清理工作

移除 CRA 特有的文件:

  • public/index.html
  • src/index.tsx
  • src/react-app-env.d.ts
  • reportWebVitals 相关配置
  • package.json 中的 react-scripts 依赖

其他注意事项

处理 CRA 的 homepage 配置

若曾在 CRA 的 package.json 中使用 homepage 字段指定子路径,可在 Next.js 的 next.config.ts 中通过 basePath 配置实现:

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

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

export default nextConfig

迁移自定义 Service Worker

如需迁移 CRA 的 Service Worker(如 create-react-app 生成的 serviceWorker.js),可参考 Next.js 的渐进式 Web 应用 (PWA)指南。

代理 API 请求

若通过 package.jsonproxy 字段代理后端请求,可在 next.config.ts 中使用 Next.js 重定向

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 配置

Next.js 在检测到 tsconfig.json 时会自动配置 TypeScript。确保 next-env.d.ts 包含在 include 数组中:

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

构建工具兼容性

CRA 和 Next.js 默认均使用 webpack。Next.js 还提供 Turbopack 以加速本地开发:

next dev --turbopack

如需迁移 CRA 的高级 webpack 配置,仍可提供自定义 webpack 配置

后续步骤

完成迁移后,可逐步启用 Next.js 高级功能:

注意: 使用静态导出 (output: 'export') 时暂不支持 useParams 等服务器功能。如需完整功能,需移除 next.config.ts 中的 output: 'export'