Prikaz slika
Slike su važan dio gotovo svake web stranice, ali osim utjecaja na dojam korisnika često imaju i najveći utjecaj na brzinu učitavanja stranice. Zbog toga je važno voditi računa o slikama i načinu na koji se učitavaju i prikazuju. Jedan od gorih korisničkih doživljaja je tzv. "content shift" kada se zbog naknadnog učitavanja slike pomakne sadržaj cijele stranice. Next.js sadrži ugrađenu <Image> komponentu koja predstavlja optimizirano rješenje za učitavanje i prikaz slika.
Neke od najvažnijih funkcionalnosti <Image> komponente su:
-
Automatska optimizacija: Slike se poslužuju u dimenzijama prikladnim za uređaj na kojem se prikazuju te koriste moderne formate (WebP)
-
Sprječavanje “layout shift”: Slikama postavljamo dimenzije (širinu i visinu) ili koristimo atribut fill, te na taj način rezerviramo prostor na stranici, čime izbjegavamo "skakanje" sadržaja nakon učitavanja.
-
Brže učitavanje: Slike se učitavaju kada uđu u vidljivo polje preglednika (viewport) - tzv. "lazy-loading, uz opcionalno korištenje blur placeholder-a.
Izvor slika može biti interni (slike se nalaze na istom poslužitelju kao i stranica) ili eksterni (slike dohvaćamo sa udaljene lokacije). U našem slučaju koristimo vanjski API, te slike dohvaćamo sa udaljenog poslužitelja.
Učitavanje vanjskih slika
Pogledajmo prvo API dokumentaciju (opens in a new tab) kako bi vidjeli gdje nam se nalaze slike Pokemona. Slike se nalaze u svojstvu sprites te vidimo da su spremljene na adresi "https://raw.githubusercontent.com/ (opens in a new tab)".
Prvo što moramo napraviti kod učitavanja slika sa udaljenje (remote) lokacije je konfigurirati našu aplikaciju i definirati sa koje lokacije može učitavati slike. Ovo je ujedno i sigurnosna značajka kako bi spriječili bilo kakvo učitavanje malicioznih datoteka iz nepouzdanih izvora. Definiranje povjerljive lokacije radimo pomoću datoteke next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "raw.githubusercontent.com",
port: "",
pathname: "/PokeAPI/**",
},
],
},
};
export default nextConfig;Sada možemo prilagoditi našu stranicu sa detaljima pojedinačnog Pokemona kako bi dodali prikaz slike. Prvo što moramo napraviti je na početku stranice učitati samu "Image" komponentu
import Image from "next/image";Dio sa učitavanjem podataka nam ostaje isti (slike se nalaze u istom objektu kojeg dobivamo od API-ja). Jedino smo izbacili dio sa provjerom postoji li Pokemon. S obzirom da na ove stranice dolazimo preko poveznice, ne može se dogoditi da imamo pogrešni name parametar (u produkcijskim aplikacijama ne bi bilo loše ostaviti tu provjeru):
const { name } = await params;
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
const data = await res.json();Sam API sadrži mnoštvo različitih slika za svakog Pokemona, ali mi ćemo za početak učitati samo onu osnovnu. Nakon pregleda dokumentacije, možemo iz dobivenog podatka dohvatiti URL slike (i usput provjeriti postoji li):
const spriteUrl = data.sprites.front_default;
if (!spriteUrl) {
return <p>Pokémon slika nije dostupna.</p>;
}Preostaje nam još samo modificirati izgled same komponente. Uz prikaz postojećih podataka, dodati ćemo i sliku uz pomoć <Image> komponente. Pri korištenju te komponente vodite računa o nekoliko najvažnijih detalja:
- lokaciju slike navodimo putem src atributa (kao i kod "običnih" slika)
- kada koristimo udaljeni izvor, potrebno je navesti dimenzije (atributi width i height) slike
- po želji možemo postaviti i atribut
priority={true}što će uzrokovati brže učitavanje (preload) slike - ne zaboravite navesti alt atribut
return (
<main className={styles.info}>
<h1 className={styles.title}>{data.name}</h1>
<Image
src={spriteUrl}
alt={`Sprite ${data.name}`}
width={150}
height={150}
priority={true}
/>
<p>ID: {data.id}</p>
<p>Težina: {data.weight}</p>
<p>Visina: {data.height}</p>
</main>
);Kompletna komponenta sada izgleda ovako:
import Image from "next/image";
import styles from "./page.module.css";
export default async function PokemonDetalji({ params }) {
const { name } = await params;
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
const data = await res.json();
const spriteUrl = data.sprites.front_default;
if (!spriteUrl) {
return <p>Pokémon slika nije dostupna.</p>;
}
return (
<main className={styles.info}>
<h1 className={styles.title}>{data.name}</h1>
<Image
src={spriteUrl}
alt={`Sprite ${data.name}`}
width={150}
height={150}
priority={true}
/>
<p>ID: {data.id}</p>
<p>Težina: {data.weight}</p>
<p>Visina: {data.height}</p>
</main>
);
}Prilagodljive slike
Osim fiksnih dimenzija, slike također možemo podesiti na način da se dinamički prilagođavaju okviru u kojem se nalaze. To možemo napraviti u nekoliko koraka:
- "Image" komponenti dodamo atribut fill (možemo i
fill={true}) - Slika se mora nalaziti u roditeljskom elementu koja mora imati definirane dimenzije te svojstvo
position: "relative",position: "fixed", iliposition: "absolute". - "Image" komponenti bi također trebali dodati "objectFit" stil i postaviti ga na
cover,contain,fill,noneiliscale-down. Za detaljna objašnjenja pogledajte MDN dokumentaciju (opens in a new tab).
Ovako bi izgledao jedan primjer korištenja fill atributa (zanemariti ćemo korištenje inline stila za roditeljski element):
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src={spriteUrl}
alt={data.name}
fill
sizes="100vw"
style={{ objectFit: 'contain' }}
/>
</div>Učitavanje lokalnih slika
Pogledajmo sada primjer učitavanja lokalnih slika. Uz tekst na zaglavlju aplikacije ćemo dodati i JuniorDev logo. Logo možete preuzeti sa ove stranice (desni klik na logo u zaglavlju aplikacije i odaberite opciju "Save image as"). Spremite ga u direktorij "public" na glavnoj razini aplikacije.
Vratimo se sada u datoteku layout.js i prvo što moramo napraviti je uključiti <Image> komponentu, te našu sliku (logo).
import Image from "next/image";
import logo from "../public/juniorDev.png"
// ostatak komponenteNakon toga jednostavno dodamo komponentu i navedemo učitanu sliku kao izvor. Ovoga puta nije potrebna nikakva dodatna konfiguracija jer sliku učitavamo iz lokalnog izvora. Također možete uočiti da nije potrebno eksplicitno navoditi dimenzije slike:
export default function RootLayout({ children }) {
return (
<html lang="hr">
<body>
<header className="info">
<Image
src={logo}
alt="JuniorDev Next.js"
/>
JuniorDev Next.js
</header>
{children}
</body>
</html>
);
}S obzirom da nismo navodili dimenzije slike, učitani logo će biti prevelik u odnosu na ostatak stranice. Iskoristiti ćemo tu činjenicu kako bi demonstrirali mogućnost blur opcije u <Image> komponenti. Kod lokalnih slika, postupak je jednostavan, samo moramo dodati placeholder atribut i postaviti ga na vrijednost blur:
export default function RootLayout({ children }) {
return (
<html lang="hr">
<body>
<header className="info">
<Image
src={logo}
alt="JuniorDev Next.js"
placeholder="blur"
/>
JuniorDev Next.js
</header>
{children}
</body>
</html>
);
}Korištenjem ove opcije, prilikom učitavanje slike će se prvo pojaviti njena "zamućena" (blur) verzija koja će zatim biti zamijenjena "pravom" slikom, jednom kada se dovrši učitavanje. Kod lokalnih slika se blur verzije generiraju automatski.
Za učitavanje vanjskih slika, u slučaju korištenja blur opcije, moramo navesti i dodatno svojstvo blurDataURL koje sadrži putanju do privremene verzije slike.
Ovako bi izgledao primjer korištenja lokalne slike kao blur verzije dok se ne učita eksterna slika:
<Image
src={spriteUrl}
alt={`Sprite ${data.name}`}
width={150}
height={150}
priority={true}
placeholder="blur"
blurDataURL="/placeholder.jpg"
/>U gornjem primjeru se pretpostavlja da imamo datoteku placeholder.jpg u public direktoriju naše aplikacije. Next.js također dozvoljava i opciju učitavanja slike u "base64" formatu.
Oblikovanje pomoću Tailwinda
Iako smo na početku postavili projekt bez korištenja Tailwinda, sada ćemo demonstrirati kako naknado dodati Tailwind u naš projekt, te oblikovati <Image> komponentu pomoću Tailwind klasa.
Za početak moramo zaustaviti aplikaciju (dev poslužitelj) i pokrenuti dvije naredbe za instalaciju Tailwind paketa:
npm install tailwindcss @tailwindcss/postcss postcssZatim ćemo u glavnom dijelu aplikacije napraviti datoteku postcss.config.mjs kako bi dodali PostCSS plugin u našu aplikaciju:
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;Idući korak je import Tailwind CSS-a u globalni stil:
@import "tailwindcss";Nakon ovoga možemo pokrenuti aplikaciju sa naredbom npm run dev, te početi koristiti Tailwind klase.
Tailwind koristi tzv. utility klase pomoću kojih oblikujemo izgled elemenata na stranici. Umjesto pisanja vlastitih klasa sa pravilima oblikovanja, Tailwind ima definiran veliki broj helper klasa pomoću kojih primjenjujemo unaprijed definirana pravila na naše elemente (npr. bg-blue-500 ili text-center). Ovaj pristup nam omogućuje vrlo brzu izradu i prototipiranje dizajna bez potrebe za pisanjem odvojenih CSS datoteka za svaku komponentu.
Iskoristiti ćemo Tailwind za oblikovati <main> element na stranici za prikaz detalja pojedinačnih Pokemona:
return (
<main className="flex flex-col items-center p-4">
<h1 className={styles.title}>{data.name}</h1>
<Image
src={spriteUrl}
alt={`Sprite ${data.name}`}
width={150}
height={150}
priority={true}
/>
<p>ID: {data.id}</p>
<p>Težina: {data.weight}</p>
<p>Visina: {data.height}</p>
</main>
);Na ovom primjeru imamo kombinaciju više stilova - dio nam dolazi iz globalnih pravila definiranih unutar globals.css, dio iz Tailwinda, te dio iz lokalnog CSS modula. To definitivno nije najbolji pristup - bilo bi poželjno barem ukloniti globalna CSS pravila jer će Tailwind svakako prebrisati neke zadane postavke (npr. margin i padding). U slučaju da se odlučite za Tailwind, preporuka je da glavnina oblikovanja bude definirana kroz njega, dok module možete koristiti za neke specifične ili posebno prilagođena oblikovanja.
