Codelab: Stories-Komponente erstellen

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

  1. Klicke auf Remix zum Bearbeiten, um das Projekt bearbeitbar zu machen.
  2. Ö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.

Chrome und die Entwicklertools werden mit einem Rastervisualisierung geöffnet, das das Layout in voller Breite zeigt
In den Chrome-Entwicklertools wird ein horizontaler Bildlauf angezeigt, der den Rasterspaltenüberlauf anzeigt.

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ählen
  • touch-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 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.