Statyczna strona w Next.js — dynamiczne podstrony

  • 2020-11-10

Z poprzedniego wpisu dotyczącego Next.js mogłeś dowiedzieć się, jak utworzyć nowy projekt w Next.js, jak wygląda struktura plików oraz katalogów w takim projekcie, oraz jak skonfigurować w nim obsługę TypeScript. Jeśli jeszcze go nie przeczytałeś, zajrzyj tutaj, zanim zaczniesz czytać dalej:

Wiesz już jak modyfikować treść strony głównej. Teraz nauczę Cię, jak dodawać kolejne strony i linki pomiędzy nimi.

Przypominam, że gotowy kod projektu możesz znaleźć na naszym GitHub: https://github.com/fringers/nextjs-demo

Pamiętaj, że kod jest rezultatem całej serii artykułów, więc może wybiegać poza zakres opisany poniżej.

Nowa strona

Jak zapewne pamiętasz, aby zmodyfikować zawartość strony głównej, wystarczy zmienić zawartość pliku index.tsx. Teraz chcemy jednak dodać kolejną stronę. Aby to zrobić, przejdź do katalogu /pages i dodaj w nim nowy plik about.tsx. Uzupełnij go następującym kodem:

export default function About() {
    return (
        <div>
            O projekcie
        </div>
    )
}

Jak widzisz, kod bardzo przypomina ten z index.jsx. Pominąłem tutaj część z <Head> ale również możesz ją dodać. Co więcej, jeśli jakaś część z <Head> jest identyczna na wielu podstronach, możesz umieścić ją w pliku _app.tsx, zamiast w każdej z podstron osobno. Załóżmy, że w naszym projekcie każda podstrona ma mieć identyczny tytuł i favicon. Aby tak było, usuń cały fragment <Head>...</Head> z index.jsx i dodaj go do _app.tsx. Nowy _app.tsx powinien teraz wyglądać tak:

import Head from "next/head";
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
      <>
        <Head>
          <title>Fringers Next.js Demo</title>
          <link rel="icon" href="/favicon.ico"/>
        </Head>

        <Component {...pageProps} />
      </>
  )
}

export default MyApp

Uruchom projekt i przejdź pod adres http://localhost:3000/about. Powinieneś zobaczyć tam napis „O projekcie”, czyli zawartość pliku about.tsx. Tak właśnie działa Next.js. Każdy plik dodany w /pages/ (poza specyficznymi plikami, jak _app.tsx) staje się automatycznie nową stroną. Adres takiej strony jest generowany automatycznie na podstawie nazwy katalogu oraz nazwy pliku. Jeśli utworzysz plik w /pages/blog/posts/information.tsx, będzie on dostępny pod adresem http://localhost:3000/blog/posts/information.tsx.

Brakuje nam jeszcze linków, pozwalających na przechodzenie między poszczególnymi stronami. W Next.js najlepiej używać komponentu <Link>, dzięki któremu przejścia między stronami są szybsze, ponieważ Next.js wie, że musi przeładować tylko część strony która uległa zmianie, zamiast wykonania pełnego przekierowania w przeglądarce.

Dodamy teraz linki na obu naszych stronach. Najpierw dodaj w obu plikach (index.tsx oraz about.tsx) import komponentu Link:

import Link from 'next/link'

Następnie dodaj jeden link w kodzie index.tsx:

<Link href="/about"><a>O projekcie</a></Link>

a drugi w kodzie about.tsx:

<Link href="/"><a>Strona główna</a></Link>

Teraz wiesz już jak dodawać nowe podstrony, zmieniać ich treść i nawigować między nimi. Możesz spróbować dodać kolejne strony, umieścić je w zagnieżdżonych folderach, oraz zajrzeć do oficjalnej dokumentacji, aby dowiedzieć się więcej o komponencie <Link> https://nextjs.org/docs/api-reference/next/link.

Baza danych

Dla nas to jednak za mało. Powiedzmy, że chcesz stworzyć prostą galerię zdjęć, składającą się ze strony głównej zawierającej listę wszystkich Twoich zdjęć oraz osobnej podstrony dla każdego zdjęcia, zwierającej obrazek i jego opis. Dodawanie kolejnego pliku *.tsx za każdym razem, gdy chcesz dodać nowe zdjęcie, może nie być najlepszym rozwiązaniem, dlatego pokażę Ci teraz, jak tworzyć dynamiczne podstrony.

Zacznijmy od przygotowania zdjęć do naszej galerii. Ja dla przykładu pobrałem kilka zdjęć kotów ze strony https://www.pexels.com/ i umieściłem je w katalogu /public/cats/. Ty możesz użyć swoich zdjęć, pobrać inne zdjęcia z internetu, lub wejść na nasz GitHub i pobrać dokładnie te zdjęcia, których ja użyłem.

Następnie utwórz katalog /content/ i dodaj w nim plik gallery.json. Będzie to nasza „baza danych”, zawierająca zdjęcia i ich opisy. Oczywiście nie musisz używać plików JSON. Możesz użyć innego rodzaju plików albo nawet łączyć się do prawdziwej bazy danych. Jednak do naszego projektu taki plik będzie wystarczający i łatwo będzie pokazać na nim działanie Next.js.

Teraz musimy uzupełnić naszą bazę danych. Zróbmy to w taki sposób:

{
  "photo-1": {
    "src": "/cats/cat-1.jpg",
    "description": "To jest ładny kot"
  },
  "photo-2": {
    "src": "/cats/cat-2.jpg",
    "description": "Kot w pozycji siedzącej"
  }
}

Klucze photo-1, photo-2, itd. będą naszymi unikalnymi identyfikatorami. Każdy obiekt zawiera ścieżkę do pliku ze zdjęciem (zauważ, że w ścieżce do zdjęcia pomijamy katalog public!) oraz opis zdjęcia. Dodaj do pliku więcej wpisów, tak abyś miał co najmniej 5 zdjęć. Jeśli nie chcesz robić tego ręcznie, możesz skopiować gotowy plik z GitHub.

Teraz pora na wczytanie naszej bazy danych. Utwórzmy nowy katalog /lib/, a w nim plik gallery.ts, w którym umieścimy całą logikę odczytu i przetwarzania bazy. Będziemy potrzebować w nim trzech funkcji:

  • pobierania wszystkich zdjęć w galerii
  • pobierania identyfikatorów wszystkich zdjęć w galerii
  • pobierania jednego zdjęcia po id

Ponieważ operujemy na pliku JSON, możesz to zrobić w taki łatwy sposób:

import database from '../content/gallery.json'

export interface Image {
    id: string
    src: string
    description: string
}

export function getAllImages(): Image[] {
    return Object.entries(database).map(image => {
        return {
            id: image[0],
            src: image[1].src,
            description: image[1].description
        }
    })
}

export function getAllIds(): string[] {
    return Object.keys(database)
}

export function getImageById(id: string): Image {
    return {
        id,
        src: database[id].src,
        description: database[id].description
    }
}

Najpierw wykonywany jest import pliku JSON, a jego treść jest dostępna w obiekcie database. Dalej definiujemy interfejs Image, który mówi nam, jakie informacje o zdjęciach będziemy zwracać. Następnie eksportujemy 3 funkcje:

  1. getAllImages - mapuje wszystkie klucze z galerii, na obiekty Image i zwraca je jako tablicę.
  2. getAllIds - zwraca listę kluczy (identyfikator zdjęć) z galerii.
  3. getImageById - zwraca pojedynczy obiekt Image na podstawie podanego klucza.

Na potrzeby artykuł pominęliśmy tutaj obsługę błędów, takich jak nieprawidłowa struktura JSON czy próba pobrania obiektu poprzez niepoprawny identyfikator, jednak warto o nich pamiętać w trakcie implementacji docelowych rozwiązań.

Następnie dodajmy wyświetlanie wszystkich zdjęć na stronie głównej. Otwórz plik index.tsx i dodaj w nim następującą funkcję:

export async function getStaticProps({ params }) {
    const images = getAllImages(params.id)
    return {
        props: {
            images
        }
    }
}

Pamiętaj, aby zaimportować getAllImages z naszego pliku gallery.ts:

import {getAllImages} from "../lib/gallery";

Funkcja getStaticProps uruchamia się w czasie budowania strony i przekazuje parametry do podstrony, w ramach której ją dodaliśmy (w naszym przypadku jest to strona główna). Teraz możesz dodać przyjmowanie parametru images do Home w następujący sposób:

export default function Home({ images })

Mając obrazki, wyświetl na stronie. Możesz to zrobić na przykład w taki sposób:

