Миграция с Vite
Это руководство поможет вам перенести существующее приложение с Vite на Next.js.
Почему стоит перейти?
Есть несколько причин, по которым вы можете захотеть перейти с Vite на Next.js:
- Медленная загрузка начальной страницы: Если вы создали приложение с помощью стандартного плагина Vite для React, ваше приложение является чисто клиентским. Клиентские приложения, также известные как одностраничные приложения (SPA), часто страдают от медленной загрузки начальной страницы. Это происходит по нескольким причинам:
- Браузеру нужно дождаться загрузки и выполнения кода React и всего бандла приложения, прежде чем ваш код сможет отправить запросы для загрузки данных.
- Код вашего приложения растёт с каждым новым функционалом и дополнительными зависимостями.
- Отсутствие автоматического разделения кода: Проблему медленной загрузки можно частично решить с помощью разделения кода. Однако при ручном разделении кода вы можете случайно ухудшить производительность. Легко непреднамеренно создать водопад сетевых запросов при ручном разделении кода. Next.js предоставляет встроенное автоматическое разделение кода в своём роутере.
- Водопады сетевых запросов: Частая причина плохой производительности — последовательные клиент-серверные запросы для получения данных. В SPA распространён паттерн, когда сначала рендерится заглушка, а данные запрашиваются после монтирования компонента. К сожалению, это означает, что дочерний компонент, запрашивающий данные, не может начать запрос, пока родительский компонент не завершит загрузку своих данных. В Next.js эта проблема решена благодаря загрузке данных в серверных компонентах.
- Быстрые и контролируемые состояния загрузки: Благодаря встроенной поддержке стриминга с Suspense, в Next.js вы можете точнее контролировать, какие части интерфейса загружать первыми и в каком порядке, не создавая водопадов запросов. Это позволяет создавать страницы, которые загружаются быстрее, а также избегать сдвигов макета.
- Выбор стратегии загрузки данных: В зависимости от потребностей, Next.js позволяет выбирать стратегию загрузки данных для каждой страницы и компонента. Вы можете загружать данные во время сборки, при запросе на сервере или на клиенте. Например, вы можете загружать данные из CMS и рендерить статьи блога во время сборки, что затем позволит эффективно кэшировать их на CDN.
- Middleware: Next.js Middleware позволяет выполнять код на сервере до завершения запроса. Это особенно полезно, чтобы избежать мелькания неаутентифицированного контента, когда пользователь заходит на страницу, доступную только для авторизованных пользователей, перенаправляя его на страницу входа. Middleware также полезен для экспериментов и интернационализации.
- Встроенные оптимизации: Изображения, шрифты и сторонние скрипты часто значительно влияют на производительность приложения. 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, который будет оборачивать все страницы приложения. Этот файл находится на верхнем уровне директории 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 (одностраничное приложение), вам нужно, чтобы точка входа страницы перехватывала все возможные маршруты вашего приложения. Для этого создайте новую директорию [[...slug]]
в директории app
.
Эта директория называется опциональным перехватывающим сегментом маршрута. Next.js использует файловую систему для маршрутизации, где директории определяют маршруты. Эта специальная директория гарантирует, что все маршруты вашего приложения будут направлены в содержащийся в ней файл page.tsx
.
- Создайте новый файл
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 в виде строки:
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>
, чтобы воспользоваться автоматической оптимизацией.
- Преобразуйте абсолютные пути импорта для изображений из
/public
в относительные:
// До
import logo from '/logo.png'
// После
import logo from '../public/logo.png'
- Передавайте свойство
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.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
Теперь запустите 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, но можете начать постепенно вносить изменения. Вот что можно сделать дальше:
- Перейти с React Router на App Router в Next.js для получения:
- Автоматического разделения кода
- Стримингового рендеринга на сервере
- Серверных компонентов React
- Оптимизировать изображения с компонентом
<Image>
- Оптимизировать шрифты с
next/font
- Оптимизировать сторонние скрипты с компонентом
<Script>
- Обновить конфигурацию ESLint для поддержки правил Next.js