In diesem Codelab erfahren Sie, wie Sie eine Funktion wie Instagram Stories im Web erstellen. Wir erstellen die Komponente Schritt für Schritt, beginnend mit HTML, dann CSS und schließlich JavaScript.
In meinem Blogpost Building a Stories component (Eine Stories-Komponente erstellen) erfährst du mehr über die progressiven Verbesserungen, die beim Erstellen dieser Komponente vorgenommen wurden.
Einrichtung
- Klicken Sie auf Remix erstellen, um zu bearbeiten, um das Projekt bearbeitbar zu machen.
- Öffnen Sie
app/index.html.
HTML
Ich versuche immer, semantisches HTML zu verwenden.
Da jeder Freund eine beliebige Anzahl von Stories haben kann, hielt ich es für sinnvoll, für jeden Freund ein <section>-Element und für jede Story ein <article>-Element zu verwenden.
Fangen wir aber von vorne an. Zuerst benötigen wir einen Container für unsere Stories-Komponente.
Fügen Sie Ihrer <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 Storys darzustellen:
<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 Bilderdienst (
picsum.com), um Prototypen für Geschichten zu erstellen. - Das Attribut
stylefür jedes<article>ist Teil einer Platzhalter-Ladetechnik, über die Sie im nächsten Abschnitt mehr erfahren.
CSS
Unsere Inhalte sind bereit für die Stilgenerierung. Lassen Sie uns diese Knochen in etwas verwandeln, mit dem die Leute interagieren möchten. Wir arbeiten heute nach dem Mobile-First-Prinzip.
.stories
Für den <div class="stories">-Container möchten wir einen horizontal scrollbaren Container.
Das können wir so erreichen:
- Container als Raster festlegen
- Jedes untergeordnete Element so einstellen, dass es den Zeilen-Track füllt
- Die Breite jedes untergeordneten Elements entspricht der Breite des Darstellungsbereichs eines Mobilgeräts.
Das Raster platziert weiterhin neue 100vw breite Spalten rechts neben der vorherigen, bis alle HTML-Elemente in Ihrem Markup platziert sind.
Fügen Sie am Ende von app/css/index.css das folgende CSS hinzu:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Da wir nun Inhalte haben, die über den Darstellungsbereich hinausgehen, 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 horizontales 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 findest du in den Abschnitten CSS Scroll Snap Points und overscroll-behavior meines Blogposts.
Sowohl der übergeordnete Container als auch die untergeordneten Elemente müssen Scroll Snap unterstützen. Das sehen wir uns 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;
}
Ihre App funktioniert noch nicht, aber im Video unten sehen Sie, was passiert, wenn scroll-snap-type aktiviert und deaktiviert ist. Wenn diese Option aktiviert ist, wird beim horizontalen Scrollen jeweils zur nächsten Story gesprungen. Wenn die Richtlinie deaktiviert ist, verwendet der Browser das Standard-Scrollverhalten.
Damit können Sie durch Ihre Freunde scrollen, aber wir müssen noch ein Problem mit den Stories lösen.
.user
Erstellen wir im Abschnitt .user ein Layout, mit dem diese untergeordneten Story-Elemente an die richtige Stelle gebracht werden. Wir verwenden einen praktischen Stapeltrick, um dieses Problem zu lösen.
Wir erstellen im Wesentlichen ein 1x1-Raster, in dem die Zeile und die Spalte denselben Raster-Alias [story] haben. Jedes Story-Rasterelement versucht, diesen Platz zu beanspruchen, was zu einem Stapel führt.
Fügen Sie den hervorgehobenen Code zu Ihrem .user-Regelsatz hinzu:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Fügen Sie am Ende von app/css/index.css den folgenden Regelsatz hinzu:
.story {
grid-area: story;
}
Ohne absolute Positionierung, Floats oder andere Layoutanweisungen, die ein Element aus dem Fluss nehmen, befinden wir uns immer noch im Fluss. Außerdem ist es kaum Code, sehen Sie sich das an! Im Video und im Blogpost wird das genauer erläutert.
.story
Jetzt müssen wir nur noch das Story-Element selbst gestalten.
Wir haben bereits erwähnt, dass das style-Attribut für jedes <article>-Element Teil einer Platzhalter-Ladetechnik ist:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Wir verwenden die CSS-Eigenschaft background-image, mit der wir mehr als ein Hintergrundbild angeben können. Wir können sie so anordnen, dass das Nutzerbild oben ist und automatisch angezeigt wird, wenn es geladen wurde. Dazu fügen wir die Bild-URL in eine benutzerdefinierte Eigenschaft (--bg) ein und verwenden sie in unserem CSS, um sie mit dem Ladeplatzhalter zu überlagern.
Aktualisieren Sie zuerst den Regelsatz .story, um einen Farbverlauf durch ein Hintergrundbild zu ersetzen, sobald er geladen ist. 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));
}
Wenn Sie background-size auf cover setzen, wird sichergestellt, dass im Darstellungsbereich kein leerer Bereich vorhanden ist, da das Bild ihn ausfüllt. Durch die Definition von zwei Hintergrundbildern können wir einen raffinierten CSS-Webtrick namens Loading Tombstone anwenden:
- Hintergrundbild 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)
CSS ersetzt den Farbverlauf automatisch durch das Bild, sobald das Bild heruntergeladen wurde.
Als Nächstes fügen wir CSS hinzu, um einige 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: noneverhindert, dass Nutzer versehentlich Text auswählentouch-action: manipulationweist den Browser an, diese Interaktionen als Touch-Events zu behandeln. Der Browser muss dann nicht mehr entscheiden, ob Sie auf eine URL klicken oder nicht.
Fügen Sie abschließend ein wenig CSS hinzu, um den Übergang zwischen den Storys zu animieren. Fügen Sie den hervorgehobenen Code 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;
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);
&.seen {
opacity: 0;
pointer-events: none;
}
}
Die Klasse .seen wird einer Story hinzugefügt, die einen Exit benötigt.
Die benutzerdefinierte Easing-Funktion (cubic-bezier(0.4, 0.0, 1,1)) stammt aus dem Easing-Leitfaden von Material Design (scrollen Sie zum Abschnitt Beschleunigtes Easing).
Wenn Sie genau hingesehen haben, ist Ihnen wahrscheinlich die Deklaration pointer-events: none aufgefallen und Sie fragen sich, was es damit auf sich hat. Das ist meiner Meinung nach der einzige Nachteil der Lösung. Das ist erforderlich, weil ein .seen.story-Element oben liegt und Berührungen empfängt, obwohl es unsichtbar ist. Wenn wir pointer-events auf none setzen, wird die Glas-Story zu einem Fenster und es werden keine Nutzerinteraktionen mehr gestohlen. Das ist ein ganz guter Kompromiss und lässt sich in unserem CSS derzeit recht einfach verwalten. Wir jonglieren nicht mit z-index. Ich habe immer noch ein gutes Gefühl.
JavaScript
Die Interaktionen einer Stories-Komponente sind für den Nutzer recht einfach: Tippen Sie rechts, um vorwärts zu gehen, und links, um zurückzugehen. Einfache Dinge für Nutzer bedeuten für Entwickler meist harte Arbeit. Wir kümmern uns aber um einen Großteil davon.
Einrichtung
Berechnen und speichern Sie zu Beginn 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)
In der ersten Zeile des JavaScript-Codes wird ein Verweis auf das primäre HTML-Element „root“ abgerufen und gespeichert. In der nächsten Zeile wird berechnet, wo sich die Mitte unseres Elements befindet, damit wir entscheiden können, ob ein Tippen vorwärts oder rückwärts gehen soll.
Bundesland
Als Nächstes erstellen wir ein kleines Objekt mit einem Zustand, der für unsere Logik relevant ist. In diesem Fall sind wir nur an der aktuellen Story 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 weiterzuleiten.
Maus
Beginnen wir damit, auf das 'click'-Ereignis im Stories-Container zu warten.
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 und es sich nicht um ein <article>-Element handelt, wird nichts unternommen.
Wenn es sich um einen Artikel handelt, wird die horizontale Position der Maus oder des Fingers mit clientX erfasst. Wir haben navigateStories noch nicht implementiert, aber das Argument, das es annimmt, gibt an, in welche Richtung wir gehen müssen. Wenn die Position des Nutzers größer als der Median ist, müssen wir zu next navigieren, andernfalls zu prev (vorherige).
Tastatur
Jetzt wollen wir auf Tastendrücke warten. Wenn der Abwärtspfeil gedrückt wird, navigieren wir zu next. Wenn es der Aufwärtspfeil ist, gehen wir zu prev.
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')
})
document.addEventListener('keydown', ({key}) => {
if (key !== 'ArrowDown' || key !== 'ArrowUp')
navigateStories(
key === 'ArrowDown'
? 'next'
: 'prev')
})
Storys-Navigation
Jetzt ist es an der Zeit, sich mit der einzigartigen Geschäftslogik von Stories und der UX zu befassen, für die sie bekannt sind. Das sieht ziemlich kompliziert aus, aber wenn Sie es Zeile für Zeile durchgehen, werden Sie feststellen, dass es eigentlich ganz einfach ist.
Zuerst speichern wir einige Selektoren, mit denen wir entscheiden können, ob wir zu einem Freund scrollen oder eine Story ein- oder ausblenden möchten. Da wir im HTML-Code arbeiten, fragen wir ihn nach dem Vorhandensein von Freunden (Nutzern) oder Storys (Story) ab.
Diese Variablen helfen uns, Fragen wie die folgende zu beantworten: „Bedeutet ‚Weiter‘ bei Story X, dass zur nächsten Story desselben Freundes oder zu einem anderen Freund gewechselt werden soll?“ Ich habe dazu die von uns erstellte Baumstruktur verwendet und auf über- und untergeordnete Elemente zugegriffen.
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 nah wie möglich an natürlicher Sprache:
- Entscheiden, wie mit dem Tippen umgegangen werden soll
- Wenn es eine nächste/vorherige Story gibt: Diese Story anzeigen
- Wenn es die letzte/erste Story des Freundes ist: neuen Freund anzeigen
- Wenn es in dieser Richtung keine Story gibt: nichts tun
- Neue aktuelle Story in
statespeichern
Fügen Sie der Funktion navigateStories den hervorgehobenen Code hinzu:
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, klicken Sie auf View App (App ansehen) und dann auf Fullscreen (Vollbild)
.
Fazit
Das war alles, was ich für die Komponente brauchte. Sie können sie gerne ausbauen, mit Daten untermauern und ganz allgemein zu Ihrer eigenen machen.