Z tego kursu dowiesz się, jak utworzyć w internecie funkcję podobną do relacji na Instagramie. Komponent będziemy tworzyć stopniowo, zaczynając od HTML, potem CSS i JavaScript.
Więcej informacji o ulepszeniach wprowadzonych podczas tworzenia tego komponentu znajdziesz w moim poście na blogu Building a Stories component.
Konfiguracja
- Kliknij Remiksuj, aby edytować, aby umożliwić edycję 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, dlatego 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 dla komponentu
historii.
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 kilka elementów <article>
reprezentujących historie:
<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 tworzenia prototypów artykułów używamy usługi obrazów (
picsum.com
). - Atrybut
style
w każdym<article>
jest częścią techniki ładowania elementów zastępczych, o której dowiesz się więcej w następnej sekcji.
CSS
Nasze treści są gotowe do zastosowania stylu. Zmieńmy te kości w coś, z czym ludzie będą chcieli wchodzić w interakcje. Dziś będziemy pracować w trybie mobilnym.
.stories
W przypadku kontenera <div class="stories">
chcemy, aby przewijanie było poziome.
Możemy to osiągnąć, wykonując te czynności:
- Przekształcanie kontenera w siatkę
- Ustawienie każdego elementu podrzędnego tak, aby wypełniał ścieżkę wiersza
- Ustawienie szerokości każdego elementu podrzędnego na szerokość obszaru wyświetlania urządzenia mobilnego
Siatka będzie nadal umieszczać nowe kolumny o szerokości 100vw
po prawej stronie poprzedniej, dopóki nie umieści wszystkich elementów HTML w Twoim znaczniku.

Dodaj ten kod CSS na końcu pliku app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Teraz, gdy mamy już treść wykraczającą poza widoczny obszar, musimy poinformować kontener, jak ją obsługiwać. Dodaj do zestawu reguł .stories
wyróżnione wiersze kodu:
.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 przewijania poziomego, więc ustawimy overflow-x
na auto
. Gdy użytkownik przewija, chcemy, aby komponent delikatnie zatrzymywał się na następnej historii, dlatego użyjemy scroll-snap-type: x mandatory
. Więcej informacji o tym 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 przyciąganie przewijania, więc zajmijmy się tym teraz. Dodaj ten kod na końcu pliku app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Twoja aplikacja jeszcze nie działa, ale poniższy film pokazuje, co się dzieje, gdy funkcja
scroll-snap-type
jest włączona i wyłączona. Gdy ta funkcja jest włączona, każde przewinięcie w poziomie powoduje przejście do następnej historii. Gdy ta opcja jest wyłączona, przeglądarka używa domyślnego działania przewijania.
W ten sposób przejdziesz do listy znajomych, ale nadal mamy problem z historiami do rozwiązania.
.user
Utwórzmy układ w sekcji .user
, który umieści te elementy podrzędne w odpowiednich miejscach. Aby to zrobić, użyjemy sprytnej sztuczki z nakładaniem.
Tworzymy w zasadzie siatkę 1x1, w której wiersz i kolumna mają ten sam alias Grid: [story]
. Każdy element siatki historii będzie próbował zająć to miejsce, co spowoduje utworzenie stosu.
Dodaj zaznaczony kod do zestawu reguł .user
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Na końcu pliku app/css/index.css
dodaj ten zestaw reguł:
.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 żaden kod, spójrz! Szczegółowe informacje znajdziesz w filmie i poście na blogu.
.story
Teraz musimy tylko ostylować sam element historii.
Wcześniej wspomnieliśmy, że atrybut style
w każdym elemencie <article>
jest częścią techniki ładowania elementó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 załadowaniu. Aby to zrobić, 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 symbol zastępczy ładowania.
Najpierw zaktualizujmy .story
zestaw reguł, aby po zakończeniu wczytywania zastąpić gradient obrazem tła. Dodaj zaznaczony 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));
}
Ustawienie background-size
na cover
zapewnia, że w obszarze wyświetlania nie będzie pustego miejsca, ponieważ wypełni go nasz obraz. Zdefiniowanie 2 obrazów tła umożliwia zastosowanie sprytnej sztuczki CSS o nazwie loading tombstone:
- Obraz tła 1 (
var(--bg)
) to adres URL przekazany w kodzie HTML. - Obraz tła 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
to gradient wyświetlany podczas wczytywania adresu URL
Gdy obraz zostanie pobrany, CSS automatycznie zastąpi nim gradient.
Następnie dodamy trochę kodu CSS, aby usunąć niektóre zachowania, co przyspieszy działanie przeglądarki.
Dodaj zaznaczony 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;
}
user-select: none
uniemożliwia użytkownikom przypadkowe zaznaczenie tekstu.touch-action: manipulation
informuje przeglądarkę, że te interakcje powinny być traktowane jako zdarzenia dotyku, co zwalnia przeglądarkę z konieczności decydowania, czy klikasz adres URL, czy nie.
Na koniec dodajmy trochę kodu CSS, aby animować przejście między opowiadaniami. 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 relacji, która wymaga zamknięcia.
Niestandardową funkcję łagodzenia (cubic-bezier(0.4, 0.0, 1,1)
) zaczerpnąłem z przewodnika Easing w Material Design (przewiń do sekcji Accerlerated easing).
Jeśli masz sokoli wzrok, prawdopodobnie zauważyłeś(-aś) pointer-events: none
deklarację i zastanawiasz się teraz, o co chodzi. To jedyna wada tego rozwiązania. Jest to konieczne, ponieważ element .seen.story
będzie na wierzchu i będzie odbierać kliknięcia, mimo że jest niewidoczny. Ustawiając wartość pointer-events
na none
, zamieniamy historię szkła w okno i nie przechwytujemy już interakcji użytkownika. Nie jest to zbyt duży kompromis i nie jest to zbyt trudne do zarządzania w naszej usłudze porównywania cen. Nie żonglujemy z-index
. Nadal mam dobre przeczucia.
JavaScript
Interakcje z komponentem Stories są dla użytkownika dość proste: dotknij prawej strony, aby przejść dalej, a lewej, aby wrócić. Proste rozwiązania dla użytkowników zwykle wymagają od deweloperów dużo pracy. Jednak w wielu przypadkach to my się tym zajmiemy.
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)
Pierwsza linia kodu JavaScript pobiera i przechowuje odwołanie do głównego elementu HTML. Kolejna linia oblicza, gdzie znajduje się środek elementu, dzięki czemu możemy określić, czy kliknięcie ma spowodować przejście do przodu czy do tyłu.
Stan
Następnie tworzymy mały obiekt z pewnym stanem istotnym dla naszej logiki. W tym przypadku interesuje nas tylko bieżąca historia. W naszym znaczniku HTML możemy uzyskać do niego dostęp, pobierając pierwszego znajomego i jego najnowszą historię. 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ącą logikę, aby zacząć nasłuchiwać zdarzeń użytkownika i kierować nimi.
Mysz
Zacznijmy od nasłuchiwania zdarzenia 'click'
w kontenerze historii.
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
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
Jeśli kliknięcie nastąpi w miejscu, które nie jest elementem <article>
, przerywamy działanie i nic nie robimy.
Jeśli jest to artykuł, pobieramy położenie poziome myszy lub palca za pomocą funkcji clientX
. Nie zaimplementowaliśmy jeszcze funkcji navigateStories
, ale argument, który przyjmuje, określa, w jakim kierunku musimy się poruszać. Jeśli pozycja użytkownika jest większa niż mediana, wiemy, że musimy przejść do next
, w przeciwnym razie do prev
(poprzednia).
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 stanu prev
.
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
}
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 na zajęcie się unikalną logiką biznesową relacji i UX, z którego stały się znane. Wygląda to na skomplikowane, ale jeśli przeanalizujesz to wiersz po wierszu, zobaczysz, że jest to całkiem proste.
Z góry zapisujemy niektóre selektory, które pomagają nam zdecydować, czy przewinąć do znajomego, czy pokazać lub ukryć historię. Ponieważ pracujemy w HTML-u, będziemy wyszukiwać w nim znajomych (użytkowników) lub historie (story).
Te zmienne pomogą nam odpowiedzieć na pytania takie jak: „czy w przypadku historii x „dalej” oznacza przejście do innej historii tego samego znajomego czy innego znajomego?”. Zrobiłem to, korzystając z zbudowanej przez nas struktury drzewa, docierając do rodziców i ich dzieci.
Dodaj ten kod na końcu 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 dotyczący logiki biznesowej, opisany w sposób jak najbardziej zbliżony do języka naturalnego:
- Określ, jak ma być obsługiwane kliknięcie.
- Jeśli jest następna lub poprzednia relacja: wyświetl ją.
- Jeśli jest to ostatnia lub pierwsza historia znajomego: pokaż nowego znajomego.
- Jeśli w tym kierunku nie ma żadnej historii: nic nie rób.
- Przenieś nową bieżącą relację do
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 strony, kliknij Wyświetl aplikację, a następnie Pełny ekran
.
Podsumowanie
To wszystko, czego potrzebowałem w przypadku tego komponentu. Możesz go rozbudowywać, wykorzystywać w nim dane i dostosowywać go do swoich potrzeb.