‫Codelab: ساختن عنصر «داستان‌ها»

این codelab به شما آموزش می‌دهد که چگونه تجربه‌ای مانند «داستان‌های اینستاگرام» در وب بسازید. همان‌طور که پیش می‌رویم، مؤلفه را می‌سازیم، ابتدا با 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> بخشی از تکنیک بار کردن جای‌بان است که در بخش بعدی بیشتر درباره آن خواهید آموخت.

CSS

محتوای ما برای سبک آماده است. بیایید آن استخوان‌ها را به چیزی تبدیل کنیم که مردم بخواهند با آن تعامل داشته باشند. امروز اولویت ما تلفن همراه خواهد بود.

.stories

برای محفظه <div class="stories">، محفظه پیمایش افقی می‌خواهیم. می‌توانیم این کار را ازطریق زیر انجام دهیم:

  • تبدیل کردن محفظه به شبکه
  • تنظیم هر فرزند برای پر کردن قطعه ردیف
  • تنظیم عرض هر فرزند به‌اندازه عرض نمای دستگاه همراه

«شبکه» به قرار دادن ستون‌های جدید با عرض 100vw در سمت راست ستون قبلی ادامه می‌دهد تا زمانی که همه عناصر HTML را در نشان‌گذاری شما قرار دهد.

Chrome و DevTools با تصویری از شبکه باز هستند که طرح‌بندی تمام‌عرض را نشان می‌دهد
Chrome DevTools سرریز ستون شبکه را نشان می‌دهد که باعث ایجاد پیمایشگر افقی می‌شود.

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 Scroll Snap Points و overscroll-behavior پست وبلاگم، درباره این CSS بیشتر بخوانید.

هم محفظه اصلی و هم محفظه‌های فرعی باید با میزان کردن پیمایش موافقت کنند، پس بیایید اکنون به این موضوع بپردازیم. کد زیر را به پایین app/css/index.css اضافه کنید:

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

برنامه‌تان هنوز کار نمی‌کند، اما ویدیو زیر نشان می‌دهد که وقتی scroll-snap-type فعال و غیرفعال است چه اتفاقی می‌افتد. وقتی فعال باشد، هر پیمایش افقی به داستان بعدی می‌رود. وقتی غیرفعال باشد، مرورگر از رفتار پیمایش پیش‌فرض خود استفاده می‌کند.

این کار باعث می‌شود بتوانید در فهرست دوستانتان پیمایش کنید، اما هنوز مشکلی در داستان‌ها وجود دارد که باید حل شود.

.user

بیایید چیدمانی در بخش .user ایجاد کنیم که عناصر داستان فرزند را در جای خود قرار دهد. برای حل این مسئله، از ترفند مفید پشته‌سازی استفاده می‌کنیم. ما اساساً یک شبکه ۱×۱ ایجاد می‌کنیم که در آن ردیف و ستون دارای نام مستعار شبکه یکسان [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 استفاده خواهیم کرد که به ما امکان می‌دهد بیش از یک تصویر پس‌زمینه را مشخص کنیم. می‌توانیم آن‌ها را به‌ترتیبی قرار دهیم که عکس کاربر در بالا باشد و وقتی بارگیری تمام شد، به‌طور خودکار نشان داده شود. برای فعال کردن این ویژگی، نشانی وب تصویرمان را در یک ویژگی سفارشی (--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 تضمین می‌کند که هیچ فضای خالی‌ای در نمای دید وجود نداشته باشد زیرا تصویر ما آن را پر می‌کند. تعریف ۲ تصویر پس‌زمینه به ما امکان می‌دهد ترفند وب CSS جالبی به‌نام سنگ قبر بارگیری را اجرا کنیم:

  • تصویر پس‌زمینه ۱ (var(--bg)) نشانی وبی است که به‌خط در HTML ارسال کردیم
  • تصویر پس‌زمینه ۲ (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) گرادیانی است که هنگام بار شدن نشانی وب نشان داده می‌شود

وقتی بارگیری تصویر تمام شود، 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 به مرورگر دستور می‌دهد که این تعاملات باید به‌عنوان رویدادهای لمسی درنظر گرفته شوند، که این امر مرورگر را از تلاش برای تصمیم‌گیری درباره اینکه آیا روی نشانی وب کلیک می‌کنید یا نه آزاد می‌کند

در آخر، بیایید کمی 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)) را از راهنمای هموارسازی «طراحی مواد» دریافت کردم (به بخش هموارسازی شتاب‌دار پیمایش کنید).

اگر چشمان تیزبینی داشته باشید، احتمالاً متوجه اعلان 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)

اولین خط جاوا اسکریپت ما مرجعی به ریشه عنصر 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
}

شنوندگان

اکنون منطق کافی برای شروع گوش دادن به رویدادهای کاربر و هدایت آن‌ها داریم.

موش

بیایید با گوش دادن به رویداد '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')
})

پیمایش داستان‌ها

زمان آن رسیده است که به منطق کسب‌وکار منحصربه‌فرد داستان‌ها و تجربه کاربری که به‌خاطر آن معروف شده‌اند بپردازیم. این به‌نظر پیچیده و دشوار می‌رسد، اما فکر می‌کنم اگر آن را خط‌به‌خط بخوانید، متوجه می‌شوید که کاملاً قابل‌فهم است.

Upfront, we stash some selectors that help us decide whether to scroll to a friend or show/hide a story. ازآنجایی‌که 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
    }
  }
}

امتحان کنید

  • برای پیش‌دید کردن سایت، مشاهده برنامه را فشار دهید. سپس تمام‌صفحه تمام‌صفحه را فشار دهید.

نتیجه‌گیری

این خلاصه نیازهایی است که با این مؤلفه داشتم. می‌توانید آن را توسعه دهید، با داده‌ها هدایتش کنید، و به‌طور کلی آن را ازآن خود کنید!