How to migrate from Vite to Next.js
本指南将帮助你将现有的 Vite 应用迁移到 Next.js。
为什么要切换?
你可能出于以下几个原因想要从 Vite 切换到 Next.js:
初始页面加载时间慢
如果你使用默认的 Vite React 插件构建应用,那么你的应用是一个纯客户端应用。纯客户端应用,也称为单页应用(SPA),通常会遇到初始页面加载时间慢的问题。这是由以下几个原因造成的:
- 浏览器需要等待 React 代码和整个应用包下载并运行完成后,你的代码才能发送请求来加载一些数据。
- 你的应用代码会随着每个新功能和额外依赖的增加而增长。
没有自动代码拆分
前面提到的加载时间慢的问题可以通过代码拆分在一定程度上得到改善。然而,如果你尝试手动进行代码拆分,往往会使性能变得更糟。手动进行代码拆分时很容易无意中引入网络瀑布流。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 配置选项。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // 输出单页应用(SPA)。
distDir: './dist', // 将构建输出目录更改为 `./dist/`。
}
export default nextConfig值得注意的是:你可以为 Next.js 配置文件使用
.js或.mjs扩展名。
步骤 3:更新 TypeScript 配置
如果你使用 TypeScript,需要对 tsconfig.json 文件进行以下更改,以使其与 Next.js 兼容。如果你不使用 TypeScript,可以跳过此步骤。
- 删除对
tsconfig.node.json的项目引用 - 将
./dist/types/**/*.ts和./next-env.d.ts添加到include数组 - 将
./node_modules添加到exclude数组 - 将
{ "name": "next" }添加到compilerOptions中的plugins数组:"plugins": [{ "name": "next" }] - 将
esModuleInterop设置为true:"esModuleInterop": true - 将
jsx设置为react-jsx:"jsx": "react-jsx" - 将
allowJs设置为true:"allowJs": true - 将
forceConsistentCasingInFileNames设置为true:"forceConsistentCasingInFileNames": true - 将
incremental设置为true:"incremental": true
以下是应用这些更改后的 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 文件,其中包含你的 <html>、<head> 和 <body> 标签。
在此步骤中,你将把 index.html 文件转换为根布局文件:
- 在
src文件夹中创建一个新的app目录。 - 在该
app目录中创建一个新的layout.tsx文件:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return '...'
}值得注意的是:布局文件可以使用
.js、.jsx或.tsx扩展名。
- 将
index.html文件的内容复制到之前创建的<RootLayout>组件中,同时用<div id="root">{children}</div>替换body.div#root和body.script标签:
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>
)
}- Next.js 默认已经包含了 meta charset 和 meta viewport 标签,因此你可以安全地从
<head>中删除这些:
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>
)
}- 任何元数据文件,如
favicon.ico、icon.png、robots.txt,只要放置在app目录的顶层,就会自动添加到应用的<head>标签中。将所有支持的文件移动到app目录后,你可以安全地删除它们的<link>标签:
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>
)
}- 最后,Next.js 可以使用 Metadata API 管理你最后的
<head>标签。将你的最终元数据信息移动到导出的metadata对象中:
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 和网络可分享性。
步骤 5:创建入口页面
在 Next.js 中,你通过创建一个 page.tsx 文件来声明应用的入口点。此文件在 Vite 中最接近的等价物是你的 main.tsx 文件。在此步骤中,你将设置应用的入口点。
- 在
app目录中创建一个[[...slug]]目录。
由于在本指南中我们首先将 Next.js 设置为 SPA(单页应用),因此你需要让页面入口点捕获应用的所有可能路由。为此,在 app 目录中创建一个新的 [[...slug]] 目录。
这个目录被称为可选的全捕获路由段。Next.js 使用基于文件系统的路由器,其中文件夹用于定义路由。这个特殊目录将确保应用的所有路由都将被定向到其包含的 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 应用的其余部分。
'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 })现在,更新入口页面以使用新组件:
import '../../index.css'
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}步骤 6:更新静态图片导入
Next.js 处理静态图片导入的方式与 Vite 略有不同。使用 Vite 时,导入图片文件将返回其公共 URL 作为字符串:
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> 的 width 和 height 属性。这可以防止图片加载时的布局偏移。但是,如果你的应用包含只有一个维度被样式化而另一个维度未被样式化为 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属性时可能会遇到类型错误。你现在可以安全地忽略这些错误。它们将在本指南结束时得到修复。
步骤 7:迁移环境变量
Next.js 支持 .env 环境变量,与 Vite 类似。主要区别在于用于在客户端公开环境变量的前缀。
- 将所有带有
VITE_前缀的环境变量更改为NEXT_PUBLIC_。
Vite 在特殊的 import.meta.env 对象上公开了一些内置环境变量,这些变量不受 Next.js 支持。你需要按如下方式更新它们的用法:
import.meta.env.MODE⇒process.env.NODE_ENVimport.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 环境变量。但是,如果需要,你仍然可以配置一个:
- 将以下内容添加到你的
.env文件:
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"- 在
next.config.mjs文件中将basePath设置为process.env.NEXT_PUBLIC_BASE_PATH:
/** @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- 将
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:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}# ...
.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 的大部分优势,但你现在可以开始进行增量更改以获得所有好处。以下是你接下来可能想做的事情:
- 从 React Router 迁移到 Next.js App Router 以获得:
- 自动代码拆分
- 流式服务器渲染
- React 服务器组件
- 使用
<Image>组件优化图片 - 使用
next/font优化字体 - 使用
<Script>组件优化第三方脚本 - 更新你的 ESLint 配置以支持 Next.js 规则