跳到内容

如何从 Vite 迁移到 Next.js

本指南将帮助您将现有的 Vite 应用程序迁移到 Next.js。

为什么要切换?

您可能希望从 Vite 切换到 Next.js 的原因有以下几点:

初始页面加载时间慢

如果您使用 Vite 默认的 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` 作为依赖。

终端
npm install next@latest

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

在您的项目根目录中创建一个 `next.config.mjs` 文件。此文件将包含您的 Next.js 配置选项

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Outputs a Single-Page Application (SPA).
  distDir: './dist', // Changes the build output directory to `./dist/`.
}
 
export default nextConfig

值得注意: 您的 Next.js 配置文件可以使用 `.js` 或 `.mjs` 扩展名。

步骤 3:更新 TypeScript 配置

如果您正在使用 TypeScript,您需要更新您的 `tsconfig.json` 文件,以使其与 Next.js 兼容。如果您不使用 TypeScript,则可以跳过此步骤。

  1. 删除对 `tsconfig.node.json` 的项目引用
  2. 将 `./dist/types/**/*.ts` 和 `./next-env.d.ts` 添加到`include` 数组
  3. 将 `./node_modules` 添加到`exclude` 数组
  4. 将 `{ "name": "next" }` 添加到`compilerOptions` 中的 `plugins` 数组:`"plugins": [{ "name": "next" }]`
  5. 设置`esModuleInterop`为 `true`:`"esModuleInterop": true`
  6. 设置`jsx`为 `react-jsx`:`"jsx": "react-jsx"`
  7. 设置`allowJs`为 `true`:`"allowJs": true`
  8. `forceConsistentCasingInFileNames`设置为 `true`:`"forceConsistentCasingInFileNames": true`
  9. `incremental`设置为 `true`:`"incremental": true`

以下是一个包含这些更改的工作 `tsconfig.json` 示例

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

您可以在Next.js 文档中找到有关配置 TypeScript 的更多信息。

步骤 4:创建根布局

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

在 Vite 应用程序中,根布局文件最接近的等效项是 `index.html` 文件,其中包含您的 ``、`` 和 `` 标签。

在此步骤中,您将把 `index.html` 文件转换为根布局文件

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

须知:布局文件可以使用 `.js`、`.jsx` 或 `.tsx` 扩展名。

  1. 将 `index.html` 文件的内容复制到之前创建的 `<RootLayout>` 组件中,同时将 `body.div#root` 和 `body.script` 标签替换为 `<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" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js 已经默认包含 meta charsetmeta viewport 标签,因此您可以安全地将它们从 `` 中删除。
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 任何元数据文件,例如 `favicon.ico`、`icon.png`、`robots.txt`,只要您将它们放置在 `app` 目录的顶层,就会自动添加到应用程序的 `` 标签中。将所有支持的文件移动到 `app` 目录后,您可以安全地删除它们的 `` 标签。
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 最后,Next.js 可以使用 Metadata API 管理您最终的 `` 标签。将您最终的元数据信息移动到一个导出的 `metadata` 对象中。
app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}
 
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 和 Web 可共享性。

步骤 5:创建入口页面

在 Next.js 中,您通过创建一个 `page.tsx` 文件来声明应用程序的入口点。此文件在 Vite 中最接近的等效文件是您的 `main.tsx` 文件。在此步骤中,您将设置应用程序的入口点。

  1. 在您的 `app` 目录中创建一个 `[[...slug]]` 目录。

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

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

  1. 在 `app/[[...slug]]` 目录中创建一个新的 `page.tsx` 文件,内容如下:
app/[[...slug]]/page.tsx
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // We'll update this
}

值得注意: 页面文件可以使用 `.js`、`.jsx` 或 `.tsx` 扩展名。

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

此文件导入我们的全局 CSS 并告诉 `generateStaticParams` 我们只生成一条路由,即根路由 `/`。

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

app/[[...slug]]/client.tsx
'use client'
 
import React from 'react'
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 '../../index.css'
import { ClientOnly } from './client'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return <ClientOnly />
}

步骤 6:更新静态图片导入

Next.js 处理静态图片导入的方式与 Vite 略有不同。使用 Vite,导入图片文件将返回其公共 URL 作为字符串。

App.tsx
import image from './img.png' // `image` will be '/assets/img.2d8efhg.png' in production
 
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 服务器。

  1. 将从 `public` 导入的图片的绝对导入路径转换为相对导入。
// Before
import logo from '/logo.png'
 
// After
import logo from '../public/logo.png'
  1. 将图片 `src` 属性(而不是整个图片对象)传递给您的 `<img>` 标签。
// Before
<img src={logo} />
 
// After
<img src={logo.src} />

或者,您可以根据文件名引用图像资源的公共 URL。例如,`public/logo.png` 将在您的应用程序中以 `/logo.png` 提供图像,这将是 `src` 值。

警告:如果您正在使用 TypeScript,您在访问 `src` 属性时可能会遇到类型错误。您现在可以安全地忽略这些错误。它们将在本指南结束时修复。

步骤 7:迁移环境变量

Next.js 支持 `.env` 环境变量,类似于 Vite。主要区别在于用于在客户端公开环境变量的前缀。

  • 将所有以 `VITE_` 开头的环境变量更改为 `NEXT_PUBLIC_`。

Vite 在特殊的 `import.meta.env` 对象上暴露了一些内置环境变量,这些变量 Next.js 不支持。您需要按如下方式更新它们的用法:

  • `import.meta.env.MODE` ⇒ `process.env.NODE_ENV`
  • `import.meta.env.PROD` ⇒ `process.env.NODE_ENV === 'production'`
  • `import.meta.env.DEV` ⇒ `process.env.NODE_ENV !== 'production'`
  • `import.meta.env.SSR` ⇒ `typeof window !== 'undefined'`

Next.js 也不提供内置的 `BASE_URL` 环境变量。但是,如果需要,您仍然可以配置一个。

  1. 将以下内容添加到您的 `.env` 文件中
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. 将您的 `next.config.mjs` 文件中的`basePath`设置为 `process.env.NEXT_PUBLIC_BASE_PATH`。
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Outputs a Single-Page Application (SPA).
  distDir: './dist', // Changes the build output directory to `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Sets the base path to `/some-base-path`.
}
 
export default nextConfig
  1. 将 `import.meta.env.BASE_URL` 的用法更新为 `process.env.NEXT_PUBLIC_BASE_PATH`

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

您现在应该能够运行您的应用程序,以测试是否已成功迁移到 Next.js。但在此之前,您需要使用 Next.js 相关命令更新 `package.json` 中的 `scripts`,并将 `.next` 和 `next-env.d.ts` 添加到您的 `.gitignore` 中。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

现在运行 `npm run dev`,并打开 `https://:3000`。您应该会看到您的应用程序现在正在 Next.js 上运行。

示例: 查看 此拉取请求,了解将 Vite 应用程序迁移到 Next.js 的工作示例。

步骤 9:清理

您现在可以从代码库中清除 Vite 相关文件了。

  • 删除 `main.tsx`
  • 删除 `index.html`
  • 删除 `vite-env.d.ts`
  • 删除 `tsconfig.node.json`
  • 删除 `vite.config.ts`
  • 卸载 Vite 依赖项

后续步骤

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