跳转到内容

从 Create React App 迁移

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

为什么要切换?

有几个原因可能让你想要从 Create React App 切换到 Next.js

初始页面加载时间慢

Create React App 纯粹使用客户端 React。纯客户端应用程序,也称为单页应用程序 (SPA),通常会遇到初始页面加载时间慢的问题。发生这种情况有几个原因

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

没有自动代码拆分

先前加载时间慢的问题可以通过代码拆分在某种程度上得到缓解。但是,如果你尝试手动进行代码拆分,则可能会无意中引入网络瀑布流。Next.js 在其路由器和构建管道中内置了自动代码拆分和 tree-shaking。

网络瀑布流

当应用程序进行连续的客户端-服务器请求以获取数据时,就会发生性能不佳的常见原因。在 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,其中包括你的 <html><head><body> 标签。

  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 通过其 Metadata APITesting 设置提供支持。

步骤 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.icoicon.pngrobots.txt,只要你将它们放置在 app 目录的顶层,就会自动添加到应用程序 <head> 标签中。将所有支持的文件移动到 app 目录后,你可以安全地删除它们的 <link> 标签

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 管理你最后的 <head> 标签。将你最后的元数据信息移动到导出的 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 并拦截所有路由,因此我们将使用可选的 catch-all 路由

  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' 指令使此文件成为客户端组件
  • 带有 ssr: falsedynamic 导入禁用了 <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>widthheight 属性。这可以防止图像加载时发生布局偏移。但是,如果你的应用程序包含仅对其一个维度进行样式设置,而另一个维度未设置为 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 添加到你的 include 数组tsconfig.json 文件中。当你运行应用程序到步骤 9 时,Next.js 将自动生成此文件。

步骤 9:迁移环境变量

Next.js 支持 环境变量,方式与 CRA 类似,但要求任何你想在浏览器中暴露的变量都必须有 NEXT_PUBLIC_ 前缀。

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

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

更新你的 package.json 脚本以使用 Next.js 命令。此外,将 .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

打开 https://127.0.0.1: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-appserviceWorker.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 / Babel 配置

如果你在 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 脚本中删除 --turbopack 来禁用 Turbopack。

TypeScript 设置

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

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

打包器兼容性

Create React App 和 Next.js 默认都使用 webpack 进行打包。Next.js 还提供了 Turbopack,以便在本地开发中获得更快的速度

next dev --turbopack

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

下一步

如果一切正常,你现在应该有一个作为单页应用程序运行的 Next.js 应用程序。你尚未利用 Next.js 的服务器端渲染或基于文件的路由等功能,但你现在可以逐步这样做了

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