跳到内容

如何从 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 Proxy 允许您在请求完成之前在服务器上运行代码。这对于避免用户访问仅限认证的页面时出现未经认证内容闪烁的问题特别有用,通过将用户重定向到登录页面。代理对于实验和 国际化 也很有用。

内置优化

图片字体第三方脚本 通常对应用程序的性能有显著影响。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. compilerOptions 中的 plugins 数组 中添加 { "name": "next" }"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 文件,它包含您的 <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> 组件中,同时将 body.div#rootbody.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 标签,因此您可以安全地将它们从 <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>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.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', // 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,并将 .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,并打开 https://:3000。您应该会看到您的应用程序现在正在 Next.js 上运行。

示例: 查看 此拉取请求,了解一个已迁移到 Next.js 的 Vite 应用程序的运行示例。

步骤 9:清理

您现在可以从代码库中清理与 Vite 相关的文件

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

后续步骤

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