Menu

从 Create React App 迁移

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

为什么要切换?

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

初始页面加载时间慢

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

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

没有自动代码分割

通过代码分割可以在一定程度上管理上述加载时间慢的问题。但是,如果你尝试手动进行代码分割,往往会使性能变得更糟。手动进行代码分割很容易无意中引入网络瀑布。Next.js 在其路由器中内置了自动代码分割。

网络瀑布

性能不佳的一个常见原因是应用程序为获取数据而进行连续的客户端-服务器请求。SPA 中常见的一种数据获取模式是最初渲染一个占位符,然后在组件挂载后获取数据。不幸的是,这意味着获取数据的子组件只能在父组件完成加载自己的数据后才能开始获取。

虽然 Next.js 支持在客户端获取数据,但它还提供了将数据获取转移到服务器的选项,这可以消除客户端-服务器瀑布。

快速且有意图的加载状态

通过对 React Suspense 的流式传输支持,你可以更有意图地决定首先加载 UI 的哪些部分以及以什么顺序加载,而不会引入网络瀑布。

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

选择数据获取策略

根据你的需求,Next.js 允许你在页面和组件的基础上选择数据获取策略。你可以决定在构建时、在服务器上的请求时或在客户端获取数据。例如,你可以从 CMS 获取数据并在构建时渲染博客文章,然后可以在 CDN 上高效缓存。

中间件

Next.js 中间件 允许你在请求完成之前在服务器上运行代码。这对于避免在用户访问仅认证页面时出现未认证内容闪现特别有用,可以将用户重定向到登录页面。中间件也适用于实验和 国际化

内置优化

图像字体第三方脚本 通常对应用程序的性能有重大影响。Next.js 提供了内置组件,可以自动为你优化这些内容。

迁移步骤

我们的迁移目标是尽快获得一个可工作的 Next.js 应用程序,以便你可以逐步采用 Next.js 功能。首先,我们将保持它作为一个纯客户端应用程序 (SPA),而不迁移你现有的路由器。这有助于最小化迁移过程中遇到问题的机会,并减少合并冲突。

步骤 1:安装 Next.js 依赖

你需要做的第一件事是安装 next 作为依赖项:

Terminal
npm install next@latest

步骤 2:创建 Next.js 配置文件

在项目根目录创建一个 next.config.mjs 文件。该文件将保存你的 Next.js 配置选项

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export", // 输出单页应用 (SPA)。
  distDir: "./build", // 将构建输出目录更改为 `./dist`。
};
 
export default nextConfig;

步骤 3:创建根布局

Next.js App Router 应用程序必须包含一个 根布局 文件,这是一个 React 服务器组件,它将包装你应用程序中的所有页面。该文件在 app 目录的顶层定义。

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

在这一步中,你将把 index.html 文件转换为根布局文件:

  1. src 目录中创建一个新的 app 目录。
  2. 在该 app 目录内创建一个新的 layout.tsx 文件:
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return "...";
}
app/layout.js
export default function RootLayout({ children }) {
  return "...";
}

值得注意的是:布局文件可以使用 .js.jsx.tsx 扩展名。

index.html 文件的内容复制到先前创建的 <RootLayout> 组件中,同时将 body.div#rootbody.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>
  );
}
app/layout.js
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 文件、额外的图标 (除了 faviconiconapple-icon) 以及 测试配置,但如果这些是必需的,Next.js 也支持这些选项。有关更多信息,请参阅 元数据 API测试 文档。

步骤 4:元数据

Next.js 默认已经包含 meta charsetmeta viewport 标签,因此你可以安全地从 <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>
  );
}
app/layout.js
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>
  );
}

只要你将 所有支持的文件 放置在 app 目录的顶层,任何 元数据文件,如 favicon.icoicon.pngrobots.txt 都会自动添加到应用程序的 <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>
  );
}
app/layout.js
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 对象 中:

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>
  );
}
app/layout.js
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:样式

与 Create React App 一样,Next.js 内置支持 CSS Modules

如果你正在使用全局 CSS 文件,将其导入到 app/layout.tsx 文件中:

app/layout.tsx
import "../index.css";
 
// ...

如果你正在使用 Tailwind,你需要安装 postcssautoprefixer

Terminal
npm install postcss autoprefixer

然后,在项目根目录创建一个 postcss.config.js 文件:

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

步骤 6:创建入口点页面

在 Next.js 中,你通过创建 page.tsx 文件来声明应用程序的入口点。这个文件在 CRA 中最接近的等价物是 src/index.tsx 文件。在这一步中,你将设置应用程序的入口点。

