Миграция с Vite
Это руководство поможет вам перенести существующее приложение с Vite на Next.js.
Почему стоит перейти?
Есть несколько причин, по которым вы можете захотеть перейти с Vite на Next.js:
Медленная загрузка начальной страницы
Если вы создали приложение с помощью стандартного плагина Vite для React, ваше приложение является чисто клиентским. Клиентские приложения, также известные как одностраничные приложения (SPA), часто страдают от медленной загрузки начальной страницы. Это происходит по нескольким причинам:
- Браузеру нужно дождаться загрузки и выполнения кода React и всего бандла приложения, прежде чем ваш код сможет отправить запросы для загрузки данных.
- Код вашего приложения растёт с каждым новым функционалом и дополнительной зависимостью.
Отсутствие автоматического разделения кода
Проблему медленной загрузки можно частично решить с помощью разделения кода (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
как зависимость:
npm install next@latest
Шаг 2: Создание файла конфигурации Next.js
Создайте файл next.config.mjs
в корне вашего проекта. Этот файл будет содержать настройки конфигурации Next.js.
/** @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 не используется, этот шаг можно пропустить.
- Удалите ссылку на проект
tsconfig.node.json
- Добавьте
./dist/types/**/*.ts
и./next-env.d.ts
в массивinclude
- Добавьте
./node_modules
в массивexclude
- Добавьте
{ "name": "next" }
в массивplugins
вcompilerOptions
:"plugins": [{ "name": "next" }]
- Установите
esModuleInterop
вtrue
:"esModuleInterop": true
- Установите
jsx
вpreserve
:"jsx": "preserve"
- Установите
allowJs
вtrue
:"allowJs": true
- Установите
forceConsistentCasingInFileNames
вtrue
:"forceConsistentCasingInFileNames": true
- Установите
incremental
вtrue
:"incremental": true
Пример рабочего 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
в файл корневого макета:
- Создайте новую директорию
app
в вашей директорииsrc
. - Создайте новый файл
layout.tsx
внутри директорииapp
:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return null
}
export default function RootLayout({ children }) {
return null
}
Полезно знать: Для файлов макета можно использовать расширения
.js
,.jsx
или.tsx
.
- Скопируйте содержимое вашего файла
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>
)
}
- 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>
)
}
- Любые файлы метаданных, такие как
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>
)
}
- Наконец, 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
. На этом шаге вы настроите входную точку вашего приложения.
- Создайте директорию
[[...slug]]
в вашей директорииapp
.
Поскольку в этом руководстве мы сначала стремимся настроить Next.js как SPA (Single Page Application), вам нужно, чтобы ваша входная точка страницы перехватывала все возможные маршруты вашего приложения. Для этого создайте новую директорию [[...slug]]
в директории app
.
Эта директория называется опциональным сегментом маршрута с перехватом всех путей. Next.js использует файловую систему для маршрутизации, где директории используются для определения маршрутов. Эта специальная директория гарантирует, что все маршруты вашего приложения будут направлены в содержащийся в ней файл page.tsx
.
- Создайте новый файл
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 в виде строки:
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, который имеет автоматическую оптимизацию изображений.
- Преобразуйте абсолютные пути импорта для изображений из
/public
в относительные импорты:
// До
import logo from '/logo.png'
// После
import logo from '../public/logo.png'
- Передавайте свойство
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.MODE
⇒process.env.NODE_ENV
import.meta.env.PROD
⇒process.env.NODE_ENV === 'production'
import.meta.env.DEV
⇒process.env.NODE_ENV !== 'production'
import.meta.env.SSR
⇒typeof window !== 'undefined'
Next.js также не предоставляет встроенную переменную окружения BASE_URL
. Однако вы всё равно можете её настроить, если она вам нужна:
- Добавьте следующее в ваш файл
.env
:
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
- Установите
basePath
вprocess.env.NEXT_PUBLIC_BASE_PATH
в вашем файле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
- Обновите использование
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
:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
# ...
.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, но теперь можете начать вносить постепенные изменения, чтобы получить все выгоды. Вот что вы можете сделать дальше:
- Перейдите с React Router на Next.js App Router, чтобы получить:
- Автоматическое разделение кода
- Потоковый серверный рендеринг (Streaming Server-Rendering)
- Серверные компоненты React (React Server Components)
- Оптимизируйте изображения с помощью компонента
<Image>
- Оптимизируйте шрифты с помощью
next/font
- Оптимизируйте сторонние скрипты с помощью компонента
<Script>
- Обновите конфигурацию ESLint для поддержки правил Next.js