Sponsor
ntab.devntab.dev 提升效率的新标签页组件
点击查看
Menu

Migrating from Create React App

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

为什么要切换?

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

初始页面加载时间慢

Create React App 使用纯客户端 React。纯客户端应用,也称为单页应用(SPAs),通常会经历较慢的初始页面加载时间。这是由几个原因造成的:

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

没有自动代码分割

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

网络瀑布

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

虽然 Next.js 支持客户端数据获取,但 Next.js 还允许你将数据获取移动到服务器端。这通常可以完全消除客户端-服务器瀑布。

快速且有意图的加载状态

通过内置对通过 React Suspense 流式传输的支持,你可以定义 UI 的哪些部分首先加载以及按什么顺序加载,而不会创建网络瀑布。

这使你能够构建加载更快的页面并消除布局偏移

选择数据获取策略

根据你的需求,Next.js 允许你在页面或组件级别选择数据获取策略。例如,你可以在构建时(SSG)从 CMS 获取数据并渲染博客文章以获得快速加载速度,或者在必要时在请求时(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:

Terminal
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 App Router 应用程序必须包含一个根布局文件,这是一个React 服务器组件,它将包装你所有的页面。

CRA 应用程序中根布局文件最接近的等效文件是 public/index.html,其中包括你的 <html><head><body> 标签。

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

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

app/layout.tsx
TypeScript
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
TypeScript
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
TypeScript
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 可以使用元数据 API 管理你最后的 <head> 标签。将你最终的元数据信息移动到导出的 metadata 对象中:

app/layout.tsx
TypeScript
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 内置于框架中的基于约定的方法(元数据 API)。这种方法使你能够更轻松地改进页面的 SEO 和网络共享能力。

步骤 5:样式

与 CRA 一样,Next.js 默认支持 CSS 模块。它还支持全局 CSS 导入

如果你有一个全局 CSS 文件,将其导入到 app/layout.tsx 中:

app/layout.tsx
TypeScript
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
TypeScript
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):

app/[[...slug]]/client.tsx
TypeScript
'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
TypeScript
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 导入的图像的绝对导入路径转换为相对导入:

// 之前
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 的图像,这将是 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

打开 http://localhost:3000。你现在应该可以看到你的应用程序(在 SPA 模式下)在 Next.js 上运行了。

步骤 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-appserviceWorker.js),你可以了解如何使用 Next.js 创建渐进式 Web 应用程序(PWAs)

代理 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 }) => {
    // 在这里修改 webpack 配置
    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 钩子或其他服务器功能。要使用所有 Next.js 功能,请从 next.config.ts 中删除 output: 'export'