从 Vite 迁移到 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 中间件 可以在请求完成前在服务端执行代码。这对于避免未授权内容闪现特别有用(例如将用户重定向到登录页)。中间件还适用于 A/B 测试和 国际化。
内置优化
图片、字体 和 第三方脚本 对应用性能影响显著。Next.js 提供的内置组件可自动优化这些资源。
迁移步骤
我们的迁移目标是快速获得可运行的 Next.js 应用,以便后续逐步采用 Next.js 特性。初始阶段将保持为纯客户端应用(SPA),暂不迁移现有路由,以降低迁移风险和合并冲突。
第一步:安装 Next.js 依赖
首先安装最新版 Next.js:
npm install next@latest
第二步:创建 Next.js 配置文件
在项目根目录创建 next.config.mjs
,用于配置 Next.js 选项:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // 输出单页应用 (SPA)
distDir: './dist', // 将构建输出目录改为 `./dist/`
}
export default nextConfig
须知:配置文件可使用
.js
或.mjs
扩展名
第三步:更新 TypeScript 配置(如适用)
如果使用 TypeScript,需更新 tsconfig.json
以兼容 Next.js。非 TypeScript 项目可跳过此步:
- 移除对
tsconfig.node.json
的 项目引用 - 在
include
数组 添加./dist/types/**/*.ts
和./next-env.d.ts
- 在
exclude
数组 添加./node_modules
- 在
compilerOptions.plugins
添加{ "name": "next" }
- 启用
esModuleInterop
:true
- 设置
jsx
为preserve
- 启用
allowJs
:true
- 启用
forceConsistentCasingInFileNames
:true
- 启用
incremental
:true
完整配置示例:
{
"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 文档。
第四步:创建根布局文件
Next.js App 路由 应用必须包含 根布局文件,这是一个 React 服务端组件,用于包裹所有页面。该文件位于 app
目录顶层。
Vite 应用中与之对应的是 index.html
,包含 <html>
, <head>
, <body>
标签。现在将其转换为根布局文件:
- 在
src
目录下新建app
文件夹 - 创建
layout.tsx
文件:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return '...'
}
export default function RootLayout({ children }) {
return '...'
}
提示:布局文件可使用
.js
,.jsx
或.tsx
扩展名
- 将
index.html
内容复制到布局组件,并将body.div#root
和body.script
替换为<div id="root">{children}</div>
:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<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>
)
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<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>
)
}
- 将
favicon.ico
,icon.png
,robots.txt
等 元数据文件 移到app
目录后,可删除对应的<link>
标签:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
<title>我的应用</title>
<meta name="description" content="这是一个..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
- 最后使用 元数据 API 管理剩余
<head>
内容,将元数据移至metadata
对象:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '我的应用',
description: '这是一个...',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
通过以上更改,您从手动管理 index.html
转变为使用 Next.js 基于约定的 元数据 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 '...' // 稍后更新此处
}
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // 稍后更新此处
}
须知:页面文件可以使用
.js
、.jsx
或.tsx
扩展名。
此文件是一个 服务端组件 (Server Component)。运行 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 />
}
此文件是一个 客户端组件 (Client Component),由 'use client'
指令定义。客户端组件在发送到客户端之前仍会在服务端 预渲染为 HTML。
由于我们最初需要一个纯客户端应用,可以通过以下配置禁用从 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 />
}
步骤 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>
组件具有 自动图片优化 的额外优势。该组件会根据图片尺寸自动设置结果 <img>
的 width
和 height
属性,防止图片加载时的布局偏移。但如果应用中存在仅设置了一个维度而未将另一个维度设为 auto
的图片,可能会导致问题。未设置为 auto
的维度会默认使用 <img>
维度属性的值,可能导致图片显示变形。
保留 <img>
标签可以减少应用的改动量并避免上述问题。之后您可以逐步迁移到 <Image>
组件,通过 配置加载器 (loader) 或切换到默认的 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 支持与 Vite 类似的 .env
环境变量。主要区别在于客户端暴露环境变量时使用的前缀。
- 将所有
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
环境变量。但如有需要,您可以自行配置:
- 在
.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。但在那之前,需要更新 package.json
中的 scripts
为 Next.js 相关命令,并将 .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 的大部分优势,您可以逐步进行以下改进: