Z tego ćwiczenia dowiesz się, jak utworzyć w internecie środowisko podobne do Instagram Stories. Będziemy tworzyć komponent krok po kroku, zaczynając od HTML, potem CSS, a na końcu JavaScript.
Aby dowiedzieć się więcej o progresywnych ulepszeniach wprowadzonych podczas tworzenia tego komponentu, przeczytaj mój post na blogu Tworzenie komponentu Relacje.
Konfiguracja
- Kliknij Zremiksuj, aby edytować, aby umożliwić edytowanie projektu.
- Otwórz pokój
app/index.html.
HTML
Zawsze staram się używać semantycznych znaczników HTML.
Każdy znajomy może mieć dowolną liczbę historii, więc uznałem, że warto użyć elementu <section> dla każdego znajomego i elementu <article> dla każdej historii.
Zacznijmy jednak od początku. Najpierw potrzebujemy kontenera
na komponent relacji.
Dodaj element <div> do <body>:
<div class="stories">
</div>
Dodaj kilka elementów <section>, które będą reprezentować znajomych:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Dodaj elementy <article>, które będą reprezentować artykuły:
<div class="stories">
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
</section>
</div>
- Do prototypowania artykułów używamy usługi graficznej (
picsum.com). - Atrybut
stylew każdym elemencie<article>jest częścią techniki wczytywania obiektów zastępczych, o której dowiesz się więcej w następnej sekcji.
CSS
Nasze treści są gotowe do stylizacji. Zmieńmy te kości w coś, z czym ludzie będą chcieli wchodzić w interakcje. Dzisiaj skupimy się na urządzeniach mobilnych.
.stories
W przypadku kontenera <div class="stories"> chcemy, aby był on przewijany poziomo.
Możemy to osiągnąć przez:
- Ustawianie kontenera jako siatki
- Ustawianie każdego elementu podrzędnego tak, aby wypełniał ścieżkę wiersza
- ustawienie szerokości każdego elementu podrzędnego na szerokość widocznego obszaru urządzenia mobilnego,
Siatka będzie nadal umieszczać nowe kolumny o szerokości 100vw po prawej stronie poprzedniej, dopóki nie zostaną umieszczone wszystkie elementy HTML w znacznikach.
Dodaj ten kod CSS na dole pliku app/css/index.css:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Teraz, gdy mamy zawartość, która wykracza poza widoczny obszar, musimy określić, jak kontener ma ją obsługiwać. Dodaj wyróżnione wiersze kodu do zbioru reguł .stories:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior: contain;
touch-action: pan-x;
}
Chcemy, aby przewijanie odbywało się w poziomie, więc ustawimy overflow-x na auto. Gdy użytkownik przewija, chcemy, aby komponent delikatnie zatrzymywał się na następnej historii, więc użyjemy scroll-snap-type: x mandatory. Więcej informacji o tych właściwościach CSS znajdziesz w sekcjach CSS Scroll Snap Points i overscroll-behavior w moim poście na blogu.
Zarówno kontener nadrzędny, jak i elementy podrzędne muszą wyrazić zgodę na przewijanie z przyciąganiem, więc zajmijmy się tym teraz. Dodaj ten kod na dole pliku app/css/index.css:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Twoja aplikacja jeszcze nie działa, ale film poniżej pokazuje, co się dzieje, gdy scroll-snap-type jest włączone i wyłączone. Gdy ta opcja jest włączona, każde przewinięcie w poziomie powoduje przejście do następnej historii. Gdy zasada jest wyłączona, przeglądarka używa domyślnego sposobu przewijania.
To pozwoli Ci przewijać znajomych, ale nadal mamy do rozwiązania problem z relacjami.
.user
Utwórzmy w sekcji .user układ, który uporządkuje te elementy podrzędne. Aby rozwiązać ten problem, użyjemy przydatnej sztuczki ze stosami.
Zasadniczo tworzymy siatkę 1 x 1, w której wiersz i kolumna mają ten sam alias siatki [story], a każdy element siatki artykułu będzie próbował zająć tę przestrzeń, co spowoduje utworzenie stosu.
Dodaj zaznaczony kod do zbioru reguł .user:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Dodaj ten zestaw reguł na dole pliku app/css/index.css:
.story {
grid-area: story;
}
Teraz, bez pozycjonowania bezwzględnego, elementów pływających ani innych dyrektyw układu, które wyjmują element z przepływu, nadal jesteśmy w przepływie. Poza tym to prawie nie wymaga kodu, spójrzcie tylko! Więcej szczegółów znajdziesz w filmie i poście na blogu.
.story
Teraz musimy tylko nadać styl samemu elementowi relacji.
Wcześniej wspomnieliśmy, że atrybut style każdego elementu <article> jest częścią techniki wczytywania obiektów zastępczych:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Użyjemy właściwości background-image CSS, która pozwala określić więcej niż 1 obraz tła. Możemy je uporządkować tak, aby zdjęcie użytkownika było na górze i wyświetlało się automatycznie po zakończeniu wczytywania. Aby to umożliwić, umieścimy adres URL obrazu we właściwości niestandardowej (--bg) i użyjemy go w kodzie CSS, aby nałożyć go na obiekt zastępczy ładowania.
Najpierw zaktualizujmy zestaw reguł .story, aby po zakończeniu wczytywania zastąpić gradient obrazem tła. Dodaj zaznaczony kod do zbioru reguł .story:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Ustawienie właściwości background-size na cover sprawia, że w obszarze widoku nie ma pustego miejsca, ponieważ obraz wypełnia go w całości. Zdefiniowanie 2 obrazów tła pozwala nam zastosować sprytną sztuczkę CSS zwaną nagrobkiem wczytywania:
- Obraz tła 1 (
var(--bg)) to adres URL, który przekazaliśmy w kodzie HTML. - Obraz tła 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))) to gradient, który wyświetla się podczas wczytywania adresu URL.
CSS automatycznie zastąpi gradient obrazem, gdy tylko obraz zostanie pobrany.
Następnie dodamy kod CSS, aby usunąć niektóre zachowania i przyspieszyć działanie przeglądarki.
Dodaj zaznaczony kod do zbioru reguł .story:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
}
user-select: noneuniemożliwia użytkownikom przypadkowe zaznaczenie tekstutouch-action: manipulationinformuje przeglądarkę, że te interakcje powinny być traktowane jako zdarzenia dotknięcia, dzięki czemu przeglądarka nie musi decydować, czy klikasz adres URL, czy nie.
Na koniec dodajmy trochę kodu CSS, aby animować przejścia między artykułami. Dodaj wyróżniony kod do zestawu reguł .story:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);
&.seen {
opacity: 0;
pointer-events: none;
}
}
Klasa .seen zostanie dodana do scenariusza, który wymaga wyjścia.
Niestandardową funkcję wygładzania (cubic-bezier(0.4, 0.0, 1,1)) pobrałem z przewodnika po wygładzaniu w Material Design (przewiń do sekcji Wygładzanie przyspieszone).
Jeśli masz wprawne oko, prawdopodobnie zauważysz deklarację pointer-events: none i będziesz się zastanawiać, o co chodzi. To chyba jedyna wada tego rozwiązania. Jest to konieczne, ponieważ element .seen.story będzie na górze i będzie odbierać kliknięcia, mimo że jest niewidoczny. Ustawiając właściwość pointer-events na none, zmieniamy szklaną warstwę w okno i nie przechwytujemy już interakcji użytkownika. To nie jest zła wymiana, a zarządzanie tym w naszym CSS nie jest zbyt trudne. Nie żonglujemy z-index. Nadal mam dobre przeczucia.
JavaScript
Interakcje z komponentem Relacje są dla użytkownika dość proste: kliknięcie po prawej stronie powoduje przejście do przodu, a kliknięcie po lewej stronie – powrót. To, co dla użytkowników jest proste, dla deweloperów jest zwykle trudne. My zajmiemy się większością z nich.
Konfiguracja
Na początek obliczmy i zapiszmy jak najwięcej informacji.
Dodaj do pliku app/js/index.js ten kod:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
Nasz pierwszy wiersz kodu JavaScript pobiera i przechowuje odwołanie do głównego elementu HTML. Następny wiersz oblicza, gdzie znajduje się środek naszego elementu, dzięki czemu możemy zdecydować, czy kliknięcie ma spowodować przejście do przodu czy do tyłu.
Stan
Następnie tworzymy mały obiekt ze stanem istotnym dla naszej logiki. W tym przypadku interesuje nas tylko bieżąca historia. W znacznikach HTML możemy uzyskać do niego dostęp, pobierając pierwszego znajomego i jego najnowszą relację. Dodaj wyróżniony kod do pliku app/js/index.js:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Detektory
Mamy już wystarczająco dużo logiki, aby zacząć nasłuchiwać zdarzeń użytkownika i je kierować.
Mysz
Zacznijmy od nasłuchiwania zdarzenia 'click' w kontenerze relacji.
Dodaj do pliku app/js/index.js zaznaczony kod:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
Jeśli kliknięcie nie dotyczy elementu <article>, nic nie robimy.
Jeśli jest to artykuł, pobieramy poziomą pozycję myszy lub palca za pomocą clientX. Nie zaimplementowaliśmy jeszcze metody navigateStories, ale argument, który przyjmuje, określa kierunek, w którym musimy się udać. Jeśli pozycja użytkownika jest większa niż mediana, wiemy, że musimy przejść do next, w przeciwnym razie do prev (poprzedniej).
Klawiatura
Teraz posłuchajmy naciśnięć klawiszy. Jeśli naciśniesz strzałkę w dół, przejdziesz do next. Jeśli jest to strzałka w górę, przechodzimy do prev.
Dodaj do pliku app/js/index.js zaznaczony kod:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
document.addEventListener('keydown', ({key}) => {
if (key !== 'ArrowDown' || key !== 'ArrowUp')
navigateStories(
key === 'ArrowDown'
? 'next'
: 'prev')
})
Nawigacja po relacjach
Czas zająć się unikalną logiką biznesową relacji i wrażeniami użytkowników, z których słyną. Może to wyglądać na skomplikowane, ale jeśli przeanalizujesz to linijka po linijce, okaże się, że jest całkiem zrozumiałe.
Na początku przechowujemy kilka selektorów, które pomagają nam zdecydować, czy przewinąć do znajomego, czy pokazać/ukryć historię. Ponieważ pracujemy w HTML, będziemy wysyłać do niego zapytania o obecność znajomych (użytkowników) lub historii (historia).
Te zmienne pomogą nam odpowiedzieć na pytania takie jak: „Czy w przypadku relacji X »dalej« oznacza przejście do innej relacji tego samego znajomego czy do relacji innego znajomego?”. Udało mi się to dzięki wykorzystaniu utworzonej przez nas struktury drzewa, która obejmuje rodziców i ich dzieci.
Dodaj ten kod na dole pliku app/js/index.js:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
}
Oto nasz cel logiki biznesowej, jak najbardziej zbliżony do języka naturalnego:
- Zdecyduj, jak obsłużyć kliknięcie.
- Jeśli jest następna/poprzednia relacja: wyświetl ją.
- Jeśli jest to ostatnia/pierwsza relacja znajomego: pokaż nowego znajomego.
- Jeśli w tym kierunku nie ma żadnej relacji, nic się nie dzieje.
- Zapisz nową bieżącą historię w:
state
Dodaj wyróżniony kod do funkcji navigateStories:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
if (direction === 'next') {
if (lastItemInUserStory === story && !hasNextUserStory)
return
else if (lastItemInUserStory === story && hasNextUserStory) {
state.current_story = story.parentElement.nextElementSibling.lastElementChild
story.parentElement.nextElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.classList.add('seen')
state.current_story = story.previousElementSibling
}
}
else if(direction === 'prev') {
if (firstItemInUserStory === story && !hasPrevUserStory)
return
else if (firstItemInUserStory === story && hasPrevUserStory) {
state.current_story = story.parentElement.previousElementSibling.firstElementChild
story.parentElement.previousElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.nextElementSibling.classList.remove('seen')
state.current_story = story.nextElementSibling
}
}
}
Wypróbuj
- Aby wyświetlić podgląd witryny, naciśnij Wyświetl aplikację, a następnie Pełny ekran
.
Podsumowanie
To wszystko, czego potrzebowałem od tego komponentu. Możesz go rozbudowywać, wykorzystywać w nim dane i dostosowywać do swoich potrzeb.