本指南将帮助您将现有的 Vite 应用迁移到 Next.js。
您可能希望从 Vite 切换到 Next.js 的原因有以下几点:
如果您使用 React 默认的 Vite 插件 构建了您的应用程序,那么您的应用程序将是一个纯客户端应用程序。纯客户端应用程序,也称为单页应用程序 (SPAs),通常会遇到初始页面加载时间慢的问题。这通常是由于以下几个原因:
前面提到的加载时间慢的问题可以通过代码分割在一定程度上得到解决。然而,如果您尝试手动进行代码分割,通常会导致性能变差。手动代码分割很容易无意中引入网络瀑布。Next.js 的路由内置了自动代码分割功能。
应用程序进行顺序的客户端-服务器请求来获取数据是导致性能不佳的常见原因。SPA 中数据获取的一种常见模式是,首先渲染一个占位符,然后在组件挂载后才获取数据。不幸的是,这意味着一个获取数据的子组件,在其父组件完成自身数据加载之前,无法开始获取数据。
虽然 Next.js 支持在客户端获取数据,但它也提供了将数据获取转移到服务器的选项,这可以消除客户端-服务器瀑布效应。
借助对 通过 React Suspense 进行流式传输 的内置支持,您可以更有意地决定您的 UI 的哪些部分先加载以及加载顺序,而不会引入网络瀑布。
这使您能够构建加载更快的页面,并消除 布局偏移。
根据您的需求,Next.js 允许您在页面和组件级别选择数据获取策略。您可以决定在构建时、在服务器请求时或在客户端获取数据。例如,您可以从 CMS 获取数据并在构建时渲染您的博客文章,然后这些文章可以高效地缓存在 CDN 上。
Next.js 代理 允许您在请求完成之前在服务器上运行代码。当用户访问仅限认证页面时,这特别有用,可以通过将用户重定向到登录页面来避免出现未经认证的内容闪现。该代理还有助于实验和 国际化。
图片、字体 和 第三方脚本 通常对应用程序的性能有显著影响。Next.js 提供了内置组件,可自动为您优化这些资源。
本次迁移的目标是尽快获得一个可工作的 Next.js 应用程序,以便您随后可以逐步采用 Next.js 的功能。首先,我们将保留它作为一个纯客户端应用程序 (SPA),而不迁移您现有的路由。这有助于最大限度地减少迁移过程中遇到问题的可能性并减少合并冲突。
您需要做的第一件事是安装 next 作为依赖项:
npm install next@latest在您的项目根目录创建 next.config.mjs 文件。此文件将包含您的 Next.js 配置选项。
/** @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扩展名。
如果您正在使用 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": truejsx 设置为 react-jsx:"jsx": "react-jsx"allowJs 设置为 true:"allowJs": trueforceConsistentCasingInFileNames 设置为 true:"forceConsistentCasingInFileNames": trueincremental 设置为 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 的信息。
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 '...'
}export default function RootLayout({ children }) {
return '...'
}须知:布局文件可以使用
.js、.jsx或.tsx扩展名。
index.html 文件的内容复制到之前创建的 <RootLayout> 组件中,同时将 body.div#root 和 body.script 标签替换为 <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" 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>
)
}export default function RootLayout({ children }) {
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>
)
}<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>
)
}export default function RootLayout({ children }) {
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>
)
}export default function RootLayout({ children }) {
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>
)
}<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>
)
}export const metadata = {
title: 'My App',
description: 'My App is a...',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<div id="root">{children}</div>
</body>
</html>
)
}通过上述更改,您将从 index.html 中声明所有内容的方式转变为使用 Next.js 框架内置的基于约定的方法(Metadata API)。这种方法使您能够更轻松地改善页面的 SEO 和网络可分享性。
在 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 '...' // We'll update this
}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 应用程序的其余部分,这些部分将只在客户端运行。
'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'
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 />
}import '../../index.css'
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}Next.js 处理静态图片导入的方式与 Vite 略有不同。使用 Vite,导入图片文件将返回其公共 URL 作为字符串:
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 服务器。
/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.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', // 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 nextConfigimport.meta.env.BASE_URL 的使用更新为 process.env.NEXT_PUBLIC_BASE_PATHpackage.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 上运行。
示例: 请参阅 此拉取请求,了解一个已迁移到 Next.js 的 Vite 应用程序的实际示例。
您现在可以从代码库中清理与 Vite 相关的遗留文件:
main.tsxindex.htmlvite-env.d.tstsconfig.node.jsonvite.config.ts如果一切按计划进行,您现在已经拥有一个作为单页应用程序运行的正常 Next.js 应用程序。然而,您尚未充分利用 Next.js 的大部分优势,但现在您可以开始进行增量更改以获得所有好处。以下是您接下来可能需要做的事情:
<Image> 组件优化图片next/font 优化字体<Script> 组件优化第三方脚本