app 目录中创建一个 [[...slug]] 目录。

由于本指南的目标是首先将 Next.js 设置为 SPA (单页应用程序),你需要让页面入口点捕获应用程序的所有可能路由。为此,在 app 目录中创建一个新的 [[...slug]] 目录。

这个目录被称为 可选的全捕获路由段。Next.js 使用基于文件系统的路由器,其中 目录用于定义路由。这个特殊的目录将确保应用程序的所有路由都被导向到其包含的 page.tsx 文件。

app/[[...slug]] 目录中创建一个新的 page.tsx 文件,内容如下:

app/[[...slug]]/page.tsx
export function generateStaticParams() {
  return [{ slug: [""] }];
}
 
export default function Page() {
  return "..."; // 我们稍后会更新这里
}
app/[[...slug]]/page.js
export function generateStaticParams() {
  return [{ slug: [""] }];
}
 
export default function Page() {
  return "..."; // 我们稍后会更新这里
}

这个文件是一个 服务器组件。当你运行 next build 时,该文件会被预渲染成一个静态资源。它不需要任何动态代码。

这个文件导入我们的全局 CSS 并告诉 generateStaticParams 我们只会生成一个路由,即根路由 /

现在,让我们移动 CRA 应用程序的其余部分,它将只在客户端运行。

app/[[...slug]]/client.tsx
"use client";
 
import dynamic from "next/dynamic";
 
const App = dynamic(() => import("../../App"), { ssr: false });
 
export function ClientOnly() {
  return <App />;
}
app/[[...slug]]/client.js
"use client";
 
import dynamic from "next/dynamic";
 
const App = dynamic(() => import("../../App"), { ssr: false });
 
export function ClientOnly() {
  return <App />;
}

这个文件是一个 客户端组件,由 'use client' 指令定义。客户端组件在被发送到客户端之前仍然会在服务器上 预渲染为 HTML

由于我们想要一个仅客户端的应用程序开始,我们可以配置 Next.js 从 App 组件开始禁用预渲染。

const App = dynamic(() => import("../../App"), { ssr: false });

现在,更新你的入口点页面以使用新的组件:

app/[[...slug]]/page.tsx
import { ClientOnly } from "./client";
 
export function generateStaticParams() {
  return [{ slug: [""] }];
}
 
export default function Page() {
  return <ClientOnly />;
}
app/[[...slug]]/page.js
import { ClientOnly } from "./client";
 
export function generateStaticParams() {
  return [{ slug: [""] }];
}
 
export default function Page() {
  return <ClientOnly />;
}

步骤 7:更新静态图像导入

Next.js 处理静态图像导入的方式与 CRA 略有不同。在 CRA 中,导入图像文件会返回其公共 URL 作为字符串:

App.tsx
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 将自动生成这个文件。

步骤 8:迁移环境变量

Next.js 支持与 CRA 类似的 .env 环境变量

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

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

现在你应该能够运行你的应用程序来测试是否成功迁移到 Next.js。但在此之前,你需要用 Next.js 相关的命令更新 package.json 中的 scripts,并将 .nextnext-env.d.ts 添加到你的 .gitignore 文件中:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "npx serve@latest ./build"
  }
}
.gitignore
# ...
.next
next-env.d.ts

现在运行 npm run dev,并打开 http://localhost:3000。你应该能看到你的应用程序现在运行在 Next.js 上了。

步骤 10:清理

你现在可以清理你的代码库中与 Create React App 相关的内容:

  • 删除 public/index.html
  • 删除 src/index.tsx
  • 删除 src/react-app-env.d.ts
  • 删除 reportWebVitals 设置
  • 卸载 CRA 依赖项 (react-scripts)

打包工具兼容性

Create React App 和 Next.js 默认都使用 webpack 进行打包。

在将 CRA 应用程序迁移到 Next.js 时,你可能有自定义的 webpack 配置需要迁移。Next.js 支持提供 自定义 webpack 配置

此外,Next.js 通过 next dev --turbo 支持 Turbopack,以提高本地开发性能。Turbopack 还支持一些 webpack 加载器,以实现兼容性和增量采用。

下一步

如果一切按计划进行,你现在应该有一个作为单页应用程序运行的功能性 Next.js 应用程序。然而,你还没有利用 Next.js 的大部分优势,但你现在可以开始进行增量更改以获得所有好处。以下是你接下来可能想做的事情:

值得注意的是:使用静态导出 目前不支持 使用 useParams hook。