Codelab: ساخت جزء داستان

این کد لبه به شما می آموزد که چگونه تجربه ای مانند استوری اینستاگرام در وب بسازید. ما کامپوننت را همانطور که پیش می‌رویم، با HTML، سپس CSS و سپس جاوا اسکریپت شروع می‌کنیم.

برای آشنایی با پیشرفت‌های پیشرونده‌ای که در ساخت این مؤلفه ایجاد شده است، پست وبلاگ من «ساخت یک داستان» را بررسی کنید.

برپایی

  1. روی Remix to Edit کلیک کنید تا پروژه قابل ویرایش باشد.
  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"> ما یک ظرف اسکرول افقی می خواهیم. ما می توانیم به این هدف دست پیدا کنیم:

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

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

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

CSS زیر را به پایین app/css/index.css اضافه کنید:

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

اکنون که محتوایی فراتر از viewport را داریم، زمان آن رسیده است که به آن ظرف بگوییم چگونه آن را مدیریت کند. خطوط کد برجسته شده را به مجموعه قوانین .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 ایجاد می کنیم که در آن سطر و ستون دارای نام مستعار Grid یکسانی از [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 منظم به نام سنگ قبر بارگذاری کنیم:

  • تصویر پس‌زمینه 1 ( var(--bg) ) آدرس اینترنتی است که ما در 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) ) را از راهنمای آسان‌سازی متریال دیزاین دریافت کردم (به بخش کاهش سرعت تسریع شده بروید).

اگر چشم تیزبین داشته باشید، احتمالاً متوجه pointer-events: none اعلامیه ای وجود ندارد و در حال حاضر سر خود را می خارید. من می توانم بگویم این تنها نقطه ضعف راه حل تا کنون است. ما به این نیاز داریم زیرا یک عنصر .seen.story در بالای صفحه قرار می‌گیرد و ضربه‌ها را دریافت می‌کند، حتی اگر نامرئی باشد. با تنظیم pointer-events روی none ، داستان شیشه‌ای را به یک پنجره تبدیل می‌کنیم و دیگر تعاملات کاربر را نمی‌دزدیم. خیلی بد نیست، مدیریت اینجا در CSS ما در حال حاضر خیلی سخت نیست. ما با z-index اشتباه نمی کنیم. هنوز در این مورد احساس خوبی دارم.

جاوا اسکریپت

تعاملات یک جزء Stories برای کاربر بسیار ساده است: برای رفتن به جلو روی سمت راست ضربه بزنید، برای بازگشت به سمت چپ روی آن ضربه بزنید. کارهای ساده برای کاربران معمولا برای توسعه دهندگان کار سختی است. با این حال، ما از بسیاری از آن مراقبت خواهیم کرد.

برپایی

برای شروع، بیایید تا آنجا که می توانیم اطلاعات را محاسبه و ذخیره کنیم. کد زیر را به 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')
})

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

وقت آن است که با منطق تجاری منحصر به فرد داستان ها و تجربه کاربری که به خاطر آن مشهور شده اند مقابله کنیم. این به نظر درشت و مشکل به نظر می رسد، اما فکر می کنم اگر خط به خط آن را بررسی کنید، متوجه خواهید شد که کاملا قابل هضم است.

در ابتدا، برخی از انتخابگرها را ذخیره می‌کنیم که به ما کمک می‌کنند تصمیم بگیریم به یک دوست برویم یا داستانی را نشان دهیم/پنهان کنیم. از آنجایی که 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
    }
  }
}

آن را امتحان کنید

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

نتیجه

این یک جمع بندی برای نیازهایی است که من با کامپوننت داشتم. با خیال راحت بر روی آن بسازید، آن را با داده هدایت کنید، و به طور کلی آن را مال خود کنید!