בשיעור הזה תלמדו איך ליצור חוויה כמו סטוריז של Instagram באינטרנט. אנחנו ניצור את הרכיב תוך כדי עבודה, ונתחיל עם HTML, אחר כך CSS, ואז ב-JavaScript.
תוכלו לקרוא את הפוסט בבלוג שלי על Building a Stories כדי לקבל מידע על השיפורים ההדרגתיים שבוצעו במהלך בניית הרכיב הזה.
הגדרה
- לוחצים על רמיקס לעריכה כדי שיהיה אפשר לערוך את הפרויקט.
- פתיחת
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">
, אנחנו זקוקים למאגר גלילה אופקית.
כדי לעשות זאת:
- הגדרת הקונטיינר כרשת
- הגדרת כל ילד כך שימלא את שורת השורה.
- קביעת הרוחב של כל צאצא כרוחב של אזור תצוגה של מכשיר נייד
ה-Grid תמשיך למקם עמודות חדשות ברוחב 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>
הוא חלק משיטת טעינה של 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));
}
אם מגדירים את הערך של background-size
לערך cover
, אין מספיק מקום באזור התצוגה כי התמונה שלנו תמלא אותו. הגדרה של 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 (גוללים לקטע Accerleated המונטיזציה).
אם יש לכם עין חזקה, סביר להניח שהבחנתם בהצהרה pointer-events: none
ואתם מגרדים את הראש עכשיו. אפשר לומר שזה החיסרון היחיד של הפתרון עד עכשיו. אנחנו זקוקים לכך כי רכיב .seen.story
יופיע למעלה ויקבל הקשות, למרות שהוא בלתי נראה. כשמגדירים את הערך none
בשדה pointer-events
, אנחנו הופכים את סיפור הזכוכית לחלון ולא גונבים יותר אינטראקציות של משתמשים. לא כל כך חבל, לא קשה לנהל את זה כאן,
בשירות ה-CSS שלנו כרגע. אנחנו לא עושים ג'אגלינג עם z-index
. אני מרגיש טוב
עם זה.
JavaScript
האינטראקציות של משתמשים עם רכיב 'סטוריז' די פשוטות: מקישים על צד ימין כדי להתקדם, ומקישים על צד שמאל כדי לחזור אחורה. פעולות פשוטות למשתמשים בדרך כלל הן עבודה קשה למפתחים. אבל אנחנו נטפל בהמון דברים.
הגדרה
תחילה נחשב ונאחסן מידע רב ככל האפשר.
מוסיפים את הקוד הבא ל-app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
השורה הראשונה של JavaScript מאחזרת ומאחסנת הפניה לשורש רכיב ה-HTML הראשי שלנו. השורה הבאה מחשבת את החלק האמצעי של האלמנט, כדי שנוכל להחליט אם הקשה תפנה קדימה או אחורה.
ארץ
לאחר מכן ניצור אובייקט קטן עם כמה מצבים שרלוונטיים ללוגיקה שלנו. במקרה הזה, אנחנו מתעניינים רק בסיפור הנוכחי. בתגי העיצוב של HTML, אנחנו יכולים לגשת אליו
על ידי לחיצה על החבר הראשון ואת הסיפור האחרון שלו. מוסיפים את הקוד המודגש ל-app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
פונקציות מסוג listener
יש לנו מספיק היגיון עכשיו כדי להתחיל להאזין לאירועי משתמשים ולכוון אותם.
עכבר
נתחיל בהאזנה לאירוע '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 הוא המקום שבו אנחנו עובדים, נשלח אליו שאילתה לגבי נוכחות של חברים (משתמשים) או סיפורים (סיפור).
המשתנים האלה יעזרו לנו לענות על שאלות כמו "בהתחשב בסיפור כ', האם "הבא" פירושו מעבר לסיפור אחר מאותו חבר או לחבר אחר?" עשיתי את זה בעזרת מבנה העץ שבנינו, כשיצרתי קשר עם ההורים והילדים שלהם.
מוסיפים את הקוד הבא בחלק התחתון של 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
}
}
}
אני רוצה לנסות
- כדי לראות תצוגה מקדימה של האתר, מקישים על View App ואז על Fullscreen .
סיכום
זהו סיכום הצרכים שהיו לי עם הרכיב. אתם יכולים להסתמך על המידע הזה, להוסיף לו נתונים ובאופן כללי להפוך אותו לאישי!