跳到内容

从 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 和网络可共享性。

步骤 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: 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>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 添加到你的 tsconfig.json 文件的 include 数组中。当你运行步骤 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.config.ts 中的 basePath 配置在 Next.js 中复制它

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 rewrites 来复现此操作。

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

注意:这将需要禁用 Turbopack,方法是从您的 dev 脚本中移除 --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'