本指南将帮助您将现有 Create React App (CRA) 站点迁移到 Next.js。
您可能希望从 Create React App 切换到 Next.js 的原因有以下几点:
Create React App 纯粹使用客户端渲染。仅限客户端的应用程序,也称为单页应用程序 (SPA),通常会遇到初始页面加载时间慢的问题。这通常是由于以下几个原因:
上述加载时间慢的问题可以通过代码分割在一定程度上缓解。然而,如果您尝试手动进行代码分割,可能会无意中引入网络瀑布。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 中复制或调整这些功能的提示。
在您现有项目中安装 Next.js:
npm install next@latest在项目根目录(与 package.json 同级)创建 next.config.ts 文件。此文件包含您的 Next.js 配置选项。
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 服务器功能。
Next.js App Router 应用程序必须包含一个根布局文件,它是一个将包裹所有页面的 React Server Component。
CRA 应用程序中根布局文件最接近的等价物是 public/index.html,它包含您的 <html>、<head> 和 <body> 标签。
src 文件夹内(或者如果您更喜欢 app 在项目根目录,则在项目根目录)创建一个新的 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 通过其 Metadata API 和测试设置提供了支持。
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 目录的顶层,就会自动添加到应用程序的 <head> 标签中。将所有受支持的文件移动到 app 目录后,您可以安全地删除它们的 <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 可以通过 Metadata 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 内置于框架的基于约定的方法(Metadata API)。这种方法使您能够更轻松地改进页面的 SEO 和网络分享性。
与 CRA 类似,Next.js 开箱即用地支持 CSS Modules。它还支持全局 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,请参阅我们的安装文档。
Create React App 使用 src/index.tsx (或 index.js) 作为入口点。在 Next.js (App Router) 中,app 目录内的每个文件夹都对应一个路由,并且每个文件夹都应该有一个 page.tsx。
由于我们现在希望将应用程序保留为 SPA 并拦截所有路由,因此我们将使用可选的捕获所有路由。
app 内部创建一个 [[...slug]] 目录。app
┣ [[...slug]]
┃ ┗ page.tsx
┣ layout.tsxpage.tsx 中:export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // We'll update this
}export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // We'll update this
}这会告诉 Next.js 为空 slug (/) 生成一个单一路由,有效地将所有路由映射到同一个页面。此页面是一个 Server Component,预渲染为静态 HTML。
接下来,我们将您的 CRA 根 App 组件嵌入到一个 Client Component 中,以便所有逻辑都保留在客户端。如果这是您第一次使用 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' 指令使此文件成为一个 Client Component。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 />
}在 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数组中。Next.js 将在您在步骤 9 运行应用程序时自动生成此文件。
Next.js 对环境变量的支持类似于 CRA,但要求任何您希望在浏览器中暴露的变量都必须带有 NEXT_PUBLIC_ 前缀。
主要区别在于用于在客户端暴露环境变量的前缀。将所有带有 REACT_APP_ 前缀的环境变量更改为 NEXT_PUBLIC_。
package.json 中的脚本更新您的 package.json 脚本以使用 Next.js 命令。此外,将 .next 和 next-env.d.ts 添加到您的 .gitignore 中:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "npx serve@latest ./build"
}
}# ...
.next
next-env.d.ts现在您可以运行:
npm run dev打开 http://localhost:3000。您现在应该看到您的应用程序正在 Next.js 上运行(在 SPA 模式下)。
现在您可以移除 Create React App 特定的文件:
public/index.htmlsrc/index.tsxsrc/react-app-env.d.tsreportWebVitals 设置react-scripts 依赖(从 package.json 中卸载它)homepage如果您在 CRA package.json 中使用 homepage 字段在特定子路径下提供应用程序,您可以使用 next.config.ts 中的 basePath 配置在 Next.js 中复制此功能:
import { NextConfig } from 'next'
const nextConfig: NextConfig = {
basePath: '/my-subpath',
// ...
}
export default nextConfigService Worker如果您使用了 CRA 的 Service Worker(例如,create-react-app 中的 serviceWorker.js),您可以学习如何在 Next.js 中创建渐进式 Web 应用程序 (PWAs)。
如果您的 CRA 应用程序在 package.json 中使用了 proxy 字段来将请求转发到后端服务器,您可以通过 next.config.ts 中的 Next.js 重写来复制此功能:
import { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://your-backend.com/:path*',
},
]
},
}如果您在 CRA 中有自定义的 webpack 或 Babel 配置,您可以在 next.config.ts 中扩展 Next.js 的配置:
import { NextConfig } from 'next'
const nextConfig: NextConfig = {
webpack: (config, { isServer }) => {
// Modify the webpack config here
return config
},
}
export default nextConfig注意:这将需要通过在您的
dev脚本中添加--webpack来使用 Webpack。
如果您有一个 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 的服务器端渲染或基于文件的路由等功能,但现在您可以逐步进行:
<Image> 组件优化图片next/font优化字体<Script> 组件优化第三方脚本注意:使用静态导出 (
output: 'export') 目前不支持useParamshook 或其他服务器功能。要使用所有 Next.js 功能,请从您的next.config.ts中移除output: 'export'。