NextJS

Next.js - założenia

Framework React produkt firmy Zeit (obecnie Vercel). Wszechstronne narzędzie do budowania dynamicznych stron WWW.

Na reddicie ukazał się post pod nieco prowokacyjnym tytułem: "Does NextJS will be soon obsolete?". Dokładnej odpowiedzi wyjaśniającej podstawowe założenia i działanie udzielił jeden z głównych deweloperów Next.js.

  • *Kompilator- komponenty React są kompilowane przy użyciu webpacka i Babel, dzięki temu odbywa się to szybko i zachowane są globalne zależności
  • *SSR- On-demand rendering (traditional SSR) każde żadanie powoduje wyrenderowanie unikalnej strony, wymaga to działającego serwera Node.js.
  • *Eksport do dokumentów statycznych- (static exporting) wszystkie strony mogą być wyświetlone jako zwykły html.
  • *Automatyczne rozdzielenie kodu- (automatic code splitting) cała struktura strony jest podzielona na niewpływające na siebie elementy, które sa wyświetlane dynamicznie niezależnie od ich liczby. Jeżeli z jakimś elementem jest cos nie tak nie wpływa to na resztę.
  • *Wymiana modułów- (hot module replacement) automatyczne przeładowanie modułów bez konieczności odświeżania przeglądarki.
  • *Routing / prefetching- routing wewnątrz katalogu może przebiegać automatycznie, w stylu SPA, Możliwy jest także prefetching.
  • *Dynamiczne wyświetlenie dokumentów- (dynamic loading) jak w React.lazy ale bardziej optymalizowane dla SSR.
  • CSS w każdy dostępny sposób.
  • Manipulacja nagłówkiem strony.
  • Asynchroniczne pobieranie danych przed ich wyświetleniem (data fetching).
  • Zarządzenie wyświetlaniem błedów.
  • Serverless
  • Serverful - możliwe jest także bardziej tradycyjne podejście wszystkie routingi są zarządzane przez jeden serwer.
  • Optymalizacja dla produkcji.
  • *Optymizacja podczas dewelopmentu- - kompilowanie na żądanie (on-demand); uruchomienie serwera deweloperskiego nie powoduje kompilacji wszystkich składników strony, np podczas każdego zapisu, kompilowane są dopiero kiedy przeglądarka je uruchamia, to znacznie przyspiesza działanie procesu.
  • Zones / Microfrontends - można wprowadzać stopniowo, pojedynczymi elementami
  • Stabilność zapewniona jest podczas rozwoju przez wszechstronne testowanie .

Użytkownicy Next.js to m.in. Hulu, Marvel, Docker i npm.

Instalacja

Poniższe polecenie w katalogu bieżącym utworzy katalog aplikacji o nazwie projektu i w nim instaluje react, react-dom i next:

 npx create-next-app
 npx: zainstalowano 1 w 3.446s
 ✔ What is your project named? … // tu podajemy nazwę projektu
 Creating a new Next.js app in // tu pojawia się ścieżka dostępu

Potem widzimy następujące komunikaty, które są instrukcją co zrobić dalej:

 Installing react, react-dom, and next using npm...


 > core-js@2.6.10 postinstall /sciezka/dostepu/Nextjs/next-app/node_modules/core-js
 > node postinstall || echo "ignore"
 
 + react@16.11.0
 + react-dom@16.11.0
 + next@9.1.2
 added 757 packages from 356 contributors and audited 10252 packages in 45.785s
 found 0 vulnerabilities
 
 
 Success! Created next-app-01 at /sciezka/dostepu/Nextjs/next-app
 Inside that directory, you can run several commands:
 
   npm run dev
     Starts the development server.
 
   npm run build
     Builds the app for production.
 
   npm start
     Runs the built app in production mode.
 
 We suggest that you begin by typing:
 
   cd next-app
   npm run dev
 ✔ What is your project named? … next-app-js3
 ✔ Pick a template › Default starter app
 Creating a new Next.js app in /media/tadek/Nextjs/next-app-js3.
 
 Installing react, react-dom, and next using npm...
 
 + react-dom@16.13.1
 + react@16.13.1
 + next@9.3.5
 added 782 packages from 307 contributors and audited 8527 packages in 57.389s
 
 29 packages are looking for funding
 run \`npm fund\` for details

found 0 vulnerabilities


 Initialized a git repository.

 Success! Created next-app-js3 at /media/tadek/Nextjs/next-app-js3
 Inside that directory, you can run several commands:

   npm run dev
     Starts the development server.

   npm run build
     Builds the app for production.

   npm start
     Runs the built app in production mode.

 We suggest that you begin by typing:

   cd next-app-js3
   npm run dev

Całość to ok 50 MB. Od razu będziemy mieli gita, i .gitignore z dodanymi node_modules. W zależnościach w package.json są tylko: react, react-dom i next.

Budowanie strony

Potem

 $ cd katalog_aplikacji
 $ npm run dev
 
 > next-app-js3@0.1.0 dev /scieżka_dostępu/katalog_aplikacji
 > next dev
 
 [ wait ]  starting the development server ...
 [ info ]  waiting on http://localhost:3000 ...
 [ ready ] compiled successfully - ready on http://localhost:3000
 [ wait ]  compiling ...
 [ ready ] compiled successfully - ready on http://localhost:3000
 [ event ] build page: /
 [ wait ]  compiling ...
 [ ready ] compiled successfully - ready on http://localhost:3000

I to wszystko, strona/aplikacja jest skompilowana i gotowa do użycia. Polecenie cały czas działa w tle, kompilując aplikacje na bieżąco po każdej dokonanej i zapisanej zmianie. Kiedy wejdziemy na wskazany adres zobaczymy wygenerowany domyślną stronę przygotowana przez Zeit: jest to /pages/index.js.

Najprostszy dokument wygląda tak:

 function Home() {
  return <div>Welcome to Next.js!</div>
}

export default Home;
}

export default Home;

Jak widać jest to zwykła funkcja, która zwraca JSX i na końcu jej wynik jest eksportowany.

Istotą tworzenia strony jest zagnieżdżanie komponentów dostępnych globalnie w obrębie aplikacji dzięki importom i eksportom. Wszystkie znajdują się w katalogu /components (jego nazwa jest arbitralna, po prostu musi być używana konsekwentnie). W taki sposób można stworzyć np. komponent menu używający elementu link, tutaj przykład menu zawierającego nielinkujące elementy i stylowanie:

import Link from "next/link";

const links = [  
  { label: "Tytuł menu", className: "title" },
  { href: "/page01", label: "Strona 01" },
  { href: "/page02", label: "Strona 02" },  
].map(link => {
  link.key = \`nav-link-$\{link.href\}-$\{link.label\}\`;
  return link;
});

const Navbar = () => (
  <nav className="navigation_one">
    <ul>
      {links.map(({ key, href, label, className }) => (
        <li key={key} className={className}>
          <Link href={href}>
            <a>{label}</a>
          </Link>
        </li>
      ))}
    </ul>
  </navl>
);

export default Navbar;

Potem taki komponent nawigacyjny można umieścić w komponencie Header, a ten z kolei w Layout:

 import Header from "./header";
 import Sidebar from "./sidebar";
 import Footer from "./footer";
 import "../style.css";
 
 const Layout = props => (
   <div className="container">
     <Header />
     <main>
       <Sidebar />
       <article className="article">{props.children}</article>
     </main>
     <Footer />
   </div>
 );
 
 export default Layout;

W prosty sposób można stworzyć szablon strony, nad którą ma się całkowitą kontrolę. Importy zawsze dotyczą wykorzystywanych właściwości React lub Next.js, komponent Link byl importowany kiedy użyto go w szablonie nawigacji, a style aplikują się do złożonego w całość szablonu layoutu strony. Wszystkie elementy używane globalnie trzymamy w /components, a pliki stron w /pages.

Kluczowe znaczenie ma tu obiekt props.children, czyli miejsce, w którym zostanie umieszczona unikalna treść podstrony. Mając taki schemat możemy utworzyć pliki stron, wg powyższych reguł, które w returnie mają tylko tagi <Layout> i treść:

<Layout>
 tu właściwa treść podstrony...
</Layout>

Ponieważ jest to React, kod w returnie (komponent klasowy) musi być zawarty w nawiasach i w jednym nadrzędnym tagu, np. React Fragment.

 return (
  <>
      [kod tu]
  </>
    )

Albo w funkcji strzałkowej (komponent funkcyjny) w jednym nadrzędnym elemencie HTML

 const Home = () => (
  <div>
[kod tu]
  </div>

CSS

W przypadku CSS mamy dostępne właściwie wszystkie istniejące możliwości. CSS możemy linkować statycznie z HTML-owego elementu szablonu layoutu (tag link).

Można też importować do modułu (import "sciezka/dostepu/styles.css";), ale wtedy trzeba doinstalować moduł next-css

 npm install --save @zeit/next-css
 npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/fsevents):
 npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
 
 + @zeit/next-css@1.0.1
 added 22 packages from 14 contributors and audited 10440 packages in 16.803s
 found 0 vulnerabilities

next.config.js - tutaj także załączony raw-loader dla plików .txt i .md.

 /- eslint-disable */
 const withCss = require("@zeit/next-css");
 
 module.exports = withCss({
   publicRuntimeConfig: {
     // ...
   },
   webpack: (config, { isServer }) => {
     config.module.rules.push({
       test: /\.(md|txt)$/,
       use: "raw-loader"
     });
 
     return config;
   }
 });

Można też umieścić CSS w samym pliku szablonu lub podstrony w elemencie JSX:

<style jsx>;{`
tutaj style ...
`}</style>;

App.js - /pages/_app.js

Inaczej niż w Express, Next.js nie ma głównego pliku z konfiguracją takiego jak App.js. A ściśle rzecz biorąc nie jest on udostępniany użytkownikowi, nie mamy do niego dostępu. Ale można go nadpisać własnym: /pages/_app.js (nazwa obligatoryjna). Może on wyglądać tak jak poniżej, tutaj przedstawiony jest _app.js, który jednocześnie jest komponentem Layout:

 import React from "react";
 import App from "next/app";
 
 import Header from "../components/header";
 import Footer from "../components/footer";
 
 import "../styles/style.css";
 
 class Layout extends App {
   render() {
     return (
       [tu JSX]
     )
 }}
 
 export default Layout;

Taki szablon layoutu ma przewagę nad domyślnym bo jest komponentem klasowym. Trzeba pamiętać, żeby go w plikach stron zaimportować. Używamy go wtedy zamiast dotychczasowego szablonu layout.

Link.active i scrollspy

Next.js niestety nie udostępnia w komponencie Link możliwości automatycznego przypisywania klasy .active dla aktywnego elementu menu. Najprostszym rozwiązaniem jest zastosowanie skryptu autorstwa Flavio Copesa - "Using the router to detect the active link in Next.js". Wystarczy skopiować go do katalogu komponentów pod nazwą Link.js (przykładowo) i zaimportować go w komponencie menu zamiast systemowego komponentu Link.

Stosowanie scrollspy jest w Next.js utrudnione dlatego, że mamy do czynienia z dynamicznie renderowaną stroną w Node.js, który nie posiada obiektów przeglądarki. Tutaj rozwiązaniem jest moduł react-scrollspy. Wystarczy go zainstalować, zaimportować. Komponent Scrollspy jest listą, można mu nadać własną klasę. Wymaga komponentu Link.

Pojawia się jednak problem jeżeli używamy scrollspy jako spisu treści podstrony. Trzeba wtedy umieścić ten komponent w każdym dokumencie, a nie w layoucie i w każdej instancji edytować, ręcznie wpisując i adresy i tytuły linków. Przynajmniej część tej pracy można zautomatyzować. Można przy tym zrezygnować z komponentu Link, trzeba będzie bowiem przepisać szablon komponentu scrollspy. Tytuły odnośników umieszczamy w tablicy, dodajemy funkcję, która pobiera elementy tablicy i tworzy inną tablicę, którą możemy mapować już wewnątrz komponentu na odnośniki. Wygląda to tak:

 // import komponentu
 import Scrollspy from "react-scrollspy";
 
 // tablica z tytułami rozdziałów dokumentu
 let items = [
   "Zażółć gęślą jaźń",
   'Ścichapęk "o2o27(oo0("',
   "Odnośnik 3"
   // ... itd
 ];
 
 // utworzenie pustej, docelowej tablicy
 let itemsList = [];
 
 // funkcja rozpisująca pierwotną tablice na docelową
 for (let i = 0; i < items.length; i++) {
  let item = { key: i, href: `#${items[i]}`, label: items[i] };
   itemsList.push(item);
 }
 
 // komponent z mapowaniem
 const ScrollSpy = () => (
   <Scrollspy items={items} currentClassName="current" className="scrollspy">
     {itemsList.map(item => (
       <li key={item.key}>
         <a href={item.href}>{item.label}</a>
       </li>
     ))}
   </Scrollspy>
 );
 
 // eksport i koniec pliku
 export default ScrollSpy;

Dla pełnej automatyzacji wystarczy pobrać tytuły odnośników JS przeglądarki. Zakładając, że treść dokumentu znajduje się w tagu article i każdy rozdział jest zatytułowany nagłówkiem H3, taka funkcja może wyglądać następująco:

 const items = [];

 const makeList = () => {
 const listOfElements = [...document.querySelectorAll("article>h3")];
 for (let i = 0; i < listOfElements.length; i++) {
    itemsList.push(listOfElements[i].id);
   }

Jak widać wymaga to konsekwentnego stosowania konwencji, w której adres hiperłącza jest identyczny z tytułem. Przesłanie jej do powyższego komponentu Node.js to jeszcze jest TODO.

Router

 import { useRouter } from "next/router";
 import Layout from "../../components/layout";
 
 export default function Post() {
   const router = useRouter();
 
   return (
     <Layout>
       <h1>{router.query.id}</h1>
       <p>This is the blog post content.</p>
     </Layout>
   );
 }

Fetch

 import Layout from "../components/layout";
import Link from "next/link";
import fetch from "isomorphic-unfetch";

const Index = props => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(show => (
        <li key={show.id}>
          <Link href="/p/[id]" as={\`/p/$\{show.id}\`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
);

Index.getInitialProps = async function() {
  const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
  const data = await res.json();

  // console.log(\`Show data fetched. Count: $\{data.length}\`);
  // console.log(data);

  return {
    shows: data.map(entry => entry.show)
  };
};

export default Index;
import Link from "next/link";
import Layout from "../components/layout";

const PostLink = props => (
  <li>
  {/- <Link href={`/post?title=${props.title}`}> */}
    <Link href="/p/[id]" as={`/p/${props.id}`}>
      <a>{props.id}</a>
      {/- <a>{props.title}</a> */}
    </Link>
  </li>
);

export default function Index() {
  return (
    <Layout>
      <h2>Hello Next.js</h2>
      <ul>
        <PostLink id="Post title blah01" />
        <PostLink id="This is the next post title" />
        <PostLink id="Not a last, not just a next. Just" />
      </ul>
    </Layout>
  );
}

Odnośniki

Tutoriale

Artykuły

GraphQL

Youtube

Narzędzia

Inne