يشرح لك هذا الدرس التطبيقي حول الترميز كيفية إنشاء تجربة مشابهة لـ "قصص Instagram" على الويب. سننشئ المكوّن أثناء التقدّم، بدءًا من HTML ثم CSS، ثم JavaScript.
يمكنك الاطّلاع على مقالة المدوّنة إنشاء مكوّن "قصص" للتعرّف على التحسينات التدريجية التي تم إجراؤها أثناء إنشاء هذا المكوّن.
ضبط إعدادات الجهاز
- انقر على Remix to Edit (إنشاء ريمكس لتعديل المحتوى) ليصبح المشروع قابلاً للتعديل.
- فتح "
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>
جزءًا من أسلوب loading البديل، ويمكنك الاطّلاع على مزيد من المعلومات عنه في القسم التالي.
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
وoverscroll-behavior
في مشاركة المدوّنة.
يجب أن توافق كلّ من الحاوية الرئيسية والعناصر الثانوية على ميزة "الانتقال السريع"، لذلك دعونا نتناول هذه النقطة الآن. أضِف الرمز التالي إلى أسفل app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
لا يعمل تطبيقك بعد، ولكن يوضّح الفيديو أدناه ما يحدث عند تفعيل ميزة
scroll-snap-type
وإيقافها. عند تفعيل هذه الميزة، يتم الانتقال إلى القصة التالية عند التمرير بشكل أفقي. وعند إيقاف هذه الميزة، يستخدم المتصفِّح
سلوك التمرير التلقائي.
سيؤدي ذلك إلى تصفّح أصدقائك، ولكن لا تزال لدينا مشكلة في القصص يجب حلّها.
.user
لننشئ تنسيقًا في القسم .user
يُعيد ترتيب عناصر القصص الفرعية. سنستخدم خدعة رائعة لحلّ هذه المشكلة.
نحن بصدد إنشاء شبكة 1×1 يكون فيها الصف والعمود يحملان العنوان الرمزي نفسه للشبكة
[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>
هي جزء من أسلوب loading placeholder (تحميل العنصر النائب):
<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
عدم توفّر مساحة فارغة في
مجال العرض لأنّ صورتنا ستملؤه. من خلال تحديد صورتَي خلفية،
يمكننا استخدام خدعة رائعة على الويب باستخدام 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
إلى قصة تحتاج إلى مخرج.
حصلتُ على دالة التخفيف المخصّصة (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 الأساسي ويخزّنه. يحسب السطر التالي موضع منتصف العنصر، حتى تتمكّن من تحديد ما إذا كانت النقرة ستؤدي إلى التقديم أو الإيقاف.
الحالة
بعد ذلك، ننشئ كائنًا صغيرًا يتضمّن بعض الحالات ذات الصلة بمنطقنا. في
هذا السياق، لا يهمّنا سوى القصة الحالية. في ترميز 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
}
}
}
جرّبه الآن
- لمعاينة الموقع الإلكتروني، اضغط على عرض التطبيق. ثم اضغط على ملء الشاشة .
الخاتمة
هذه هي المتطلّبات التي كانت لديّ بشأن المكوّن. لا تتردد في الاستفادة من هذه الميزة وتطويرها باستخدام البيانات، واستخدامها بشكل عام بما يناسبك.