从 Create React App 迁移到 Next.js 指南
本指南将帮助您将现有的 Create React App (CRA) 项目迁移到 Next.js。
为什么选择迁移?
从 Create React App 切换到 Next.js 有以下几个重要原因:
初始页面加载缓慢
Create React App 使用纯客户端渲染的 React。这种纯客户端应用(也称为单页应用 (SPA))通常会遇到初始加载缓慢的问题,主要原因包括:
- 浏览器需要等待 React 代码和整个应用包下载执行完毕后,才能发起数据请求
- 随着功能增加和依赖项增多,应用代码体积会不断膨胀
缺乏自动代码分割
虽然可以通过手动代码分割缓解加载缓慢问题,但不当操作可能导致网络请求瀑布流。Next.js 的路由器和构建管道内置了自动代码分割和摇树优化功能。
网络请求瀑布流
当应用需要连续发起客户端-服务器数据请求时,常会出现性能问题。在单页应用中,常见模式是先渲染占位内容,等组件挂载后再获取数据。这会导致子组件必须等待父组件完成数据加载,形成请求"瀑布"。
虽然 Next.js 支持客户端数据获取,但它也允许将数据获取移到服务端,从而消除客户端-服务端的请求瀑布问题。
快速可控的加载状态
通过内置的React Suspense 流式渲染支持,您可以定义 UI 的加载优先级和顺序,避免产生网络瀑布流。
这使得您可以构建加载更快的页面,并消除布局偏移。
灵活的数据获取策略
Next.js 允许在页面或组件级别选择数据获取策略。例如,您可以从 CMS 获取数据并在构建时预渲染博客文章(SSG)以获得快速加载,或在需要时采用请求时渲染(SSR)。
中间件
Next.js 中间件允许在请求完成前在服务端运行代码。例如,您可以通过中间件将未认证用户重定向到登录页,避免出现未授权内容闪现。还可用于 A/B 测试、实验功能和国际化。
内置优化
图片、字体和第三方脚本通常对应用性能影响很大。Next.js 提供专用组件和 API 来自动优化这些资源。
迁移步骤
我们的目标是快速获得可运行的 Next.js 应用,以便后续逐步采用更多功能。首先,我们将您的应用视为纯客户端应用(单页应用),暂不替换现有路由,以减少复杂性和合并冲突。
注意:如果您使用了高级 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 选项。
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 应用路由必须包含根布局文件,这是一个服务端组件,会包裹所有页面。
在 CRA 应用中,最接近根布局的是 public/index.html
,包含 <html>
、<head>
和 <body>
标签。
- 在
src
文件夹内创建app
目录(或直接在项目根目录创建) - 在
app
目录中创建layout.tsx
(或layout.js
)文件:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return '...'
}
export default function RootLayout({ children }) {
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>
)
}
export default function RootLayout({ children }) {
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>
中移除:
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>
)
}
export default function RootLayout({ children }) {
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
目录顶层后,Next.js 会自动将它们添加到应用 <head>
标签中。移动所有支持的文件后,可以安全删除对应的 <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>
)
}
export default function RootLayout({ children }) {
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
对象:
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>
)
}
export const metadata = {
title: 'React App',
description: 'Web site created with Next.js.',
}
export default function RootLayout({ children }) {
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
:
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
。
由于我们暂时希望保持应用为单页应用并拦截所有路由,我们将使用可选全捕获路由。
- 在
app
内创建[[...slug]]
目录
app
┣ [[...slug]]
┃ ┗ page.tsx
┣ layout.tsx
- 在
page.tsx
中添加以下内容:
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // 后续会更新
}
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'
import dynamic from 'next/dynamic'
const App = dynamic(() => import('../../App'), { ssr: false })
export function ClientOnly() {
return <App />
}
'use client'
指令使该文件成为客户端组件- 带
ssr: false
的dynamic
导入会禁用<App />
组件的服务端渲染,使其成为真正的纯客户端组件(SPA)
现在更新 page.tsx
(或 page.js
)以使用新组件:
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}
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>
的 width
和 height
属性,防止图片加载时的布局偏移。但请注意,如果应用中存在仅设置单边尺寸(另一侧未设为 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
路径提供服务,该路径即可作为 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
:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "npx serve@latest ./build"
}
}
# ...
.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 的 basePath
配置 实现:
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 的 渐进式网页应用 (PWA) 指南。
代理 API 请求
若通过 package.json
的 proxy
字段代理后端请求,可使用 Next.js 的 rewrites 配置:
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
中扩展:
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 应用。接下来可逐步启用更多特性:
- 从 React Router 迁移 到 Next.js 应用路由,获得:
- 自动代码分割
- 流式服务端渲染
- React 服务端组件
- 使用
<Image>
组件 优化图片 - 通过
next/font
优化字体 - 利用
<Script>
组件 优化第三方脚本 - 运行
npx next lint
启用 ESLint 并配置符合项目的规则
注意: 使用静态导出 (
output: 'export'
) 时 暂不支持useParams
钩子等服务器功能。如需完整功能,请移除next.config.ts
中的output: 'export'
。