跳到主要内容

如何从 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 作为依赖项:

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: './dist', // 将构建输出目录更改为 `./dist/`。
}

export default nextConfig

提示: 您可以使用 .js.mjs 作为 Next.js 配置文件。

步骤 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. compilerOptions 中的 plugins 数组 中添加 { "name": "next" }"plugins": [{ "name": "next" }]
  5. esModuleInterop 设置为 true"esModuleInterop": true
  6. jsx 设置为 preserve"jsx": "preserve"
  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": "preserve",
"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 路由器 应用程序必须包含一个根布局文件,这是一个React 服务器组件,将包装应用程序中的所有页面。此文件在 app 目录的顶层定义。

Vite 应用程序中最接近根布局文件的等效文件是 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 '...'
}

提示: 布局文件可以使用 .js.jsx.tsx 扩展名。

  1. index.html 文件的内容复制到之前创建的 <RootLayout> 组件中,同时用 <div id="root">{children}</div> 替换 body.div#rootbody.script 标签:
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>我的应用</title>
<meta name="description" content="我的应用是一个..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
  1. 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" type="image/svg+xml" href="/icon.svg" />
<title>我的应用</title>
<meta name="description" content="我的应用是一个..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
  1. 任何元数据文件,如 favicon.icoicon.pngrobots.txt,只要您将它们放在 app 目录的顶层,就会自动添加到应用程序的 <head> 标签中。将所有支持的文件移动到 app 目录后,您可以安全地删除它们的 <link> 标签:
app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<title>我的应用</title>
<meta name="description" content="我的应用是一个..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
  1. 最后,Next.js 可以使用元数据 API 管理您的最后 <head> 标签。将您的最终元数据信息移动到导出的metadata 对象中:
app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: '我的应用',
description: '我的应用是一个...',
}

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:创建入口点页面

在 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 '...' // 我们将更新这个
}

提示: 页面文件可以使用 .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` 在生产环境中将是 '/assets/img.2d8efhg.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 服务器。

  1. 将从 /public 导入的图片的绝对导入路径转换为相对导入:
// 之前
import logo from '/logo.png'

// 之后
import logo from '../public/logo.png'
  1. 将图片的 src 属性而不是整个图片对象传递给您的 <img> 标签:
// 之前
<img src={logo} />

// 之后
<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.MODEprocess.env.NODE_ENV
  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'
  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'
  • import.meta.env.SSRtypeof 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', // 输出单页应用程序(SPA)。
distDir: './dist', // 将构建输出目录更改为 `./dist/`。
basePath: process.env.NEXT_PUBLIC_BASE_PATH, // 将基础路径设置为 `/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,并将 .nextnext-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,并打开 http://localhost: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 的大部分好处,但现在您可以开始进行增量更改以获得所有好处。以下是您接下来可能想要做的事情: