Wpadki i wypadki #17

Dzisiaj kolejne Wpadki i wypadki, po raz kolejny o linkach, ale tym razem na „tradycyjnych” stronach WWW.

W 12. odcinku Wpadek i wypadków poruszałem temat płynnego przewijania do poszególnych sekcji na stronach typu one-page. Jednym z najważniejszych problemów, jakie tam wymieniłem, było to, że linki często są nie do końca działające. Bardzo podobny problem może wystąpić w przypadku witryn składających się z wielu podstron, a w których poszczególne podstrony są dynamicznie wczytywane bez przeładowywania całej strony. Wówczas nawigacja też często zawiera puste linki lub wręcz zrobiona jest na przyciskach:

<nav class="navigation">
	<ul class="navigation__menu menu">
		<li class="menu__item">
			<button id="about-me-link" class="menu__link">About me</button>
		</li>
		<li class="menu__item">
			<button id="contact-link" class="menu__link">Contact</button>
		</li>
	</ul>
</nav>

Po kliknięciu w przycisk ładowana jest treść danej podstrony (np. z backendu, który zwraca każdą podstronę w formie JSON-a, albo najzwyczajniej w świecie z pliku HTML). W naszym przypadku znajduje się ona w plikach .json w katalogu pages. Jest ona wczytywana przy pomocy funkcji loadPage():

async function loadPage( name ) {
	const pageInfoURL = `./pages/${ name }.json`;
	const pageInfoResponse = await fetch( pageInfoURL );
	const pageInfo = await pageInfoResponse.json();

	updatePage( pageInfo );
}

Z kolei funkcja updatePage() wyciąga dane z tego JSON-a i wstawia je w odpowiednie miejsca na stronie:

function updatePage( { title, content } ) {
	document.querySelector( 'title' ).textContent = title;
	document.querySelector( 'main' ).innerHTML = content;
}

Dodatkowo, funkcja loadPage() jest używana do wczytywania zawartości głównej strony w chwili wejścia na witrynę. Mówiąc inaczej, jest tutaj (dość prymitywnie) zastosowany wzorzec app shell (ang. powłoki aplikacji). I taka witryna jak najbardziej działa. Niemniej trapiące ją problemy są niemal takie same, jak wspomnianej wcześniej strony typu one-page:

  • URL-e są częścią UI. Z racji tego, że podstrony nie mają swoich własnych adresów, nie można podlinkować do określonej podstrony. I to nie jest problem tylko dla użytkowników podsyłających linki między sobą. Wystarczy zadać sobie proste pytanie: czy wolelibyśmy klientowi napisać w mailu, żeby wszedł na adres https://example.com/oferta i zapoznał się z naszą ofertą, czy raczej musieć się rozpisywać, że musi wejść na naszą stronę główną, kliknąć w menu „odnośnik” Oferta i dopiero wówczas móc się z nią zapoznać?

  • Brak URL-a to też zdecydowanie niższa użyteczność w innych obszarach. Najprostszy przykład: nie da się otworzyć podstrony w nowej karcie. W przypadku przycisków w menu zamiast linków nie da się tego zrobić w ogóle, w przypadku pustych linków – otworzymy znowu stronę główną. Nie da się też skopiować linków do poszczególnych podstron.

  • Wzorzec powłoki aplikacji i jego pochodne działają bardzo fajnie w przypadku aplikacji internetowych. Ale w przypadku stron oznacza to, że w pełni uzależniamy jej działanie od działania JS-a. A ten może nie zadziałać. Pytanie, czy jest nam on całkowicie niezbędny? W przypadku aplikacji odpowiedź prawdopodobnie brzmi tak. W przypadku stron – już niekoniecznie. Na stronie firmowej chcemy przedstawić użytkownikowi informacje na temat naszej firmy – i to jest główny jej cel. Dynamiczne przejścia między podstronami są tylko miłym dodatkiem.

Według mnie strony informacyjne nie muszą (czy wręcz nie powinny) być uzależnione od JS-a. I istnieją techniki, które pozwalają przygotować stronę w taki sposób, by działała dobrze zarówno z działającym, jak i niedziałającym JS-em, oraz miała całkowicie poprawne linki. Jedna z nich to Progressive Enhancement (ang. Stopniowe Ulepszanie). W największym skrócie: podstawowa funkcjonalność powinna być dostarczona przy pomocy najprostszego możliwego rozwiązania, a na tym fundamencie powinny być dodawane kolejne funkcje. Podstrony wraz z linkami pomiędzy nimi można zrobić w czystym HTML-u. I mając witrynę w czystym HTML-u, można do tego dodać dynamiczne wczytywanie treści.

Zmieńmy zatem nawigację tak, by wskazywała poprawne podstrony:

<nav class="navigation">
	<ul class="navigation__menu menu">
		<li class="menu__item">
			<a href="./" id="homepage-link" class="menu__link">Homepage</a>
		</li>
		<li class="menu__item">
			<a href="./about-me.html" id="about-me-link" class="menu__link">About me</a>
		</li>
		<li class="menu__item">
			<a href="./contact.html" id="contact-link" class="menu__link">Contact</a>
		</li>
	</ul>
</nav>

Gdy już będziemy mieli poprawne linki, można dodać kod JS, który będzie blokował domyślne zachowanie linków i na ich miejsce podstawiał nasze dynamiczne podmienianie treści:

document.querySelector( '.navigation__menu' ).addEventListener( 'click', ( evt ) => {
	const link = evt.target.closest( '.menu__link' );

	if ( !link ) { // 1
		return;
	}

	evt.preventDefault(); // 2

	loadPage( link.href ); // 3
} );

Stosuję tutaj event delegation i sprawdzam, czy został kliknięty link (1). Jeśli tak, blokuję jego domyślną akcję (2), a następnie wywołuję funkcję loadPage() z adresem z linku (3). Sama funkcja loadPage() nie zmieniła się aż tak:

async function loadPage( url ) {
	const response = await fetch( url );
	const html = await response.text();
	const htmlParser = new DOMParser();
	const dom = htmlParser.parseFromString( html, 'text/html' );

	updatePage( url, dom );
}

Główna różnica polega na tym, że zamiast JSON-a pobieram bezpośrednio HTML z określonej podstrony i parsuje go do DOM-u przy pomocy natywnego API DOMParser. Dzięki temu funkcja updatePage() będzie mogła bez problemu wyciągnać sobie interesujące ją dane z wczytanej podstrony. A skoro już mówimy o funkcji updatePage():

function updatePage( url, dom ) {
	const title = dom.querySelector( 'title' ).textContent;
	const content = dom.querySelector( 'main' ).innerHTML;

	document.querySelector( 'title' ).textContent = title;
	document.querySelector( 'main' ).innerHTML = content;

	history.pushState( {}, '', url );
}

Najciekawszą częścią jest tutaj ostatnia linijka, która przy pomocy History API dodaje nowy wpis do historii przeglądarki, przy okazji zmieniając adres w pasku adresu na ten z klikniętego linku. To wymusza też dodanie obsługi zdarzenia popstate, by przyciski Wstecz i Dalej w przeglądarce się nie zepsuły:

window.addEventListener( 'popstate', () => {
	loadPage( location.href );
} );

I to tyle, całość działa. Dodanie tego kodu na każdą podstronę pozwoli na włączenie dynamicznego wczytywania podstron, a równocześnie sprawi, że wszystkie linki będą dalej działały poprawnie. Jest to też dość prosty sposób, by taki bajer dodać do już istniejących tradycyjnych stron WWW.

Oczywiście powyższe rozwiązanie jest bardzo mocno naiwne i sprawdzi się w prostych projektach. Istnieją gotowe biblioteki, które tego typu rzeczy zrobią za nas, jak np. Turbo czy jquery-pjax. Także w przypadku projektów na frameworkach są kompleksowe rozwiązania tego typu problemów, dostarczające w pakiecie znacznie więcej (jak choćby sensowny SSR), np. Next.js. A nawet w przypadku stron, które nie korzystają z żadnego frameworka, raczej nie tworzy się już plików HTML z palca, a korzysta z generatorów. Niemniej przykład zaprezentowany w tym artykule jest celowo jak najbardziej uproszczony, by pokazać w jaki sposób można wykorzystać niektóre zasady Stopniowego Ulepszania w celu poprawy użyteczności prostej strony WWW.

No i warto zaznaczyć, że zastosowanie wzorca powłoki aplikacji pokazane w tym artykule faktycznie jest mocno prymitywne. Obecnie bardzo często wykorzystuje się go razem z Service Workerami, dzięki czemu zwykle trafiają do niego dane z cache’u. Frameworki też mogą wykorzystywać ten wzorzec w sposób zdecydowanie bardziej wyszukany niż to, co pokazałem tutaj.

Niemniej mam nadzieję, że ten krótki artykuł zainspiruje kogoś do bliższego zapoznania się z metodyką Stopniowego Ulepszania, bo często małymi nakładami pracy pozwala ulepszyć „tradycyjne” rozwiązania webdeveloperskie. Może też poprawić dostępność naszej strony. W końcu – może też poszerzyć nieco perspektywę patrzenia na technologie sieciowe.

Jeden komentarz do “Wpadki i wypadki #17”

  1. Bardzo dziękuję za ten artykuł. Cenna wiedza i koncepty zebrane w jednym miejscu!

    Jak zwykle kawał dobrej i ciężkiej roboty Comandeer odwałił na bożonarodzeniowy prezent!

    Potestowałem Twoje rozwiązanie https://www.webkrytyk.pl/repozytorium/wpadki-i-wypadki/17/final/ i powiem tylko tyle że pokusiłbym się o jego lekkie ulepszenie ponieważ może przydać się ono niejednej osobie.

    Nie każdy chce korzystać z bibliotek, a Twoje rozwiązanie wcale nie musi być prymitywne, bo do prostych stronek jest mega dobre i „szyte na miarę”!

    Znalazelm takie rzeczy do ulepszenia UX:

    1) Wchodzę https://www.webkrytyk.pl/repozytorium/wpadki-i-wypadki/17/final/ – pierwszy link „Homepage” na dzień dobry nie jest podświetlony.

    2) Klikam „About me” ten już jest podświetlony, ale gdy dam wstecz, to powrót do „Homepage” co prawda następuje ale „About me” nadal jest podświetlone.

    3) Historia działa tylko na cofanie (w lewo) natomiast do przodu, pomimo tego że przed chwilą tam byłem (w prawo), już nie.

    Do podświetlania nawigacji można by uzyć czystego css z aria-current=”page” zamiast .menu__link:focus. Coś mniej więcej w tym stylu:

    .menu__link [aria-current=”page”] a {
    background-color: #ccc;
    }

    Pozdrawiam!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.