Как мигрировать с Vite на Next.js

Это руководство поможет вам перенести существующее приложение с Vite на Next.js.

Почему стоит перейти?

Есть несколько причин, по которым вы можете захотеть перейти с Vite на Next.js:

Медленная загрузка начальной страницы

Если вы создали приложение с использованием стандартного плагина Vite для React, ваше приложение является чисто клиентским. Клиентские приложения, также известные как одностраничные приложения (SPA), часто страдают от медленной загрузки начальной страницы. Это происходит по нескольким причинам:

  1. Браузеру нужно дождаться загрузки и выполнения кода React и всего бандла приложения, прежде чем ваш код сможет отправить запросы для загрузки данных.
  2. Код вашего приложения растёт с каждым новым функционалом и дополнительной зависимостью.

Нет автоматического разделения кода

Проблему медленной загрузки можно частично решить с помощью разделения кода (code splitting). Однако при ручном разделении кода вы можете случайно ухудшить производительность. Легко непреднамеренно создать сетевые водопады (network waterfalls) при ручном разделении кода. Next.js предоставляет автоматическое разделение кода, встроенное в его роутер.

Сетевые водопады

Распространённая причина плохой производительности — когда приложение делает последовательные клиент-серверные запросы для получения данных. Типичный паттерн получения данных в SPA — сначала отрендерить заглушку, а затем загрузить данные после монтирования компонента. К сожалению, это означает, что дочерний компонент, который загружает данные, не может начать загрузку, пока родительский компонент не завершит загрузку своих данных.

Хотя Next.js поддерживает получение данных на клиенте, он также предоставляет возможность перенести получение данных на сервер, что может устранить клиент-серверные водопады.

Быстрые и контролируемые состояния загрузки

Благодаря встроенной поддержке стриминга через React Suspense, вы можете более осознанно управлять тем, какие части интерфейса загружать первыми и в каком порядке, не создавая сетевых водопадов.

Это позволяет создавать страницы, которые загружаются быстрее и исключают сдвиги макета.

Выбор стратегии получения данных

В зависимости от ваших потребностей, Next.js позволяет выбирать стратегию получения данных для каждой страницы и компонента. Вы можете решить получать данные во время сборки, во время запроса на сервере или на клиенте. Например, вы можете загружать данные из вашей CMS и рендерить посты блога во время сборки, что затем можно эффективно кэшировать на CDN.

Middleware

Middleware в Next.js позволяет выполнять код на сервере до завершения запроса. Это особенно полезно, чтобы избежать мелькания неаутентифицированного контента, когда пользователь посещает страницу, доступную только для авторизованных пользователей, перенаправляя его на страницу входа. Middleware также полезен для экспериментов и интернационализации.

Встроенные оптимизации

Изображения, шрифты и сторонние скрипты часто существенно влияют на производительность приложения. Next.js предоставляет встроенные компоненты, которые автоматически оптимизируют их для вас.

Шаги миграции

Наша цель при миграции — как можно быстрее получить работающее приложение на Next.js, чтобы вы могли затем постепенно внедрять функции Next.js. Для начала мы оставим его как чисто клиентское приложение (SPA) без миграции существующего роутера. Это помогает минимизировать вероятность возникновения проблем в процессе миграции и уменьшает количество конфликтов при слиянии.

Шаг 1: Установка зависимости Next.js

Первое, что нужно сделать — установить next как зависимость:

Terminal
npm install next@latest

Шаг 2: Создание конфигурационного файла Next.js

Создайте файл next.config.mjs в корне вашего проекта. Этот файл будет содержать настройки конфигурации Next.js.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Генерирует одностраничное приложение (SPA).
  distDir: './dist', // Изменяет директорию сборки на `./dist/`.
}

export default nextConfig

Полезно знать: Вы можете использовать либо .js, либо .mjs для конфигурационного файла Next.js.

Шаг 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" } в массив plugins в compilerOptions: "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"]
}

Больше информации о настройке TypeScript можно найти в документации Next.js.

Шаг 4: Создание корневого макета

Приложение с App Router в Next.js должно включать файл корневого макета, который является React Server Component, оборачивающим все страницы вашего приложения. Этот файл определяется на верхнем уровне директории app.

Ближайшим эквивалентом файла корневого макета в приложении Vite является файл index.html, содержащий теги <html>, <head> и <body>.

На этом шаге вы преобразуете файл index.html в файл корневого макета:

  1. Создайте новую директорию app в вашей папке src.
  2. Создайте новый файл layout.tsx внутри этой директории app:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}
export default function RootLayout({ children }) {
  return '...'
}

