跳到内容
指南迁移Create React App

如何从 Create React App 迁移到 Next.js

本指南将帮助您将现有的 Create React App (CRA) 站点迁移到 Next.js。

为什么要切换?

您可能希望从 Create React App 切换到 Next.js 的原因有以下几点:

初始页面加载时间慢

Create React App 纯粹使用客户端渲染。仅客户端应用程序,也称为单页应用程序 (SPA),通常会遇到初始页面加载时间慢的问题。这通常是由于以下几个原因造成的:

  1. 浏览器需要等待 React 代码和整个应用程序包下载并运行,然后您的代码才能发送请求加载数据。
  2. 您的应用程序代码会随着您添加的每个新功能和依赖项而增长。

无自动代码分割

上述加载时间慢的问题可以通过代码分割在一定程度上缓解。但是,如果您尝试手动进行代码分割,可能会无意中引入网络瀑布。Next.js 的路由器和构建管道中内置了自动代码分割和摇树优化。

网络瀑布

性能不佳的常见原因是应用程序进行顺序的客户端-服务器请求来获取数据。在SPA 中获取数据的一种模式是渲染一个占位符,然后在组件挂载后获取数据。不幸的是,子组件只有在其父组件完成加载自己的数据后才能开始获取数据,从而导致请求的“瀑布”效应。

虽然 Next.js 支持客户端数据获取,但 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', // Outputs a Single-Page Application (SPA)
  distDir: 'build', // Changes the build output directory to `build`
}
 
export default nextConfig

注意:使用 `output: 'export'` 意味着您正在进行静态导出。您将**无法**访问服务器端功能,例如 SSR 或 API。您可以删除此行以利用 Next.js 服务器功能。

步骤 3:创建根布局

Next.js App Router 应用程序必须包含一个根布局文件,这是一个将包裹所有页面的 React 服务器组件

CRA 应用程序中根布局文件最接近的等效项是 `public/index.html`,它包含您的 ``、`` 和 `` 标签。

  1. 在 `src` 文件夹内(或者如果您更喜欢 `app` 在项目根目录,则在项目根目录)创建一个新的 `app` 目录。
  2. 在 `app` 目录中,创建一个 `layout.tsx`(或 `layout.js`)文件。
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}

现在将旧 `index.html` 的内容复制到此 `<RootLayout>` 组件中。将 `body div#root`(和 `body noscript`)替换为 `<div id="root">{children}</div>`。

app/layout.tsx
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 通过其元数据 API测试设置提供支持。

步骤 4:元数据

Next.js 会自动包含 `<meta charset="UTF-8" />` 和 `<meta name="viewport" content="width=device-width, initial-scale=1" />` 标签,因此您可以将它们从 `<head>` 中删除。

app/layout.tsx
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.ico`、`icon.png`、`robots.txt`,只要您将它们放置在 `app` 目录的顶层,就会自动添加到应用程序的 `` 标签中。将所有支持的文件移动到 `app` 目录后,您可以安全地删除它们的 `` 标签。

app/layout.tsx
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 管理您最终的 `` 标签。将您最终的元数据信息移动到一个导出的 `metadata` 对象中。

app/layout.tsx
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 和 Web 可共享性。

步骤 5:样式

与 CRA 类似,Next.js 开箱即支持CSS Modules。它还支持全局 CSS 导入

如果您有一个全局 CSS 文件,请将其导入到 `app/layout.tsx` 中。

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 并拦截**所有**路由,我们将使用可选的捕获所有路由

  1. 在 `app` 中创建一个 `[[...slug]]` 目录。
app
  [[...slug]]
   page.tsx
  layout.tsx
  1. 将以下内容添加到 `page.tsx`::
app/[[...slug]]/page.tsx
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // We'll update this
}

这告诉 Next.js 为空 slug ( `/` ) 生成单个路由,有效地将**所有**路由映射到同一页面。此页面是一个服务器组件,预渲染为静态 HTML。

步骤 7:添加仅客户端入口

接下来,我们将您的 CRA 的根 App 组件嵌入到客户端组件中,以便所有逻辑都保留在客户端。如果这是您第一次使用 Next.js,值得知道的是客户端组件(默认情况下)仍然在服务器上预渲染。您可以将它们视为具有运行客户端 JavaScript 的附加功能。

在 `app/[[...slug]]/` 中创建 `client.tsx`(或 `client.js`)

app/[[...slug]]/client.tsx
'use client'
 
import dynamic from 'next/dynamic'
 
const App = dynamic(() => import('../../App'), { ssr: false })
 
export function ClientOnly() {
  return <App />
}
  • 'use client' 指令使此文件成为**客户端组件**。
  • dynamic 导入与 ssr: false 禁用了 <App /> 组件的服务器端渲染,使其真正成为仅客户端 (SPA) 组件。

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

app/[[...slug]]/page.tsx
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>` 的 `width` 和 `height` 属性。这可以防止图片加载时出现布局偏移。但是,如果您的应用程序中包含只有其一个尺寸被样式化而另一个没有样式化为 `auto` 的图片,这可能会导致问题。当没有样式化为 `auto` 时,该尺寸将默认为 `<img>` 尺寸属性的值,这可能会导致图片失真。

