Codelab นี้จะสอนวิธีสร้างประสบการณ์อย่าง Instagram Stories บนเว็บ เราจะสร้างคอมโพเนนต์นี้ไปเรื่อยๆ เริ่มจาก 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>
เป็นส่วนหนึ่งของการโหลดตัวยึดตำแหน่ง ซึ่งคุณจะได้เรียนรู้เพิ่มเติมในส่วนถัดไป
CSS
คอนเทนต์ของเราพร้อมให้มีสไตล์แล้ว มาเปลี่ยนกระดูกให้กลายเป็นสิ่งที่เพื่อนจะ ที่ต้องการโต้ตอบด้วย วันนี้เราจะทำงานเพื่ออุปกรณ์เคลื่อนที่เป็นหลัก
.stories
สำหรับคอนเทนเนอร์ <div class="stories">
เราต้องการคอนเทนเนอร์แบบเลื่อนแนวนอน
ซึ่งสามารถทำได้ดังนี้
- การทำให้คอนเทนเนอร์เป็นตารางกริด
- การตั้งค่าเด็กแต่ละคนให้กรอกข้อมูลในแทร็กแถว
- กำหนดความกว้างของเด็กแต่ละคนเป็นความกว้างของวิวพอร์ตของอุปกรณ์เคลื่อนที่
ตารางกริดจะยังวางคอลัมน์ที่มีความกว้าง 100vw
คอลัมน์ใหม่ไว้ทางด้านขวาของคอลัมน์ก่อนหน้า
1 จนกว่าจะวางองค์ประกอบ 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
ที่ช่วยเปลี่ยนภาพเรื่องราวของเด็กเหล่านั้นกัน
องค์ประกอบให้เข้าที่ เราจะใช้เคล็ดลับกองซ้อนที่เป็นประโยชน์เพื่อแก้ไขปัญหานี้
เรากำลังสร้างตารางกริดขนาด 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>
แต่ละรายการเป็นส่วนหนึ่งของ
เทคนิคการโหลดตัวยึดตำแหน่ง:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
เราจะใช้พร็อพเพอร์ตี้ background-image
ของ CSS ซึ่งอนุญาตให้เราระบุ
ภาพพื้นหลังมากกว่า 1 ภาพ เราสามารถจัดเรียงตามลำดับ เพื่อให้ผู้ใช้ของเรา
ภาพอยู่ด้านบนและจะแสดงโดยอัตโนมัติเมื่อโหลดเสร็จ ถึง
เปิดใช้รายการนี้ เราจะใส่ 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 เจ๋งๆ ที่เรียกว่า 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
}
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')
})
การไปยังส่วนต่างๆ ของเรื่องราว
ถึงเวลาจัดการตรรกะทางธุรกิจที่เป็นเอกลักษณ์ของเรื่องราวและ UX ที่กลายมาเป็น มีชื่อเสียง อาจจะฟังดูซับซ้อนแต่ก็ซับซ้อน แต่ผมคิดว่าถ้าคุณลองใช้ คุณจะเห็นได้ว่ารายงานนี้เข้าใจง่ายพอสมควร
อย่างแรกเลยคือเราซ่อนตัวเลือกบางรายการที่ช่วยให้ตัดสินใจได้ว่าจะเลื่อนไปที่ เพื่อน หรือแสดง/ซ่อนเรื่องราว เนื่องจาก 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
}
}
}
ลองเลย
- หากต้องการดูตัวอย่างเว็บไซต์ ให้กดดูแอป แล้วกด เต็มหน้าจอ
บทสรุป
ทั้งหมดนี้คือข้อมูลทุกอย่างที่ผมมีกับคอมโพเนนต์ คุณสามารถสร้าง ขับเคลื่อนด้วยข้อมูล และโดยทั่วไปแล้วก็ทำให้ข้อมูลเป็นของคุณ