Codelab: יצירת רכיב של סטוריז

ה-Codelab הזה מלמד איך ליצור חוויה כמו סטוריז ב-Instagram באינטרנט. ניצור את הרכיב ככל שנמשיך, החל ב-HTML, אחר כך ב-CSS, ואז JavaScript.

אני רוצה לעיין בפוסט בבלוג שלי יצירת רכיב של סטוריז לקבל מידע על השיפורים ההדרגתיים שבוצעו במהלך פיתוח הרכיב הזה.

הגדרה

  1. לוחצים על רמיקס לעריכה כדי לערוך את הפרויקט.
  2. פתיחת app/index.html.

HTML

אני תמיד שואפת להשתמש ב-HTML סמנטי. מאחר שלכל חבר יכולים להיות כמה סיפורים, חשבתי שכדאי להשתמש רכיב <section> לכל חבר ורכיב <article> לכל כתבה. אבל נתחיל מההתחלה. קודם כל, אנחנו צריכים מכל רכיב של סיפורים.

צריך להוסיף רכיב <div> ל-<body>:

<div class="stories">

</div>

הוסף רכיבי <section> לייצוג חברים:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

אפשר להוסיף כמה רכיבי <article> כדי לייצג כתבות:

<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>
  • אנחנו משתמשים בשירות תמונות (picsum.com) כדי ליצור אב-טיפוס של כתבות.
  • המאפיין style בכל <article> הוא חלק מטעינת placeholder לעיבוד טקסט, שתלמד עליו בקטע הבא.

CSS

התוכן שלנו מוכן לסגנון. בואו נהפוך את העצמות האלה למשהו שאנשים יאהבו שרוצים לקיים איתם אינטראקציה. היום נעבוד בעיקר על מכשירים ניידים.

.stories

למאגר <div class="stories"> אנחנו משתמשים במאגר גלילה אופקית. כדי לעשות את זה:

  • הגדרת הקונטיינר לרשת
  • הגדרת מילוי של כל ילד או ילדה במסלול השורות
  • קביעת הרוחב של כל ילד ורוחב של אזור תצוגה של מכשיר נייד

הרשת תמשיך למקם עמודות חדשות ברוחב 100vw מימין לשורה הקודמת אחת, עד שהוא יציב את כל רכיבי ה-HTML בתגי העיצוב.

Chrome וכלי הפיתוח נפתחים בתצוגת רשת שמציגה את הפריסה ברוחב מלא
כלי הפיתוח ל-Chrome שמציגים אפשרויות נוספות של עמודות ברשת, תוך כדי גלילה אופקית.

צריך להוסיף את שירות ה-CSS הבא לתחתית של app/css/index.css:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

עכשיו שיש לנו תוכן שחורג מאזור התצוגה, הגיע הזמן לציין בקונטיינר שמסביר איך לטפל בו. צריך להוסיף את שורות הקוד המודגשות לקבוצת הכללים .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;
}

אנחנו רוצים גלילה אופקית, ולכן נגדיר את overflow-x ל- auto. כשהמשתמש גולל, אנחנו רוצים שהרכיב יושב בעדינות על הסיפור הבא, אז נשתמש ב-scroll-snap-type: x mandatory. מידע נוסף בנושא שירות CSS ב-CSS Scroll Snaps והתנהגות גלילה יתר בפוסט בבלוג שלי.

גם המאגר של ההורה וגם הילדים צריכים להסכים לגלילה מהירה, ולכן נטפל בזה עכשיו. צריך להוסיף את הקוד הבא בחלק התחתון של app/css/index.css:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

האפליקציה עדיין לא פועלת, אבל הסרטון שבהמשך מראה מה קורה כאשר scroll-snap-type מופעל ומושבת. כשהמדיניות מופעלת, כל פורמט אופקי גוללים אל הסיפור הבא. כשהאפשרות הזו מושבתת, הדפדפן משתמש פעולת הגלילה שמוגדרת כברירת מחדל.

כך אפשר לגלול בין החברים, אבל יש עדיין בעיה את הסיפורים שצריך לפתור.

.user

בואו ניצור פריסה בקטע .user שמתארת את סיפור הילד או הילדה את הרכיבים שלו. נשתמש בטריקים שימושיים בערימה כדי לפתור את הבעיה. אנחנו למעשה יוצרים רשת בגודל 1x1 שבה לרשת ולשורה יש אותה רשת הכינוי של [story], וכל פריט ברשת של הסיפור ינסה לתבוע בעלות על המרחב הזה, התוצאה היא סטאק.

צריך להוסיף את הקוד המודגש לקבוצת הכללים .user:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

מוסיפים את קבוצת הכללים הבאה בסוף של app/css/index.css:

.story {
  grid-area: story;
}

עכשיו, ללא מיקום מוחלט, מספר ממשי (float) או הוראות פריסה אחרות אלמנט לא תקין, אנחנו עדיין נמצאים בתהליך. בנוסף, זה כמעט כמו קוד, תראו את זה! הפירוט מוצג בסרטון ובפוסט בבלוג בפירוט.

.story

עכשיו צריך רק לעצב את הפריט של הסיפור עצמו.

קודם הזכרנו שהמאפיין style בכל רכיב <article> הוא חלק שיטת הטעינה של placeholder:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

נשתמש בנכס background-image של שירות ה-CSS, שמאפשר לנו לציין יותר מתמונת רקע אחת. אנחנו יכולים לסדר אותם כך שהמשתמשים שלנו התמונה למעלה והיא תופיע באופן אוטומטי בסיום הטעינה. שפת תרגום שנפעיל את זה, נוסיף את כתובת ה-URL של התמונה בנכס מותאם אישית (--bg) ונשתמש בה ב-CSS שלנו כדי להוסיף שכבות עם ה-placeholder של הטעינה.

קודם כול, נעדכן את קבוצת הכללים .story כדי להחליף הדרגתי בתמונת רקע בסיום הטעינה. צריך להוסיף את הקוד המודגש לקבוצת הכללים .story:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

אם קובעים את הערך cover בשדה background-size, אין שטח ריק אזור התצוגה כי התמונה שלנו תמלא אותו. הגדרה של 2 תמונות רקע מאפשרות לנו לקבל טריק פשוט לשימוש באינטרנט של שירות CSS שנקרא אבן הדרך לטעינה:

  • תמונת רקע 1 (var(--bg)) היא כתובת ה-URL שהעברנו בשורה ב-HTML
  • תמונת רקע 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) היא הדרגתית) יוצג בזמן שכתובת ה-URL נטענת

CSS יחליף באופן אוטומטי את ההדרגתיות בתמונה, כאשר הורדת התמונה תסתיים.

בשלב הבא נוסיף שירות CSS כדי להסיר התנהגויות מסוימות, ונפנה את הדפדפן כך שיפעל מהר יותר. צריך להוסיף את הקוד המודגש לקבוצת הכללים .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 מונע ממשתמשים לבחור טקסט בטעות
  • touch-action: manipulation מורה לדפדפן שהאינטראקציות האלה נחשבת כאירועי מגע, שמפנה את הדפדפן מניסיון להחליט אם אתם לוחצים על כתובת URL או לא

לסיום, נוסיף קצת CSS כדי להנפיש את המעבר בין הסיפורים. מוסיפים את קוד מודגש בקבוצת הכללים .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;
  }
}

הכיתה .seen תתווסף לסטורי שצריך לצאת ממנו. קיבלתי את פונקציית ההתאמה האישית (cubic-bezier(0.4, 0.0, 1,1)) מתוך הקלות של Material Design (גוללים אל הקטע התאמה משופרת).

אם הבחנת בזה, בטח שמת לב לpointer-events: none ואנחנו מגרדים את הראש כבר עכשיו. אני אומר שזה היחיד של הפתרון עד כה. אנחנו צריכים את זה כי רכיב .seen.story יהיה מעליו ויקבל הקשות, למרות שהוא בלתי נראה. על ידי הגדרה של pointer-events לnone, אנחנו הופכים את הסיפור מזכוכית לחלון וגונבים לא יותר אינטראקציות של משתמשים. זה לא כל כך חבל, זה לא כל כך קשה לנהל כאן בשירות ה-CSS שלנו. אנחנו לא עושים ג'אגלינג עם z-index. אני מרגיש טוב לגבי זה עדיין.

JavaScript

האינטראקציות של רכיב הסטוריז די פשוטות למשתמש: מקישים על ימינה כדי להמשיך, מקישים בצד ימין כדי לחזור אחורה. דברים פשוטים למשתמשים נוטים היא עבודה קשה למפתחים. אבל אנחנו נטפל בהרבה מהם.

הגדרה

בתור התחלה, נחשוב ונאחסן מידע רב ככל האפשר. מוסיפים ל-app/js/index.js את הקוד הבא:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

השורה הראשונה של JavaScript תופס ושומרת הפניה אל קוד ה-HTML הראשי שלנו הרמה הבסיסית (root). השורה הבאה מחשבת את המיקום האמצעי של הרכיב, כך יכול לקבוע אם הקשה היא קדימה או אחורה.

מדינה

בשלב הבא אנחנו יוצרים אובייקט קטן עם מצב כלשהו שרלוונטי ללוגיקה שלנו. כאן במקרה, אנחנו מתעניינים רק בסיפור הנוכחי. בתגי העיצוב של HTML, אנחנו יכולים כדי לגשת אליו, תוכלו לאסוף את החבר הראשון ואת הסיפור האחרון שלו. הוספת הקוד המודגש אל app/js/index.js:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

מאזינים

עכשיו יש לנו מספיק היגיון כדי להתחיל להקשיב לאירועי משתמשים ולהנהל אותם.

עכבר

נתחיל בהאזנה לאירוע 'click' במאגר הסטוריז שלנו. מוסיפים את הקוד המודגש ל-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')
})

אם קליק מתרחש והוא לא נמצא ברכיב <article>, אנחנו משחררים אותו בערבות ולא עושים כלום. אם מדובר בכתבה, אנחנו תופסים את המיקום האופקי של העכבר או האצבע באמצעות clientX עדיין לא יישמנו את navigateStories, אבל הארגומנט מציין לאיזה כיוון צריך ללכת. אם מיקום המשתמש הוא גדול מהחציון, אנחנו יודעים שצריך לנווט אל next, אחרת prev (האסטרטגיה הקודמת).

מקלדת

עכשיו נקשיב ללחיצות במקלדת. אם מקישים על החץ למטה, מנווטים אל next. אם זה חץ למעלה, עוברים אל prev.

מוסיפים את הקוד המודגש ל-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')
})

ניווט בסטוריז

הגיע הזמן להתמודד עם הלוגיקה העסקית הייחודית של סיפורים והחוויית השימוש שלהם מפורסמת בזכות. זה נראה מסובך וקשה, אבל אני חושב שאם נצליח לשלב את זה הוא קל להבנה.

קודם כל, אנחנו מסתירים כמה בוררים שעוזרים לנו להחליט אם לגלול חבר או הצגה/הסתרה של סיפור. מאחר שה-HTML הוא המקום שבו אנחנו עובדים, שליחת שאילתות לגבי נוכחות של חברים (משתמשים) או סיפורים (סיפור).

המשתנים האלה יעזרו לנו לענות על שאלות כמו "נותנים סיפור x ועושים "הבא". אתה אומר לעבור לסיפור אחר מאותו חבר או מחבר אחר?" עשיתי זאת באמצעות העץ שפיתחנו, כדי להגיע להורים ולילדים שלהם.

צריך להוסיף את הקוד הבא בחלק התחתון של 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
}

מטרת הלוגיקה העסקית היא להיות קרוב ככל האפשר לשפה טבעית:

  • החלטה איך לטפל בהקשה
    • אם יש סיפור הבא/הקודם: מציגים את הכתבה הזו
    • אם זה הסיפור האחרון/הראשון של חבר/ה: להראות חבר/ה חדש/ה
    • אם אין סיפור אחר שאפשר ללכת אליו בכיוון הזה: לא לעשות כלום.
  • הסתרת הסיפור הנוכחי החדש ב-state

מוסיפים את הקוד המודגש לפונקציה 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
    }
  }
}

רוצה לנסות?

  • כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על מסך מלא מסך מלא.

סיכום

זה סיכום של הצרכים שלי עם הרכיב. אתם מוזמנים ליצור להשיג אותו, להניע אותו בעזרת נתונים, ובאופן כללי להפוך אותו לאישי!