保留 `<img>` 标签将减少应用程序中的更改量并防止上述问题。然后,您可以选择稍后迁移到 `<Image>` 组件,以利用通过配置加载器优化图片,或迁移到具有自动图片优化的默认 Next.js 服务器。

将从 `public` 导入的图片的绝对导入路径转换为相对导入。

// Before
import logo from '/logo.png'
 
// After
import logo from '../public/logo.png'

将图片 `src` 属性(而不是整个图片对象)传递给您的 `<img>` 标签。

// Before
<img src={logo} />
 
// After
<img src={logo.src} />

或者,您可以根据文件名引用图像资源的公共 URL。例如,`public/logo.png` 将在您的应用程序中以 `/logo.png` 提供图像,这将是 `src` 值。

警告: 如果您使用的是 TypeScript,访问 `src` 属性时可能会遇到类型错误。要解决这些错误,您需要将 `next-env.d.ts` 添加到 `tsconfig.json` 文件的include 数组中。当您在步骤 9 运行应用程序时,Next.js 将自动生成此文件。

步骤 9:迁移环境变量

Next.js 支持环境变量,与 CRA 类似,但对于您希望在浏览器中公开的任何变量都**需要** `NEXT_PUBLIC_` 前缀。

主要区别在于用于在客户端公开环境变量的前缀。将所有带有 `REACT_APP_` 前缀的环境变量更改为 `NEXT_PUBLIC_`。

步骤 10:更新 `package.json` 中的脚本

更新您的 `package.json` 脚本以使用 Next.js 命令。另外,将 `.next` 和 `next-env.d.ts` 添加到您的 `.gitignore` 中。

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

现在您可以运行

npm run dev

打开https://: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 中使用 `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 请求

如果您的 CRA 应用程序使用 `package.json` 中的 `proxy` 字段将请求转发到后端服务器,您可以使用 `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

如果您在 CRA 中有自定义的 webpack 或 Babel 配置,您可以在 `next.config.ts` 中扩展 Next.js 的配置

next.config.ts
import { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  webpack: (config, { isServer }) => {
    // Modify the webpack config here
    return config
  },
}
 
export default nextConfig

注意:这将需要通过在 `dev` 脚本中添加 `--webpack` 来使用 Webpack。

TypeScript 设置

如果您有 `tsconfig.json`,Next.js 会自动设置 TypeScript。请确保 `next-env.d.ts` 列在您的 `tsconfig.json` `include` 数组中。

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

打包器兼容性

Create React App 使用 webpack 进行打包。Next.js 现在默认使用Turbopack以实现更快的本地开发。

next dev  # Uses Turbopack by default

改为使用 Webpack(与 CRA 类似)

next dev --webpack

如果您需要从 CRA 迁移高级 webpack 设置,仍然可以提供自定义 webpack 配置

后续步骤

如果一切顺利,您现在拥有一个作为单页应用程序运行的 Next.js 应用程序。您尚未利用 Next.js 的功能,例如服务器端渲染或基于文件的路由,但现在您可以逐步进行:

注意:使用静态导出 (`output: 'export'`) 目前不支持 `useParams` 钩子或其他服务器功能。要使用所有 Next.js 功能,请从您的 `next.config.ts` 中删除 `output: 'export'`。