Полезно знать: Для файлов макета можно использовать расширения .js, .jsx или .tsx.

  1. Скопируйте содержимое вашего файла 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>
  )
}
  1. 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>
  )
}
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>
  )
}
  1. Любые файлы метаданных, такие как favicon.ico, icon.png, robots.txt, автоматически добавляются в тег <head> приложения, если вы разместили их на верхнем уровне директории app. После перемещения всех поддерживаемых файлов в директорию 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>
  )
}
  1. Наконец, Next.js может управлять вашими последними тегами <head> с помощью Metadata API. Перенесите вашу последнюю информацию о метаданных в экспортируемый объект 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 и возможность совместного использования ваших страниц.

Шаг 5: Создание входной страницы

В Next.js вы объявляете точку входа для вашего приложения, создавая файл page.tsx. Ближайшим аналогом этого файла в Vite является ваш main.tsx. На этом шаге вы настроите точку входа вашего приложения.

  1. Создайте директорию [[...slug]] в вашей директории app.

Поскольку в этом руководстве мы сначала настраиваем Next.js как SPA (Single Page Application), вам нужно, чтобы точка входа вашей страницы перехватывала все возможные маршруты вашего приложения. Для этого создайте новую директорию [[...slug]] в директории app.

Эта директория называется опциональным перехватывающим сегментом маршрута. Next.js использует файловую систему для маршрутизации, где папки определяют маршруты. Эта специальная директория гарантирует, что все маршруты вашего приложения будут направлены в содержащийся в ней файл page.tsx.

  1. Создайте новый файл page.tsx внутри директории app/[[...slug]] со следующим содержимым:
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 на сервере перед отправкой клиенту.

Поскольку мы хотим начать с клиентского приложения, мы можем настроить 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 />
}

Шаг 6: Обновление статических импортов изображений

Next.js обрабатывает статические импорты изображений немного иначе, чем Vite. В Vite импорт файла изображения возвращает его публичный URL в виде строки:

App.tsx
import image from './img.png' // `image` будет '/assets/img.2d8efhg.png' в продакшене

export default function App() {
  return <img src={image} />
}

В Next.js статический импорт изображения возвращает объект. Этот объект можно использовать напрямую с компонентом <Image> из Next.js, или вы можете использовать свойство src объекта с вашим существующим тегом <img>.

Компонент <Image> имеет дополнительные преимущества, такие как автоматическая оптимизация изображений. Компонент <Image> автоматически устанавливает атрибуты width и height результирующего тега <img> на основе размеров изображения. Это предотвращает сдвиги макета при загрузке изображения. Однако это может вызвать проблемы, если ваше приложение содержит изображения, у которых стилизован только один из размеров без установки другого в auto. Если размер не установлен в auto, он будет по умолчанию равен значению атрибута <img>, что может привести к искажению изображения.

Использование тега <img> уменьшит количество изменений в вашем приложении и предотвратит указанные выше проблемы. Затем вы можете постепенно перейти на компонент <Image>, чтобы воспользоваться преимуществами оптимизации изображений, настроив загрузчик, или перейдя на стандартный сервер Next.js, который имеет встроенную оптимизацию изображений.

  1. Преобразуйте абсолютные пути импорта для изображений из /public в относительные импорты:
// До
import logo from '/logo.png'

// После
import logo from '../public/logo.png'
  1. Передайте свойство 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.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. Установите basePath в process.env.NEXT_PUBLIC_BASE_PATH в вашем файле next.config.mjs:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Вывод в виде SPA (Single-Page Application).
  distDir: './dist', // Изменяет директорию сборки на `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Устанавливает базовый путь в `/some-base-path`.
}

export default nextConfig
  1. Обновите использование import.meta.env.BASE_URL на process.env.NEXT_PUBLIC_BASE_PATH

Шаг 8: Обновление скриптов в package.json

Теперь вы должны быть готовы запустить ваше приложение, чтобы проверить успешность миграции на Next.js. Но перед этим вам нужно обновить scripts в вашем package.json командами, связанными с Next.js, и добавить .next и next-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 и откройте http://localhost:3000. Вы должны увидеть ваше приложение, работающее на Next.js.

Пример: Ознакомьтесь с этим pull request для рабочего примера миграции Vite-приложения на Next.js.

Шаг 9: Очистка

Теперь вы можете очистить вашу кодовую базу от артефактов, связанных с Vite:

  • Удалите main.tsx
  • Удалите index.html
  • Удалите vite-env.d.ts
  • Удалите tsconfig.node.json
  • Удалите vite.config.ts
  • Удалите зависимости Vite

Следующие шаги

Если всё прошло по плану, у вас теперь есть работающее Next.js-приложение, функционирующее как одностраничное приложение. Однако вы пока не используете большинство преимуществ Next.js, но теперь вы можете начать вносить постепенные изменения, чтобы получить все преимущества. Вот что вы можете сделать дальше: