跳至内容

从 Vite 迁移

本指南将帮助你将现有的 Vite 应用迁移到 Next.js。

为什么要切换?

有很多理由可以解释你可能想要从 Vite 切换到 Next.js。

初始页面加载时间缓慢

如果你使用 React 的默认 Vite 插件构建了你的应用,那么你的应用就是一个纯客户端应用。仅客户端应用,也称为单页面应用 (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 作为依赖项安装。

终端
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. { "name": "next" } 添加到compilerOptions 中的 plugins 数组"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 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> 组件中,同时用 <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>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js 默认情况下已包含meta 字符集meta 视口标记,因此您可以安全地从 <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.icoicon.pngrobots.txt)放置到 app 目录的顶层,它们就会自动添加到应用程序的 <head> 标记中。将所有受支持的文件移动到 app 目录后,您可以安全地删除它们的 <link> 标记。

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 可以使用 元数据 API 管理您的最后一个 <head> 标签。将最终的元数据信息移动到导出的 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 内置于框架中的基于约定的方法(元数据 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 '...' // 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>widthheight 属性。这可以防止图像加载时出现布局偏移。但是,如果您的应用程序包含仅对其尺寸之一进行样式设置而不对另一个尺寸设置 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://127.0.0.1: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 的大多数优势,但您现在可以开始进行增量更改以获得所有优势。以下是一些您接下来可能想要做的事情