✍️ Пробую новые возможности Next.js 13. Часть 2.
Всех приветствую и желаю приятного чтения!
Next.js это fullstack фреймворк разработанный Vercel использующий последние разработки React.
Не так давно 25 октября 2022 года вышла версия 13. На данный момент последняя стабильная версия 13.2.3, и новые возможности все еще находятся в стадии бета теста, и не рекомендуется использовать в продакшен.
13 поддерживает все возможности версии 12. Для тестирования новых возможностей используется специальная директория app. Такой подход помогает попробовать новые возможности, в проектах, которые работали на версии 12.
В этой статье я пробую использовать только новые возможности версии 13, кому интересно больше узнать о Next.js рекомендую: Next.js: подробное руководство. Итерация первая.
Краткое содержание статьи
Содержание первой части статьи:
Серверные и клиентские компоненты
Серверные компоненты доступны стали доступны для использования. Рассмотрим особенности определения серверного и клиентского кода, задачи и возможности компонентов, использование одном дереве компонентов и серверные функции.
Выборка данных и кэширование
Добавлена новая функция выборки fetch c возможностью настройки кэширования, которая может использоваться на клиенте и сервере. Клиентскому маршрутизатору добавлено автоматическое кэширование сегментов при навигации. Серверный кэш сегментов.
Сегмент - это часть URL пути разделенная слешами.
Маршрутизация
Построена на работе с сегментами и новой файловой структурой. Основные темы:
- Родительский сегмент содержит компоненты обертки над дочерними сегментами, они добавляют: обработку ошибок, состояние загрузки, слои, шаблоны и другие обертки, подробнее о которых будет в главе "Файлы сегмента маршрута".
- Route groups - для организации сегментов, для того чтобы применить к ним одинаковые настройки, организовать сегменты в структуру, не влияя на структуру URL, создания нескольких корневых layout.
- Динамические сегменты - для построения маршрутов из динамических данных, основан на использование квадратных скобок в именах файлов и директорий, не сильно отличается от того что используется в pages. Подробности в главе "Динамические сегменты".
- Route Handlers - обработчики маршрута для построения своего API для обработки http запросов, альтернатива pages/api. Подробности в главе "Обработчики маршрута".
Содержание этой статьи:
Потоковой передачи Http и компонент Suspense
Использование потоковой передачи Http в сочетании компонентом Suspense возможно для серверных и клиентских компонентов, находящихся в одном дереве компонентов. Подробности в "HTTP Streaming и Suspense"
Метаданные и SEO оптимизация
Новый подход к добавлению метаданных на страницу c помощью объектов js и поддержка JSON-LD - это формат микроразметки описания контента с помощью объектов словаря связанных данных.
Немного заметок и выводы
Для каждого раздела есть пример кода…
Примеры кода
Все примеры хранятся в репозитории Github next13-app-exp и развернуты на Vercel, потому что там можно автоматически развернуть в продакшен каждую ветку.
Список примеров по названию веток:
- Code router-dynamic / Online Demo - пример работы с динамической маршрутизации, и тест параметра сегмента dynamicParams управляющим динамической генерацией страниц после сборки. Пока есть проблема с подключением своего not-found.js и в этом обсуждении есть обходной путь.
- Code context / Online demo - пример работы с контекстом в клиентских компонентах используется в главе "Работа с контекстом на стороне клиента".
- Code server-fetch-standalone / Online demo- пример работы серверного и клиентского fetch с опцией revalidate: 60, с кэшем подробнее в главе "Выборка данных и кэширование". Пока опция revalidate: 60 не работает баг репорт
- Code static-dynamic-segments / Online demo - пример использования статических и динамических сегментов в одном url пути, в зависимости от того какие будут параметры последнего сегмента, так будет генерироваться весь путь.
- Code suspense / Online Demo - демонстрация потоковой передачи данных. Нескольких серверных компонентов, делают выборку на стороне сервера, и загружаются в одном клиентском компоненте с использованием компонента Suspense не нарушая интерактивность страницы. Подробности в главе "Потоковая передача и компонент Suspense".
- server-fetch-custom-cache, - делаем свой кэш для демонстрации работы с данными в серверных компонентах. Подробнее будет в главе "Передача данных между серверными компонентами".
Примеры, используемые в главе "Маршрутизация":
- Code loading / Online Demo - пример работы файла loading.js, который добавляет обертку Suspense к сегменту
- Code error-boundaries / Online Demo - пример работы файла error.js перехват ошибок клиентских и серверных компонентов.
- Code templates / Online Demo - пример работы файла template.tsx, форма обратной связи одна для всех сегментов и перезагружается на каждый переход между сегментами, за исключением сегментов, объединенных с помощью Route Groups.
- Code multiple-root-layouts / Online Demo - пример работы нескольких Root Layout, в этом примере нет корневого файла layout.js, вместо этого созданы две папке в каждой из которых есть layout.js Root Layout. Примечание: При переключении между Root Layout происходит полная перезагрузка страницы. В 13.1.6 было немного другое поведение, и я надеялся, что полной перезагрузки не будет. Обсуждение так было в 13.1.6 можно было перейти на другой root layout 3 раза без перезагрузки страницы.
Есть еще большая демка от Vercel для тестирования новых возможностей.
В github репозитории Next.js 13 в папке examples можно найти несколько примеров адаптированных для app:
- app-dir-i18n-routing - многоязычный сайт, сделанный через множественный RootLayout
- app-dir-mdx -
- reproduction-template-app-dir
- with-grafbase - работа с graphlq
Установка и использование новых экспериментальных возможностей
Для установки с использованием новых возможностей можно использовать create-next-app с опцией experimental-app
npx create-next-app@latest --experimental-app
Если хотите попробовать самые последние обновления, которые еще не вошли в основную ветку нужно установить версию canary вместо latest.
Включаем экспериментальное возможности, если установка была без experimental-app
next.config.js
const nextConfig = {
experimental: {
appDir: true,
},
}
После установки будет доступна папка app в которой можно тестировать новые возможности, папка pages также доступна в которой все работает также, как и в 12 версии. pages и app работают одновременно, в app также, как и в pages можно настраивать маршрутизацию и нужно следить за тем чтобы маршруты не пересекались. Одновременное использование папок app и pages дает возможность протестировать уже существующие проекты, частично используя нововведения из папки app.
В документации есть гайд по миграции приложения из папки pages в app.
Потоковая передача и компонент Suspense
При потоковой передаче HTTP сервер настроен на удержание определенного запроса от клиента и сохранение ответа открытым, чтобы он мог передавать через него данные. Клиент может прослушивать обновления с сервера и получать их мгновенно без каких-либо накладных расходов, связанных с HTTP-заголовками и открытием/закрытием соединений.
В сочетании с клиентскими компонентами и Suspense, серверные компоненты React могут передавать контент через потоковую передачу по HTTP.
Потоковая передача хорошо работает с компонентной моделью React, потому что каждый компонент можно рассматривать как фрагмент (chunk). Это позволяет отображать части страницы раньше, не дожидаясь загрузки всех данных, прежде чем можно будет отрисовывать какой-либо пользовательский интерфейс.
В Next.js можете реализовать потоковую передачу используя loading.js, для всего сегмента маршрута, или с Suspense, для более детального контроля.
Полный код примера, demo online
import { Suspense } from "react";
import { Spinner } from "@/components/Spinner";
import { ServerComponent } from "@/components/ServerComponent";
import { ClientComponent } from "@/components/ClientComponent";
export default async function Page({ params }: { params: { id: string } }) {
return (
<ClientComponent id={params.id}>
<Suspense fallback={<Spinner />}>
{/* @ts-expect-error Server Component */}
<ServerComponent delay={1} />
</Suspense>
<Suspense fallback={<Spinner />}>
{/* @ts-expect-error Server Component */}
<ServerComponent delay={2} />
</Suspense>
<Suspense fallback={<Spinner />}>
{/* @ts-expect-error Server Component */}
<ServerComponent delay={3} />
</Suspense>
</ClientComponent>
);
}
export const dynamic = "force-dynamic";
В этом примере демонстрируется как с помощью Suspense блоков можно разбить на фрагменты загружаемый контент.
SEO оптимизация и метаданные
Next.js SEO & Metadata поддерживает описание метаданных с помощью тега meta и JSON-LD это формат микроразметки описания контента с помощью объектов, коллекция взаимосвязанных наборов данных в WEB. Эти данные могут быть экспортированы из layout.js и page.js. Метаданные могут быть размещены только в серверных компонентах.
Метаданных в тегах meta
До 13.2 метаданные размещались в файле head.js это был типичный html формат.
export default function Head() {
return (
<>
<title>Create Next App</title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</>
)
}
Начиная с 13.2 новый формат это статические экспортируемый объект с именем metadata или динамический созданный с помощью generateMetadata. Формат метаданных описан в документации
Пример (Code repository , sandbox, deploy) добавления метаданных индивидуальный для каждого сегмента:
app\services\page.tsx
import { metaTags } from "@/data";
export const metadata = {
title: metaTags.services.title,
description: metaTags.services.description,
keywords: metaTags.services.keywords,
icons: {
icon: "/favicon.ico",
},
};
export default async function Page (){
return <>Service page</>
}
app\solutions\page.tsx
import { metaTags } from "@/data";
export const metadata = {
title: metaTags.solutions.title,
description: metaTags.solutions.description,
keywords: metaTags.solutions.keywords,
icons: {
icon: "/favicon.ico",
},
};
export default async function Page (){
return <>Solutions page</>
}
JSON-LD
JSON-LD — это формат микроразметки описания контента с помощью объектов словаря связанных данных. JSON-LD поддерживается в Yandex и Google
Пример использования
export default async function Page({ params }) {
const product = await getProduct(params.id);
const jsonLd = {
"@context": "http://schema.org",
"@type": "FlightReservation",
reservationId: "RXJ34P",
};
return (
<section>
{/* Add JSON-LD to your page */}
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
{/* ... */}
</section>
);
}
В примере показаны три ключа:
- @context (зарезервированный) — указывает на то, что в объекте используется словарь Schema.org.
- @type (зарезервированный) — указывает на тип FlightReservation, в свойствах которого можно указать данные о бронировании билета на авиарейс.
- reservationId — соответствует свойству reservationId типа FlightReservation и содержит номер бронирования билета.
Заметки
Вызов функций в JSX клиентских компонентов
С виду безвредный код
<div>
{moment(value).format("MMMM Do YYYY, h:mm:ss a")}
</div>
дает предупреждение
Text content did not match. Server: "February 27th 2023, 10:44:57 pm" Client: "February 27th 2023, 10:44:59 pm"
Решение создать клиентский компонент, похожее решение ]предлагалось для библиотек компонентов, не адоптированных к использованию "use client"](https://beta.nextjs.org/docs/rendering/server-and-client-components#convention)
"use client"
const ClientMoment = ({ val }: { val?: string }) => {
const [valDate, setValDate] = React.useState<string>();
React.useEffect(() => {
setValDate(moment(val).format("MMMM Do YYYY, h:mm:ss a"));
}, [val]);
return <div>{valDate}</div>;
};
и вносить изменения именно через useEffect, если написать просто
const [valDate, setValDate] = React.useState(moment(val).format("MMMM Do YYYY, h:mm:ss a"));
предупреждение продолжит появляться
Работа с контекстом в клиентских компонентах
React Context - Контекст позволяет передавать данные через дерево компонентов без необходимости передавать пропсы на промежуточных уровнях. Дока по использованию контекста в app.
- подключаем контекст в layout.js и используем в каждом из сегментов about, blog и shop.
│ ClientContext.tsx
│ globals.css
│ layout.module.scss
│ layout.tsx
│ page.tsx
│
├───(marketing)
│ ├───about
│ │ page.tsx
│ │
│ └───blog
│ page.tsx
│
└───(shop)
└───account
page.tsx
файл app\ClientContext.tsx - создаем контекст и клиентский компонент, который будем подключать в дерево серверных компонентов в файле layout.js.
"use client";
import React from "react";
interface IContexte {
id: string;
setId: (id: string) => void;
}
export const Context = React.createContext<IContexte | null>(null);
export function ClientContext({ children }: { children: React.ReactNode }) {
const [id, setId] = React.useState("");
return <Context.Provider value={{ id, setId }}>{children}</Context.Provider>;
}
Подключаем контекст в layout.js
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
…
<main className={styles.main}>
<ClientContext>{children}</ClientContext>
</main>
…
);
}
в page.js добавляем клиентский компонент для использования контекста, далее работаем через useContext как обычно.
app\page.js
import { Page } from "@/components/Page";
export default function Home() {
return <Page headerText="Home"/>;
}
components\Page.tsx
"use client";
import styles from "./Page.module.scss";
import React from "react";
import { Context } from "@/app/ClientContext";
export const Page = ({ headerText }: { headerText: string; }) => {
const context = React.useContext(Context);
const [input, setInput] = React.useState(context?.id as string);
const handlerSetId = () => {
context?.setId(input);
};
return (
<section className={styles.section}>
<h2 className={styles.header}>{headerText}</h2>
<div>Current id: {context?.id} </div>
<div className={styles.inputGroup}>
<input
type="text"
onChange={(e) => setInput(e.target.value)}
value={input}
className={styles.input}
/>
<button onClick={handlerSetId} className={styles.button}>
setId
</button>
</div>
</section>
);
};
Контекст работает так, как и ожидалось никаких проблем не замечено.
Передача данных между серверными компонентами
Серверные компоненты не работают с сосанием и контекстом, для передачи данных между компонентами рекомендуется использовать:
- кэш операций функций таких как fetch, т.е. вызывая функцию с одинаковыми параметрами мы должны получать одинаковый результат, или разный в зависимости от того устарел ли кэш. В любом случае этот результат будет релевантным. Тут подойдет пример, который использовался в главе "выборка данных и кэширование" server-fetch-standalone, если переключатель radiobutton установить на работу с серверной функцией fetch, так как параметр revalidate пока не работает в браузере.
- собственные шаблоны JavaScript, такие как глобальные синглтоны, в пределах области действия модуля, если у вас есть общие данные, к которым необходимо получить доступ нескольким серверным компонентам. Этот пример разработаем в этой главе.
Пример передачи данных через модули es6 и собственный кэш.
Это код использования серверной функции fetch
import "server-only";
interface IfetchData {
(id: string): Promise<string>;
}
type TCache = {
[key: string]: string;
};
const cache: TCache = {};
export const fetchData: IfetchData = (id) =>
new Promise(async (resolve) => {
let data = "";
try {
if (cache[id]) {
data = cache[id];
} else {
const response = await fetch("http://localhost:3001/users/" + id, {
cache: "no-store",
});
data = JSON.stringify(await response.json());
cache[id] = data;
}
} catch (e) {
if (typeof e === "string") {
data = `Error: ${e.toUpperCase()} `;
} else if (e instanceof Error) {
data = `Error: ${e.message}`;
}
}
resolve(data);
});
в качестве хранилища кэша используется переменная cache. Функция в серверном компоненте fetch в этом примере вызываете с параметрами не использовать кэш ( cache: "no-store" ).
Для того чтобы протестировать как работает серверная функция fetch я использовал json-server и генератор json mockaroo
db.json - база данных для json-server
запуск json-server
json-server --watch ./db.json -p 3001
во время работы сервера ведется лог запросов
GET /users/1 200 45.345 ms - 157
GET /users/2 200 27.988 ms - 155
GET /users/3 200 20.497 ms - 155
Выводы
Сейчас все еще ведется активная разработка беты версии добавляются новые возможности. Последние из недавно добавленных в версии 13.2 это Route Handlers, есть Api которое уже устарело.
Есть некоторые нерешенные проблемы, которые публиковал я:
- Свой компонент для страницы 404 может быть вызван только с помощью функции notFound
- Next.js fetch не работает с опцией revalidate - эту проблему можно обойти использовав, свой клиент для выборки с кэшированием
Из приятных новостей:
- Хорошо написанная документация с примерами на typescript.
- Удобное использование клиентских и серверных компонентов в одном дереве компонентов.
- Кэширование сегментов и запросов на клиенте и сервере.
- Маршрутизация с использованием компонентов оберток делает код понятнее и проще.
- Потоковая передача данных по HTTP с использованием React.Suspense.
Нейтральные изменения для меня, нововведений:
- Использование нового формата метаданных и поддержка JSON-LD.
Спасибо Вам что дочитали до конца, надеюсь приятно провели время и получили полезную информацию!