<div>
    {images.map((image: Image) => {
        return (
            <div key={image.id}>
                <img src={image.src} alt={image.description} className="thumbnail"/>
            </div>
        )
    })}
</div>

Do pliku global.css dodałem jeszcze prostą klasę (której użycie widać w kodzie wyżej), żeby zdjęcia nie zajmowały całej strony:

.thumbnail {
  width: 400px;
}

Twój plik index.tsx może wyglądać teraz tak:

import Link from 'next/link'
import {getAllImages, Image} from "../lib/gallery";

export default function Home({ images }) {
    return (
        <div>
            <h1>Witamy na nasze stronie!</h1>

            <p>
                <Link href="/about"><a>O projekcie</a></Link>
            </p>

            <div>
                {images.map((image: Image) => {
                    return (
                        <div key={image.id}>
                            <img
                                src={image.src}
                                alt={image.description}
                                className="thumbnail"
                            />
                        </div>
                    )
                })}
            </div>
        </div>
    )
}

export async function getStaticProps() {
    const images = getAllImages()
    return {
        props: {
            images
        }
    }
}

Po uruchomieniu powinieneś zobaczyć wszystkie zdjęcia podane w pliku gallery.json: Strona główna ze zdjęciami

Jeśli wszystko działa poprawnie, możemy przejść do dynamicznego generowania podstron, na których będziemy wyświetlać szczegóły naszych zdjęć. Chcemy, aby każde zdjęcie miało własną podstronę, zawierającą opis oraz samo zdjęcie.

Dynamiczne generowanie stron

Stwórz nowy plik pages/images/[id].tsx. Kwadratowe nawiasy nie są błędem. Informują one Next.js, że na podstawie tego pliku będziemy chcieli stworzyć wiele dynamicznych podstron. Uzupełnij plik następującym kodem:

import {getAllIds, getImageById} from "../../lib/gallery";

export default function Image({image}) {
    return (
        <div>image: {image.id}</div>
    )
}

export async function getStaticPaths() {
    const ids = getAllIds()
    return {
        paths: ids.map(id => {
            return {
                params: {
                    id
                }
            }
        }),
        fallback: false
    }
}

export async function getStaticProps({ params }) {
    const image = getImageById(params.id)
    return {
        props: {
            image
        }
    }
}

Mamy tutaj trzy funkcje. Środkowa z nich, getStaticPaths, odpowiada za generowanie poszczególnych podstron. Pobieramy w niej listę identyfikatorów wszystkich naszych zdjęć i przekazujemy je w parametrze paths. Dzięki temu Next.js wie, jakie podstrony ma wygenerować.

Trzecia funkcja, getStaticProps, przyjmuje w parametrze identyfikator danej strony, który w naszym przypadku jest identyfikatorem zdjęcia. Następnie pobieramy w niej zdjęcie z naszej bazy i zwracamy je.

Natomiast pierwsza z funkcji, Image, jest standardowym komponentem strony. W parametrze przyjmuje dane zdjęcia, które zwróciliśmy z getStaticProps.

Jeśli teraz uruchomisz aplikację i przejdziesz na stronę http://localhost:3000/images/photo-2, gdzie photo-2 to dowolny z identyfikatorów z Twojej bazy, powinieneś w treści strony zobaczyć tekst image: photo-2, bo właśnie to wypisaliśmy w komponencie Image w linii <div>image: {image.id}</div>.

Teraz pozostaje już tylko uzupełnić Image o wyświetlanie opisu zdjęcia oraz samego obrazka. Możemy to zrobić na przykład tak:

export default function Image({image}) {
    return (
        <>
            <p>
                Opis zdjęcia: {image.description}
            </p>

            <p>
                <img src={image.src} alt="" style={{maxWidth: '80%'}}/>
            </p>
        </>
    )
}

Przechodząc pod adres http://localhost:3000/images/photo-2, powinieneś zobaczyć opis oraz zdjęcie: Podstrona ze zdjęciem

Brakuje nam jeszcze jednego istotnego elementu. Zdjęcia wyświetlane na stronie głównej powinny być linkami prowadzącymi do poszczególnych podstron. Możesz to zrobić w prosty sposób, poprzez dodanie w pliku index.tsx komponentu <Link> z odpowiednim parametrem. Wstaw go w miejsce, w którym do tej pory było jedynie wyświetlane zdjęcie:

<div key={image.id}>
    <Link href={`/images/${image.id}`}>
        <a>
            <img
                src={image.src}
                alt={image.description}
                className="thumbnail"
            />
        </a>
    </Link>
</div>

Jak widzisz, wartość parametru href jest połączeniem nazwy folderu znajdującego się w /pages oraz identyfikatora danego zdjęcia. Dzięki temu każde ze zdjęć prowadzi do swojej podstrony.

Nasza strona jest już gotowa. Może nie jest bardzo zaawansowana, ale możesz ją w łatwy sposób rozbudowywać i dostosowywać do swoich potrzeb.

Build oraz Export

Wiesz już jak edytować stronę, dodawać kolejne podstrony i przechodzić między nimi. Wiesz również, jak uruchomić stronę lokalnie, na swoim komputerze. Teraz przejdziemy do budowania, które będzie nam potrzebne, aby opublikować stronę.

Jeśli zajrzysz do pliku package.json, oprócz skryptu dev, którego używałeś do tej pory do uruchamiania strony, zobaczysz tam również skrypt build. Spróbuj go teraz uruchomić:

npm run build

Po wykonaniu skryptu wynik budowania trafia do katalogu /.next. Jeśli przejdziesz do folderu ./next/server/pages/images, zobaczysz, że dla każdego ze zdjęć zdefiniowanych w naszym pliku gallery.json zostały wygenerowane dwa pliki - *.html oraz *.json. Pierwszy z nich jest używany, jeśli użytkownik wchodzi bezpośrednio na daną stronę. Zawiera cały HTML, który musi zostać zwrócony do przeglądarki, tak aby pobierać jak najmniej zasobów dodatkowymi strzałami. Drugi jest używany, jeśli użytkownik był już na innej podstronie i klika w link. Nie musimy pobierać już całego HTML, ponieważ użytkownik ma w swojej przeglądarce większość potrzebnych elementów. Dlatego plik *.json zawiera jedynie informacje o parametrach, które różnią się między danymi podstronami.

Nasza strona jest bardzo prosta, więc nie zobaczymy na niej dużej różnicy w wydajności pomiędzy pobraniem całego *.html, a mniejszego *.json, natomiast ma to o wiele większe znaczenie na bardziej złożonych projektach.

Jak już może zdążyłeś zauważyć, przeglądaliśmy pliki znajdujące się w katalogu server. Nazwa słusznie sugeruje, że zawartość katalogu ./next wygenerowanego przez skrypt build jest dostosowana do uruchamiania na serwerze, jako aplikacja Node.js wykonująca Server Side Rendering. To jednak nie jest cel, który chcieliśmy osiągnąć. Chcemy mieć statyczną stronę, którą będziemy mogli umieścić na darmowym hostingu, bez używania Node.js. Aby to zrobić, dodajmy nowy skrypt do pliku package.json:

"export": "next export"

Wszystkie skrypty powinny wyglądać teraz tak:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "export": "next export"
},

Uruchom nowy skrypt:

npm run export

Jeśli wykonał się poprawnie, powinieneś zobaczyć kolejny nowy folder o nazwie out. Jego zawartość to połączenie publicznych plików (z katalogu public), statycznych stron HTML wygenerowanych na podstawie plików z /pages, oraz katalog _next zawierający różnego rodzaju skrypty JS i inne pliki, potrzebne do poprawnego funkcjonowania strony. W folderze /out/images powinieneś widzieć wygenerowane podstrony, dla każdego z Twoich zdjęć zdefiniowanych w pliku gallery.json, natomiast folder /out/cats zawiera wszystkie obrazki, które wcześniej zapisaliśmy w /public/cats.

Podsumowanie

Zawartość /out jest już gotową stroną, którą możesz umieścić na dowolnym hostingu. Nie musisz mieć Node.js na serwerze. W dalszej części podpowiem Ci, jak można wybrać taki hosting, jak go skonfigurować oraz jak zautomatyzować umieszczanie na nim nowych wersji strony, tak aby publikacja nowych treści była dla szybka i wygodna.

Jeśli masz jakieś pytania, sugestie, lub po prostu chciałbyś z nami porozmawiać, przejdź do pozycji „Kontakt” w menu, gdzie znajdziesz wszystkie potrzebne informacje.

Linki

Na koniec kilka przydatnych linków: