Галоўная / Артыкулы

Стварэнне стойкай сторонкі з деталямі фільма за дапамою Next.js App Router

Збір і зберагчэнне дадзейнаў OMDB у Next.js App Router за дапамою асінхронных Server Components, параметраў, якія чакаюць на выконанне, і рэальнага адпрацоўвання кода 404 для сторонкі фільма, якая не будзе выкліквала проблем.

Выдавец: REACTAPP.TOP7 хв чытання1556 слоў

Крыніцы: 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 два разы, калі дастатнек адзін вызов.