In diesem Codelab erfahren Sie, wie Sie eine Funktion wie Instagram Stories im Web erstellen. Wir erstellen die Komponente nach und nach, beginnend mit HTML, dann CSS und schließlich mit JavaScript.
In meinem Blogpost Building a Stories component (Eine Stories-Komponente erstellen) erfahren Sie mehr über die kontinuierlichen Verbesserungen, die während der Entwicklung dieser Komponente vorgenommen wurden.
Einrichtung
- Klicke auf Remix zum Bearbeiten, um das Projekt bearbeitbar zu machen.
- Öffnen Sie
app/index.html
.
HTML
Ich verwende immer semantisches HTML.
Da jeder Freund beliebig viele Geschichten haben kann, habe ich beschlossen, für jeden Freund ein <section>
-Element und für jede Geschichte ein <article>
-Element zu verwenden.
Fangen wir aber am Anfang an. Zuerst benötigen wir einen Container für die Stories-Komponente.
Fügen Sie Ihrem <body>
ein <div>
-Element hinzu:
<div class="stories">
</div>
Fügen Sie einige <section>
-Elemente hinzu, um Freunde darzustellen:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Fügen Sie einige <article>
-Elemente hinzu, um Geschichten zu repräsentieren:
<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>
- Wir verwenden einen Bilddienst (
picsum.com
), um Prototypen für Geschichten zu erstellen. - Das
style
-Attribut jedes<article>
ist Teil einer Methode zum Laden von Platzhaltern, die im nächsten Abschnitt näher erläutert wird.
CSS
Unsere Inhalte sind bereit für Stil. Lassen Sie uns diese Knochen in etwas verwandeln, mit dem Nutzer interagieren möchten. Wir arbeiten heute an einem Mobile-First-Ansatz.
.stories
Für unseren <div class="stories">
-Container möchten wir einen horizontal scrollbaren Container.
Das können wir erreichen, indem wir:
- Container in ein Raster verwandeln
- Jedes untergeordnete Element so einstellen, dass es den Zeilen-Track füllt
- Die Breite jedes untergeordneten Elements an die Breite des Darstellungsbereichs eines Mobilgeräts anpassen
Im Raster werden weiterhin neue 100vw
-breite Spalten rechts neben der vorherigen platziert, bis alle HTML-Elemente im Markup platziert sind.
Fügen Sie am Ende von app/css/index.css
den folgenden CSS-Code hinzu:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Da sich unsere Inhalte jetzt über den Darstellungsbereich hinaus erstrecken, müssen wir dem Container mitteilen, wie er damit umgehen soll. Fügen Sie Ihrem .stories
-Regelsatz die hervorgehobenen Codezeilen hinzu:
.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;
}
Wir möchten horizontal scrollen, also setzen wir overflow-x
auf auto
. Wenn der Nutzer scrollt, soll die Komponente sanft auf der nächsten Story landen. Daher verwenden wir scroll-snap-type: x mandatory
. Weitere Informationen zu diesem CSS finden Sie in den Abschnitten CSS-Scroll-Snap Points und overscroll-behavior meines Blogposts.
Sowohl der übergeordnete Container als auch die untergeordneten Elemente müssen dem Scroll-Snapping zustimmen. Gehen wir das jetzt an. Fügen Sie den folgenden Code am Ende von app/css/index.css
ein:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Deine App funktioniert noch nicht, aber im folgenden Video siehst du, was passiert, wenn scroll-snap-type
aktiviert und deaktiviert ist. Ist sie aktiviert, wird bei jedem horizontalen
Scrollen an die nächste Story angedockt. Wenn diese Option deaktiviert ist, verwendet der Browser das Standard-Scrollverhalten.
Dadurch können Sie durch Ihre Freunde scrollen, aber wir müssen noch ein Problem mit den Stories beheben.
.user
Erstellen wir im Bereich .user
ein Layout, in dem diese untergeordneten Storyelemente platziert werden. Wir verwenden einen praktischen Stacking-Trick, um das Problem zu lösen.
Im Grunde erstellen wir ein Raster mit einer Größe von 1 × 1, bei dem Zeile und Spalte denselben Rasteralias [story]
haben. Jedes Storyboard-Rasterelement versucht, diesen Bereich zu beanspruchen, was zu einem Stapel führt.
Fügen Sie den markierten Code zu Ihrer .user
-Regel hinzu:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Fügen Sie unten in app/css/index.css
die folgende Regel hinzu:
.story {
grid-area: story;
}
Ohne absolute Positionierung, Floats oder andere Layout-Direktive, die ein Element aus dem Fluss herausnehmen, sind wir immer noch im Fluss. Außerdem ist es fast kein Code, sehen Sie sich das an! Im Video und im Blogpost wird das genauer erläutert.
.story
Jetzt müssen wir nur noch dem Story-Element selbst einen Stil hinzufügen.
Wir haben bereits erwähnt, dass das style
-Attribut für jedes <article>
-Element Teil einer Methode zum Laden von Platzhaltern ist:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Wir verwenden die CSS-Eigenschaft background-image
, mit der wir mehrere Hintergrundbilder angeben können. Wir können sie so anordnen, dass das Bild des Nutzers oben angezeigt wird und automatisch erscheint, sobald das Laden abgeschlossen ist. Dazu fügen wir die Bild-URL in eine benutzerdefinierte Eigenschaft (--bg
) ein und verwenden diese in unserem CSS, um den Ladeplatzhalter übereinander zu legen.
Zuerst aktualisieren wir den Regelsatz .story
, um nach dem Laden einen Farbverlauf durch ein Hintergrundbild zu ersetzen. Fügen Sie den markierten Code zu Ihrer .story
-Regel hinzu:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Wenn Sie background-size
auf cover
festlegen, gibt es im Darstellungsbereich keinen leeren Raum, da er von unserem Bild ausgefüllt wird. Wenn wir zwei Hintergrundbilder definieren, können wir einen praktischen CSS-Webtrick namens Lade-Grabstein verwenden:
- „Background image 1“ (
var(--bg)
) ist die URL, die wir inline im HTML-Code übergeben haben. - Hintergrundbild 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
ist ein Farbverlauf, der angezeigt wird, während die URL geladen wird)
Sobald der Download des Bildes abgeschlossen ist, wird der Farbverlauf automatisch durch das Bild ersetzt.
Als Nächstes fügen wir CSS hinzu, um bestimmte Verhaltensweisen zu entfernen, damit der Browser schneller arbeiten kann.
Fügen Sie den hervorgehobenen Code zu Ihrem .story
-Regelsatz hinzu:
.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
verhindert, dass Nutzer versehentlich Text auswählentouch-action: manipulation
weist den Browser an, diese Interaktionen als Touch-Ereignisse zu behandeln. So muss der Browser nicht mehr entscheiden, ob Sie auf eine URL klicken oder nicht.
Fügen wir zum Schluss noch ein wenig CSS hinzu, um den Übergang zwischen den Storys zu animieren. Fügen Sie den hervorgehobenen Code zu Ihren .story
-Regeln hinzu:
.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;
}
}
Der Kurs „.seen
“ wird einer Story hinzugefügt, für die ein Exit erforderlich ist.
Die benutzerdefinierte Ease-Funktion (cubic-bezier(0.4, 0.0, 1,1)
) habe ich im Material Design-Leitfaden zu Ease-Funktionen gefunden (scrollen Sie zum Abschnitt Beschleunigte Ease-Funktion).
Wenn Sie ein scharfes Auge haben, haben Sie wahrscheinlich die pointer-events: none
-Erklärung bemerkt und fragen sich jetzt, was das zu bedeuten hat. Das ist der einzige Nachteil der Lösung. Das ist notwendig, da ein .seen.story
-Element oben angezeigt wird und angetippt wird, auch wenn es unsichtbar ist. Wenn wir pointer-events
auf none
setzen, verwandeln wir die Glasgeschichte in ein Fenster und stehlen keine Nutzerinteraktionen mehr. Das ist eine ziemliche Abwägung, die im Moment auch hier in unserem Preisvergleichsportal verwaltet werden kann. Wir jonglieren nicht mit z-index
. Ich bin immer noch zufrieden.
JavaScript
Die Interaktionen mit einer Stories-Komponente sind für Nutzer recht einfach: Tippen Sie nach rechts, um vorwärts zu gehen, und nach links, um zurückzugehen. Einfache Dinge für Nutzer bedeuten für Entwickler oft harte Arbeit. Wir übernehmen aber einen Großteil davon.
Einrichtung
Berechnen und speichern wir zuerst so viele Informationen wie möglich.
Fügen Sie den folgenden Code zu app/js/index.js
hinzu:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
Unsere erste JavaScript-Zeile erfasst und speichert einen Verweis auf unseren primären HTML-Elementstamm. In der nächsten Zeile wird berechnet, wo sich die Mitte unseres Elements befindet, damit wir entscheiden können, ob ein Tippen vor- oder zurückgehen soll.
Status
Als Nächstes erstellen wir ein kleines Objekt mit einem für unsere Logik relevanten Status. In diesem Fall sind wir nur an der aktuellen Meldung interessiert. In unserem HTML-Markup können wir darauf zugreifen, indem wir den ersten Freund und seine neueste Story abrufen. Fügen Sie den hervorgehobenen Code zu app/js/index.js
hinzu:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Listener
Wir haben jetzt genug Logik, um auf Nutzerereignisse zu warten und sie zu leiten.
Maus
Hören wir uns zuerst das Ereignis 'click'
in unserem Stories-Container an.
Fügen Sie den hervorgehobenen Code zu app/js/index.js
hinzu:
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')
})
Wenn ein Klick erfolgt, der nicht auf einem <article>
-Element erfolgt, wird nichts unternommen.
Wenn es sich um einen Artikel handelt, erfassen wir mit clientX
die horizontale Position der Maus oder des Fingers. Wir haben navigateStories
noch nicht implementiert, aber das Argument, das es nimmt, gibt an, in welche Richtung wir gehen müssen. Wenn diese Nutzerposition größer als der Median ist, müssen wir zu next
wechseln, andernfalls zu prev
(vorher).
Tastatur
Jetzt wollen wir auf Tastatureingaben achten. Wenn der Abwärtspfeil gedrückt wird, gelangen wir zu next
. Wenn es der Pfeil nach oben ist, gehen wir zu prev
.
Fügen Sie den markierten Code zu app/js/index.js
hinzu:
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')
})
Navigation in Stories
Es ist an der Zeit, sich mit der einzigartigen Geschäftslogik von Stories und der UX zu befassen, für die sie berühmt geworden sind. Das sieht etwas unübersichtlich und kompliziert aus, aber wenn Sie es Zeile für Zeile durchgehen, werden Sie feststellen, dass es ziemlich verständlich ist.
Vorne haben wir einige Auswahlschaltflächen, mit denen wir entscheiden können, ob wir zu einem Freund scrollen oder eine Story anzeigen oder ausblenden möchten. Da wir im HTML-Code arbeiten, fragen wir ihn nach der Anwesenheit von Freunden (Nutzern) oder Geschichten (Geschichte) ab.
Mit diesen Variablen können wir Fragen wie „Bedeutet ‚Weiter‘ bei Story X, dass ich zu einer anderen Story desselben Freundes oder zu einer Story eines anderen Freundes wechsele?“ beantworten. Dabei verwendete ich unsere Baumstruktur und sprach meine Eltern und deren Kinder an.
Fügen Sie den folgenden Code am Ende von app/js/index.js
ein:
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
}
Hier ist unser Geschäftslogikziel, so natürlich wie möglich formuliert:
- Legen Sie fest, wie mit dem Tippen verfahren werden soll.
- Wenn es eine nächste/vorherige Story gibt: Diese Story anzeigen
- Wenn es sich um die letzte oder erste Story des Freundes handelt: Einen neuen Freund anzeigen
- Wenn es in dieser Richtung keine Geschichte gibt, unternehmen Sie nichts.
- Neue aktuelle Story in
state
speichern
Fügen Sie den markierten Code in die navigateStories
-Funktion ein:
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
}
}
}
Jetzt ausprobieren
- Wenn Sie sich eine Vorschau der Website ansehen möchten, drücken Sie App ansehen und dann Vollbild .
Fazit
Das war es mit meinen Anforderungen an die Komponente. Sie können sie gerne erweitern, mit Daten optimieren und im Allgemeinen zu Ihrer eigenen machen.