Lớp học lập trình này hướng dẫn bạn cách xây dựng một trải nghiệm như Instagram Stories trên web. Chúng ta sẽ tạo thành phần trong quá trình tạo, bắt đầu với HTML, sau đó là CSS, rồi đến JavaScript.
Hãy xem bài đăng Xây dựng thành phần Stories trên blog của tôi để tìm hiểu về các tính năng nâng cao tăng dần trong khi xây dựng thành phần này.
Thiết lập
- Nhấp vào Phối lại để chỉnh sửa để có thể chỉnh sửa dự án.
- Mở
app/index.html
.
HTML
Tôi luôn cố gắng sử dụng HTML ngữ nghĩa.
Vì mỗi người bạn có thể có số lượng câu chuyện bất kỳ, nên tôi nghĩ việc sử dụng
Phần tử <section>
cho mỗi người bạn và phần tử <article>
cho mỗi câu chuyện.
Tuy nhiên, hãy bắt đầu lại từ đầu. Trước tiên, chúng ta cần một vùng chứa cho
Câu chuyện.
Thêm phần tử <div>
vào <body>
:
<div class="stories">
</div>
Thêm một số phần tử <section>
để đại diện cho bạn bè:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Thêm một số phần tử <article>
để thể hiện tin bài:
<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>
- Chúng tôi đang sử dụng dịch vụ hình ảnh (
picsum.com
) để hỗ trợ các câu chuyện nguyên mẫu. - Thuộc tính
style
trên mỗi<article>
là một phần của quá trình tải phần giữ chỗ mà bạn sẽ tìm hiểu thêm trong phần tiếp theo.
CSS
Nội dung của chúng tôi đã sẵn sàng để thể hiện phong cách riêng. Hãy biến những chiếc xương đó thành thứ mà mọi người sẽ muốn tương tác. Hôm nay, chúng tôi sẽ ưu tiên thiết bị di động.
.stories
Đối với vùng chứa <div class="stories">
, chúng ta muốn có một vùng chứa cuộn ngang.
Chúng tôi có thể đạt được điều này bằng cách:
- Đặt vùng chứa thành Lưới
- Cài đặt từng thành phần con lấp đầy dữ liệu theo dõi hàng
- Làm cho chiều rộng của mỗi trẻ làm chiều rộng của khung nhìn trên thiết bị di động
Lưới sẽ tiếp tục đặt các cột mới rộng 100vw
ở bên phải cột trước
một, cho đến khi thẻ đó được đặt tất cả các phần tử HTML vào mã đánh dấu của bạn.
Thêm CSS sau vào cuối app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Giờ đây, khi chúng ta có nội dung vượt ra ngoài khung nhìn, đã đến lúc nói rằng
vùng chứa cách xử lý. Thêm các dòng mã được đánh dấu vào bộ quy tắc .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;
}
Chúng ta muốn cuộn ngang, vì vậy chúng ta sẽ đặt overflow-x
thành
auto
Khi người dùng cuộn, chúng ta muốn thành phần này nằm nhẹ nhàng ở câu chuyện tiếp theo,
nên chúng ta sẽ dùng scroll-snap-type: x mandatory
. Đọc thêm về thông tin này
CSS trong CSS Scroll Snap Points (Điểm chụp nhanh của CSS Cuộn)
và hành vi cuộn qua
trong bài đăng trên blog của tôi.
Cả vùng chứa mẹ và phần tử con đều phải đồng ý chụp nhanh, do đó
hãy xử lý vấn đề đó ngay bây giờ. Thêm mã sau vào cuối app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Ứng dụng của bạn chưa hoạt động được, nhưng video dưới đây cho thấy điều gì sẽ xảy ra khi
scroll-snap-type
đã bật và tắt. Khi được bật, mỗi chiều ngang
di chuyển để chuyển đến tin bài tiếp theo. Khi bị tắt, trình duyệt sẽ sử dụng
hành vi cuộn mặc định.
Điều đó sẽ giúp bạn cuộn qua bạn bè của mình, nhưng chúng tôi vẫn có một vấn đề cùng những câu chuyện cần giải quyết.
.user
Hãy tạo một bố cục trong phần .user
để sắp xếp những câu chuyện trẻ em
vào đúng vị trí. Chúng ta sẽ sử dụng một thủ thuật xếp chồng hữu ích để giải quyết vấn đề này.
Về cơ bản, chúng ta đang tạo lưới 1x1 trong đó hàng và cột có cùng một Lưới
bí danh của [story]
và mỗi mục trong lưới tin bài sẽ thử và xác nhận quyền sở hữu không gian đó,
dẫn đến ngăn xếp.
Thêm mã được đánh dấu vào bộ quy tắc .user
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Thêm bộ quy tắc sau vào cuối app/css/index.css
:
.story {
grid-area: story;
}
Giờ đây, bạn sẽ không còn có thể dùng vị trí tuyệt đối, số thực độ chính xác đơn hoặc các lệnh bố cục khác một phần tử nằm ngoài luồng, chúng ta vẫn đang trong luồng. Ngoài ra, hầu như không có mã nào, hãy nhìn vào đó! Phần này sẽ được trình bày chi tiết hơn trong video và bài đăng trên blog.
.story
Bây giờ, chúng ta chỉ cần tạo kiểu cho chính mục trong câu chuyện.
Trước đó, chúng tôi đã đề cập rằng thuộc tính style
trên mỗi phần tử <article>
là một phần của
kỹ thuật tải phần giữ chỗ:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Chúng ta sẽ sử dụng thuộc tính background-image
của CSS, cho phép chỉ định
nhiều hình nền. Chúng ta có thể sắp xếp chúng theo thứ tự để người dùng
ảnh ở trên cùng và sẽ tự động hiển thị khi tải xong. Người nhận
bật tính năng này, chúng tôi sẽ đặt URL hình ảnh vào thuộc tính tùy chỉnh (--bg
) và sử dụng nó
trong CSS của chúng tôi để phân lớp với trình giữ chỗ đang tải.
Trước tiên, hãy cập nhật bộ quy tắc .story
để thay thế hiệu ứng chuyển màu bằng hình nền
sau khi tải xong. Thêm mã được đánh dấu vào bộ quy tắc .story
:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Việc đặt background-size
thành cover
sẽ đảm bảo không có không gian trống trong
khung nhìn vì hình ảnh của chúng tôi sẽ lấp đầy hình ảnh đó. Xác định 2 hình nền
cho phép chúng ta kéo một thủ thuật web CSS gọn gàng có tên là loading tombstone:
- Hình nền 1 (
var(--bg)
) là URL chúng tôi đã truyền nội tuyến trong HTML - Hình nền 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
là một dải chuyển màu) hiển thị trong khi URL đang tải
CSS sẽ tự động thay thế dải chuyển màu bằng hình ảnh sau khi hình ảnh được tải xuống xong.
Tiếp theo, chúng ta sẽ thêm CSS để loại bỏ một số hành vi, giúp trình duyệt hoạt động nhanh hơn.
Thêm mã được đánh dấu vào bộ quy tắc .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
ngăn người dùng vô tình chọn văn bảntouch-action: manipulation
hướng dẫn trình duyệt rằng các hoạt động tương tác này sẽ được coi là sự kiện chạm, điều này giúp trình duyệt không phải quyết định xem bạn có nhấp vào URL hay không
Cuối cùng, hãy thêm một CSS để tạo hiệu ứng chuyển đổi giữa các câu chuyện. Thêm
mã được đánh dấu vào bộ quy tắc .story
của bạn:
.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;
}
}
Lớp .seen
sẽ được thêm vào một câu chuyện cần thoát.
Tôi đã nhận được hàm gia tốc tuỳ chỉnh (cubic-bezier(0.4, 0.0, 1,1)
)
từ Easing của Material Design
hướng dẫn (cuộn đến phần Tăng tốc theo tỷ lệ).
Nếu để mắt quan tâm, có thể bạn đã nhận thấy pointer-events: none
khai báo và hiện đang gãi đầu. Tôi muốn nói rằng đây là lựa chọn duy nhất
nhược điểm của giải pháp này cho đến thời điểm này. Chúng ta cần hàm này vì phần tử .seen.story
sẽ ở trên cùng và sẽ nhận được các lần nhấn, mặc dù tiện ích này không nhìn thấy được. Bằng cách đặt
pointer-events
đến none
, chúng ta biến câu chuyện bằng kính thành một khung cửa sổ và không trộm cắp
nhiều tương tác của người dùng hơn. Không tệ, đánh đổi cũng không quá khó quản lý
trong CSS của chúng tôi ngay bây giờ. Chúng tôi không kết hợp z-index
. Tôi thấy ổn về điều này
.
JavaScript
Tính năng tương tác của thành phần Stories khá đơn giản đối với người dùng: hãy nhấn vào phải để tiến, nhấn vào bên trái để quay lại. Những điều đơn giản mà người dùng thường sử dụng công việc khó khăn đối với nhà phát triển. Nhưng chúng tôi sẽ xử lý rất nhiều việc.
Thiết lập
Để bắt đầu, hãy tính toán và lưu trữ nhiều thông tin nhất có thể.
Thêm mã sau vào app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
Dòng JavaScript đầu tiên của chúng tôi lấy và lưu trữ tham chiếu đến HTML chính gốc của phần tử. Dòng tiếp theo tính toán vị trí nằm giữa phần tử, vì vậy chúng ta có thể quyết định xem một lần nhấn là tiến hay lùi.
Tiểu bang
Tiếp theo, chúng ta tạo một đối tượng nhỏ có trạng thái nào đó liên quan đến logic của mình. Trong phần này
trong trường hợp đó, chúng tôi chỉ quan tâm đến tin bài hiện tại. Trong mã đánh dấu HTML, chúng tôi có thể
truy cập bằng cách lấy người bạn đầu tiên và câu chuyện gần đây nhất của họ. Thêm đoạn mã được đánh dấu
vào app/js/index.js
của bạn:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Trình nghe
Bây giờ, chúng ta đã có đủ logic để bắt đầu lắng nghe các sự kiện của người dùng và chỉ dẫn các sự kiện đó.
Chuột
Hãy bắt đầu bằng cách theo dõi sự kiện 'click'
trên vùng chứa story của chúng ta.
Thêm mã được đánh dấu vào 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')
})
Nếu một lượt nhấp xảy ra và lượt nhấp đó không nằm trong phần tử <article>
thì chúng tôi sẽ bảo lãnh và không làm gì cả.
Nếu đó là một bài viết, chúng ta sẽ giữ vị trí nằm ngang của chuột hoặc ngón tay bằng
clientX
. Chúng ta chưa triển khai navigateStories
, nhưng đối số mà
xác định hướng đi. Nếu vị trí người dùng đó là
lớn hơn trung vị, nên chúng ta biết cần chuyển đến next
, nếu không
prev
(trước).
Bàn phím
Bây giờ, hãy nghe thao tác nhấn bàn phím. Nếu nhấn Mũi tên xuống, chúng ta sẽ di chuyển
đến next
. Nếu đó là Mũi tên lên, chúng ta sẽ chuyển đến prev
.
Thêm mã được đánh dấu vào 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')
})
Khám phá câu chuyện
Đã đến lúc xử lý logic kinh doanh độc đáo của những câu chuyện và trải nghiệm người dùng mà chúng tạo ra nổi tiếng. Việc này có vẻ phức tạp và phức tạp, nhưng tôi nghĩ nếu bạn thực hiện bằng cách , bạn sẽ thấy giao diện này khá dễ hiểu.
Trước tiên, chúng tôi lưu trữ một số bộ chọn giúp chúng tôi quyết định xem có cuộn đến kết bạn hoặc giới thiệu/ẩn một câu chuyện. Vì HTML là nơi chúng tôi làm việc, nên chúng tôi sẽ truy vấn câu chuyện về sự hiện diện của bạn bè (người dùng) hoặc câu chuyện (câu chuyện).
Những biến này sẽ giúp chúng ta trả lời các câu hỏi như "đã biết câu chuyện x, thì tiếp theo" nghĩa là chuyển sang câu chuyện khác từ chính người bạn này hay sang một người bạn khác?" Tôi làm điều đó bằng cách sử dụng cái cây cấu trúc mà chúng tôi xây dựng, tiếp cận các bậc cha mẹ và con cái họ.
Thêm mã sau vào cuối 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
}
Dưới đây là mục tiêu về logic kinh doanh của chúng tôi, gần với ngôn ngữ tự nhiên nhất có thể:
- Quyết định cách xử lý thao tác nhấn
- Nếu có tin bài tiếp theo/trước: hiển thị tin bài đó
- Nếu đó là câu chuyện cuối cùng/đầu tiên của người bạn đó: hãy giới thiệu một người bạn mới
- Nếu không có sự việc nào theo hướng đó: không làm gì cả
- Lưu trữ câu chuyện hiện tại mới vào
state
Thêm mã được làm nổi bật vào hàm 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
}
}
}
Dùng thử
- Để xem trước trang web, hãy nhấn vào Xem ứng dụng. Sau đó nhấn Toàn màn hình .
Kết luận
Đó là bản tóm tắt cho nhu cầu của tôi với thành phần này. Thoả sức phát triển nó, thúc đẩy nó bằng dữ liệu và nói chung làm cho nó của bạn!