Стварэнне стойкай сторонкі з деталямі фільма за дапамою Next.js App Router
Збір і зберагчэнне дадзейнаў OMDB у Next.js App Router за дапамою асінхронных Server Components, параметраў, якія чакаюць на выконанне, і рэальнага адпрацоўвання кода 404 для сторонкі фільма, якая не будзе выкліквала проблем.
Крыніцы: A Movie Lookup Page in Next.js That Doesn’t Fall Over
Дзеяныя інструкцыі па стварэнні сторонкі для пошуку фільмаў за дапамойкай API OMDB яшчэ циркулююць, і ў іх кодзе від 2019 года: getServerSideProps, імпорт з axios, каталог pages/ і функцыя-памочнік fetch, яка проста выводзіць кожную памылку ў console.error. Нічога з гэтага больш не працюе ў сучасных налашчэннях. З версіі Next.js 16 запошук дадзеных выкананы ў асінхронных Server Components, а params тепер ў формате Promise, які трэба ачакаць пры выкарыстоўванні. Якщо падставіць гэты стары код у свежа створаны проект create-next-app, перакрыўчык типаў адмовіцца будзе саавтарабатваць.
Наступная версія ёсць тая, якаю рэальна можна выдаты: дынамічная сторонка з дакладнымі вяскапамі фільма, якая працюе за дапамойкай API OMDB, створаная за допамойкай App Router, без залежнасці ад axios, і сторонка 404, якая дзейсна паводзіцца як сторонка 404.
Слою дадзеных
OMDB — это простая REST API. Надацея ёй назва фільма, і вы отрымаете JSON. Няма канэцкай бібліятэкі для кліента, якая была б карытая, а axios нічага тут не дадзе — fetch ўжо ўбудованы як у браузер, так і ў Node, а Next.js додае свой скарыбнік на базе fetch у спосаб, у якім axios проста не можа узяць участка.
Дробак, які падводзіць людей: OMDB адпавядае з кодам HTTP 200 нават тады, калі не можа знайсці фільм. Прыгледны паказчык асалоджэння знаходзіцца ў падатку даных, у вачынку "Response": "False". Якщо ваш код пераканальвае толькі res.ok, то неправільна распісва назвы будзе вазіцца за успешны вызов, і вы будете атрымляць undefined у всій сторонцы.
const BASE_URL = "https://www.omdbapi.com/";
export type Movie = {
Title: string;
Year: string;
Plot: string;
Poster: string;
imdbRating: string;
Response: "True" | "False";
Error?: string;
};export async function fetchMovieByTitle(title: string): Promise<Movie | null> {
const apiKey = process.env.OMDB_API_KEY;
if (!apiKey) throw new Error("OMDB_API_KEY is not set"); const url = new URL(BASE_URL);
url.searchParams.set("apikey", apiKey);
url.searchParams.set("t", title);
url.searchParams.set("plot", "full"); const res = await fetch(url, { next: { revalidate: 60 * 60 * 24 } });
if (!res.ok) return null; const data = (await res.json()) as Movie;
return data.Response === "True" ? data : null;
}
Апшарон next: { revalidate: ... } у функцыі fetch якраз і дае гэтай схеме практычную масштабаваемасць. Кантэнт аповесці фільма і яго постер ў большай чысткі є статычным, таму зберагачванне адпаведнай інфармацыі на весь дзень значыць, што другі чалавек, які заходзіць на адресу /movie/inception, ніколі не спрычыняе новага запыту да OMDB. Гэта і є вся стратэгія зберагачвання — не патрэбна ні інстанцыя Redis, ні задача cron для павтапераканалізацыі дадзэнняў. Хочаце свежэй інфармацыі? Скорачыце цяжэрасць. Хочаце, каб дадзеныя ніколі не выгорэлі? Выберыце параметр cache: "force-cache". Дзесяць прямых запытоў да OMDB давалі усерэднена вялічыну 57,8 мс на запыт (50-й процэнтіль — 55,5 мс, а самы худшы прыклад — 112,8 мс), таму сэнс зберагачвання не ў тым, што іх API медленны, а ў тым, каб ужо не платіць нават гэтую незначную суму за кожны запыт.
Стварэнне URL-адреса запытку за дапамою URLSearchParams замест адмануальнага вставкі стракі мае большое значэнне, чым можа здавацца. Назвы фільмаў часта мятуюць праслёвы, знакі & і двоетачкі. Назва на кшталт t: "The Good, the Bad and the Ugly" не будзе працаваць, якщо ўжыцца адмануальная калякуванне, але searchParams.set автаматычна здзейснюе кодаванне.
Старонка
Сюды старэйшыя нарадчыкі не толькі застаюцца застарэлымі — яны нават не працуюць. У системе App Router компонент старонкі ёсць асінхронная функцыя, а params выступае як Promise, які трэба адчакаць. Якщо прыхіліцца да іншага падходу, TypeScript не толькі паведаміць пра проблему; запуск next build абаротна зазнае няўспэху.
import Image from "next/image";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { fetchMovieByTitle } from "@/lib/omdb";
type PageProps = { params: Promise<{ title: string }> };export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { title } = await params;
const movie = await fetchMovieByTitle(decodeURIComponent(title));
if (!movie) return { title: "Movie not found" };
return { title: `${movie.Title} (${movie.Year})`, description: movie.Plot };
}export default async function MoviePage({ params }: PageProps) {
const { title } = await params;
const movie = await fetchMovieByTitle(decodeURIComponent(title));
if (!movie) notFound(); return (
<main>
<h1>
{movie.Title} <span>({movie.Year})</span>
</h1>
<p>IMDb {movie.imdbRating}</p>
<p>{movie.Plot}</p>
{movie.Poster !== "N/A" && (
<Image
src={movie.Poster}
alt={`Poster for ${movie.Title}`}
width={300}
height={445}
priority
/>
)}
</main>
);
}
Выклік notFound() тут выкаанаць значымую роботу. У зменшэнні не трэба выводзіць элемент JSX на кшталт `
Фільм не знайдзены
.хоць і продаўае код статусу 200 — што самэ гэта і робяць старэйшыя версіі, фактычна кажучы Google: «эта сторанка справжняя, можаце яе індексаваць» —функцыя notFound() выклікае асывяненне, атрыбутуе вашфайл `not-found.tsx` і вяртае справжній код статусу 404. Усёлькі, што большасць западзяваных загаворак у такім пошуку за URL не будуць ператварацца на ўсё-які контэнт, гэтае разлічэнне не дазволяе вашаму сайту стаць скупкай непатрэбных сторанак у індексе пошуку.
Функцыя generateMetadata выклікае абсалютна тую ж самую дапаможную функцыю для запрашоўкі дадзэнняў, якая і сама сторана. Можна было бы спадзявацца, што гэта удвоіць колькість запрашоўкаў API, але гэта не так: Next.js автаматычна усувае дублікаты ідэнтычных запрашоўкаў fetch, якія выкананы ў межах адной процэсу перрадзіравання, таму генератор метадаў і компонент стораны выкарыстоўваюць адну і тую ж запрашоўку сеті. Гэта ўпэўненая прычына для таго, каб логіку запрашоўкі дадзэнняў залучыць унутрь Серверных компонентоў, а не пераносіць яе ў useEffect на стороне кліента — версія, якая перрадзіваецца на кліенте, не можа усунуць дублікаты за межамі метадаў.
Є два особлівасці OMDB, які стосуюцца працы з афішамі і лёгкая застаюцца непазорлівымі. Першая — якщо афіша відсутня, ёй прадаёцца буквальны строк "N/A", а не null; таму патрэбна пераконтрація на кшталт movie.Poster !== "N/A" пры передачы значэння ў next/image — якщо падаць "N/A" безпосередна як src, то будзе абярваныя картінка і паведамленне пра адзінак у консолі. Другая — next/image адмовляецца завантажваць картінкі з удаленага домэна, які не быў явна дадзен у список дазволеных, таму хост картінак OMDB патрэбуе свой элемент у next.config:
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;
Што гэта яшчэ не робіць
У гэтай реалізацыі няма поля для пошуку — яна выкарыстоўвае толькі адну точную назву з URL, і гэта ўмышлена, адтаколькі параметр t= у OMDB выкарыстоўвае точнае падабранне, тым час як ўзлёгкае падабранне за дапамогою параметра s= адбываецца на адной зусім іншай, менш прыемлівай для працы точкі выходу. Таксама няма шару для зберагчэння даных; кожны унікальны запит все равно надходзіць да OMDB, проста праз кэш. Якщо ваш проект патрабуе такіх функцыяй, як спіс улюбленых або історыя перагляду, то для гэтага патрэбна справжня база дадзеных, і гэта ўжо аблікаванне парадульнае, якая выходзіць за межы ціх старонак.
Першым пунктом, які варта розглянуць пры пераходзе на практычнае викорыстанне, є ліміт безплатнага тарифу OMDB — 1 000 запыткаў на дзень. Якірная перапрацоўка даных значна расширыяе гэты ліміт, але калі навантажэння перасягне рэверсныя значэння для хобі-праектаў, вам знадобится сашыльны слой, які будзе располагацца перед OMDB — кэш для чытання, які заполняецца толькі у разе неудачнай спробы зняць даны. Усё, што описана вышэй, ў сутнасці являе сабою першую частку такой архітектуры.
Галоўны вывод такі: «масштабаванне» ў гэтым контексте — цэлкам не якаясь додатковая платформа, яку можна прыўязаць пазней. Усё зводзіцца да правильнага задання значэння revalidate, вярнення належнага коду 404 і перакананняся, што вы ніколі не вызываеце API два разы, калі дастатнек адзін вызов.