ה-Codelab הזה מלמד איך ליצור חוויה כמו סטוריז ב-Instagram באינטרנט. ניצור את הרכיב ככל שנמשיך, החל ב-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>
הוא חלק מטעינת placeholder לעיבוד טקסט, שתלמד עליו בקטע הבא.
CSS
התוכן שלנו מוכן לסגנון. בואו נהפוך את העצמות האלה למשהו שאנשים יאהבו שרוצים לקיים איתם אינטראקציה. היום נעבוד בעיקר על מכשירים ניידים.
.stories
למאגר <div class="stories">
אנחנו משתמשים במאגר גלילה אופקית.
כדי לעשות את זה:
- הגדרת הקונטיינר לרשת
- הגדרת מילוי של כל ילד או ילדה במסלול השורות
- קביעת הרוחב של כל ילד ורוחב של אזור תצוגה של מכשיר נייד
הרשת תמשיך למקם עמודות חדשות ברוחב 100vw
מימין לשורה הקודמת
אחת, עד שהוא יציב את כל רכיבי ה-HTML בתגי העיצוב.
![Chrome וכלי הפיתוח נפתחים בתצוגת רשת שמציגה את הפריסה ברוחב מלא](https://web.dev/static/articles/codelab-building-a-stories-component/image/chrome-devtools-open-a-72aaa3c91f873.png?authuser=5&hl=he)
צריך להוסיף את שירות ה-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
}
}
}
רוצה לנסות?
- כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על
מסך מלא
.
סיכום
זה סיכום של הצרכים שלי עם הרכיב. אתם מוזמנים ליצור להשיג אותו, להניע אותו בעזרת נתונים, ובאופן כללי להפוך אותו לאישי!