Миграция с Vite

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

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

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

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

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

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

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

Проблему медленной загрузки можно частично решить с помощью разделения кода (code splitting). Однако при ручном разделении кода можно случайно ухудшить производительность. Легко непреднамеренно создать "водопады" запросов при ручном разделении кода. 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

Полезно знать: Для файла конфигурации 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" } в массив 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 null
}
export default function RootLayout({ children }) {
  return null
}

Полезно знать: Для файлов макета можно использовать расширения .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' в production

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

В Next.js статический импорт изображения возвращает объект. Этот объект можно использовать напрямую с компонентом Next.js <Image> или использовать свойство 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', // Вывод в виде Single-Page Application (SPA).
  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, но теперь можете начать вносить постепенные изменения, чтобы получить все выгоды. Вот что вы можете сделать дальше: