בשיעור ה-Codelab הזה תלמדו איך ליצור חוויה כמו Instagram Stories באינטרנט. נבנה את הרכיב תוך כדי התקדמות, החל מ-HTML, לאחר מכן CSS ואז JavaScript.
בפוסט בבלוג שלי, יצירת רכיב של סיפורים, מוסבר על השיפורים ההדרגתיים שבוצעו במהלך יצירת הרכיב הזה.
הגדרה
- לוחצים על שינוי הסגנון לעריכה כדי שיהיה אפשר לערוך את הפרויקט.
- פתיחת
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>הוא חלק מטכניקת טעינה של פלייסהולדר, שעליה נרחיב בקטע הבא.
CSS
התוכן שלנו מוכן לעיצוב. בואו נהפוך את השלד הזה למשהו שאנשים ירצו להשתמש בו. היום נתמקד קודם כל בנייד.
.stories
אנחנו רוצים שהמאגר <div class="stories"> יהיה מאגר עם גלילה אופקית.
כדי לעשות את זה, אנחנו יכולים:
- הפיכת הקונטיינר לרשת
- הגדרת כל צאצא כך שימלא את מסלול השורה
- הגדרת הרוחב של כל רכיב צאצא לרוחב של אזור התצוגה במכשיר נייד
הרשת תמשיך להציב עמודות חדשות ברוחב 100vw מימין לעמודה הקודמת, עד שכל רכיבי ה-HTML יוצבו בתגי העיצוב.
מוסיפים את ה-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 Snap Points ו-overscroll-behavior בפוסט בבלוג שלי.
כדי להפעיל גלילה עם נקודות עצירה, צריך להגדיר את זה גם בקונטיינר הראשי וגם בקונטיינרים המשניים. מוסיפים את הקוד הבא לחלק התחתון של 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;
}
עכשיו, בלי מיקום מוחלט, רכיבים צפים או הוראות פריסה אחרות שמוציאות רכיב מהזרימה, אנחנו עדיין בזרימה. בנוסף, כמעט ואין קוד, תראו את זה! בסרטון ובפוסט בבלוג יש פירוט נוסף בנושא.
.story
עכשיו צריך רק לעצב את פריט הסטורי עצמו.
קודם לכן ציינו שהמאפיין style בכל רכיב <article> הוא חלק מטכניקת טעינה של פלייסהולדר:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
נשתמש במאפיין background-image של CSS, שמאפשר לנו לציין יותר מתמונת רקע אחת. אנחנו יכולים לסדר אותם כך שתמונת המשתמש תהיה למעלה ותופיע אוטומטית כשהיא תסיים להיטען. כדי לעשות את זה, נכניס את כתובת ה-URL של התמונה למאפיין מותאם אישית (--bg), ונשתמש בה ב-CSS כדי להוסיף שכבה עם פלייסהולדר לטעינה.
קודם כל, נעדכן את קבוצת הכללים של .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));
}
הגדרת background-size ל-cover מבטיחה שלא יהיה שטח ריק באזור התצוגה כי התמונה תמלא אותו. הגדרת 2 תמונות רקע מאפשרת לנו להשתמש בטריק CSS נחמד שנקרא loading tombstone:
- תמונת רקע 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 תתווסף לסטורי שצריך יציאה.
פונקציית ה-easing המותאמת אישית (cubic-bezier(0.4, 0.0, 1,1)) נלקחה מהמדריך Easing של Material Design (צריך לגלול לקטע Accelerated easing).
אם יש לכם עין חדה, בטח שמתם לב להצהרה pointer-events: none ואתם תוהים מה היא אומרת. אני חושב שזה החיסרון היחיד
של הפתרון הזה עד עכשיו. אנחנו צריכים את זה כי רכיב .seen.story
יהיה למעלה ויקבל קליקים, למרות שהוא בלתי נראה. אם מגדירים את pointer-events לערך none, הופכים את ה-glass story לחלון, ולא גונבים יותר אינטראקציות של משתמשים. זו פשרה לא רעה, ולא קשה מדי לנהל את זה כאן ב-CSS שלנו כרגע. אנחנו לא מלהטטים ב-z-index. אני עדיין מרגיש טוב לגבי זה.
JavaScript
האינטראקציות של רכיב הסטוריז די פשוטות למשתמש: לוחצים בצד ימין כדי להתקדם, לוחצים בצד שמאל כדי לחזור אחורה. דברים פשוטים למשתמשים הם בדרך כלל עבודה קשה למפתחים. אבל אנחנו נטפל בהרבה מהדברים.
הגדרה
כדי להתחיל, נחשב ונאחסן כמה שיותר מידע.
מוסיפים את הקוד הבא ל-app/js/index.js:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
השורה הראשונה של JavaScript מאחזרת ומאחסנת הפניה לרכיב ה-HTML הראשי. השורה הבאה מחשבת איפה נמצא מרכז הרכיב, כדי שנוכל להחליט אם לחיצה תעביר קדימה או אחורה.
מדינה (State)
עכשיו ניצור אובייקט קטן עם מצב שרלוונטי ללוגיקה שלנו. במקרה הזה, אנחנו מתעניינים רק בסיפור הנוכחי. בתגי העיצוב של ה-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
}
}
}
רוצה לנסות?
- כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה ואז על מסך מלא
.
סיכום
זה מסכם את הצרכים שלי מהרכיב. אתם יכולים להוסיף לו, להשתמש בנתונים כדי להניע אותו, ובאופן כללי להפוך אותו לשלכם.