Миграция с Vite

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

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

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

  1. Медленная загрузка начальной страницы: Если вы создали приложение с помощью стандартного плагина Vite для React, ваше приложение является чисто клиентским. Клиентские приложения, также известные как одностраничные приложения (SPA), часто страдают от медленной загрузки начальной страницы. Это происходит по нескольким причинам:
    1. Браузеру нужно дождаться загрузки и выполнения кода React и всего бандла приложения, прежде чем ваш код сможет отправить запросы для загрузки данных.
    2. Код вашего приложения растёт с каждым новым функционалом и дополнительными зависимостями.
  2. Отсутствие автоматического разделения кода: Проблему медленной загрузки можно частично решить с помощью разделения кода. Однако при ручном разделении кода вы можете случайно ухудшить производительность. Легко непреднамеренно создать водопад сетевых запросов при ручном разделении кода. Next.js предоставляет встроенное автоматическое разделение кода в своём роутере.
  3. Водопады сетевых запросов: Частая причина плохой производительности — последовательные клиент-серверные запросы для получения данных. В SPA распространён паттерн, когда сначала рендерится заглушка, а данные запрашиваются после монтирования компонента. К сожалению, это означает, что дочерний компонент, запрашивающий данные, не может начать запрос, пока родительский компонент не завершит загрузку своих данных. В Next.js эта проблема решена благодаря загрузке данных в серверных компонентах.
  4. Быстрые и контролируемые состояния загрузки: Благодаря встроенной поддержке стриминга с Suspense, в Next.js вы можете точнее контролировать, какие части интерфейса загружать первыми и в каком порядке, не создавая водопадов запросов. Это позволяет создавать страницы, которые загружаются быстрее, а также избегать сдвигов макета.
  5. Выбор стратегии загрузки данных: В зависимости от потребностей, Next.js позволяет выбирать стратегию загрузки данных для каждой страницы и компонента. Вы можете загружать данные во время сборки, при запросе на сервере или на клиенте. Например, вы можете загружать данные из CMS и рендерить статьи блога во время сборки, что затем позволит эффективно кэшировать их на CDN.
  6. Middleware: Next.js Middleware позволяет выполнять код на сервере до завершения запроса. Это особенно полезно, чтобы избежать мелькания неаутентифицированного контента, когда пользователь заходит на страницу, доступную только для авторизованных пользователей, перенаправляя его на страницу входа. Middleware также полезен для экспериментов и интернационализации.
  7. Встроенные оптимизации: Изображения, шрифты и сторонние скрипты часто значительно влияют на производительность приложения. 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, который будет оборачивать все страницы приложения. Этот файл находится на верхнем уровне директории 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 (одностраничное приложение), вам нужно, чтобы точка входа страницы перехватывала все возможные маршруты вашего приложения. Для этого создайте новую директорию [[...slug]] в директории app.

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

  1. Создайте новый файл page.tsx внутри директории app/[[...slug]] со следующим содержимым:
'use client'

import dynamic from 'next/dynamic'
import '../../index.css'

const App = dynamic(() => import('../../App'), { ssr: false })

export default function Page() {
  return <App />
}
'use client'

import dynamic from 'next/dynamic'
import '../../index.css'

const App = dynamic(() => import('../../App'), { ssr: false })

export default function Page() {
  return <App />
}

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

Этот файл содержит компонент <Page>, который помечен как клиентский компонент директивой 'use client'. Без этой директивы компонент был бы серверным компонентом.

В Next.js клиентские компоненты пререндерятся в HTML на сервере перед отправкой клиенту. Но поскольку мы сначала хотим получить чисто клиентское приложение, нужно отключить пререндеринг для компонента <App>, динамически импортируя его с опцией ssr, установленной в false:

const App = dynamic(() => import('../../App'), { ssr: false })

Шаг 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>, чтобы воспользоваться автоматической оптимизацией.

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

// После
import logo from '../public/logo.png'
  1. Передавайте свойство src изображения вместо всего объекта в тег <img>:
// До
<img src={logo} />

// После
<img src={logo.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

Теперь запустите npm run dev и откройте http://localhost:3000. Вы должны увидеть своё приложение, работающее на Next.js.

Если ваше приложение следовало стандартной конфигурации Vite, этих шагов достаточно для рабочей версии приложения.

Пример: Посмотрите этот 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, но можете начать постепенно вносить изменения. Вот что можно сделать дальше: