本程式碼研究室會說明如何在網路上建構類似 Instagram Stories 的體驗。我們會逐步建構元件,從 HTML 開始,然後是 CSS,最後是 JavaScript。
請參閱我的網誌文章「建立 Stories 元件」,瞭解在建構此元件時所做的漸進式改善。
設定
- 按一下「Remix to Edit」,即可編輯專案。
- 開啟
app/index.html
。
HTML
我總是盡量使用語意式 HTML。由於每位好友可能有任意數量的短片,我認為為每位好友使用 <section>
元素,並為每部短片使用 <article>
元素,是比較有意義的做法。不過,讓我們從頭開始。首先,我們需要一個容器來放置 Stories 元件。
在 <body>
中加入 <div>
元素:
<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
) 製作故事原型。 - 每個
<article>
上的style
屬性是預留位置載入技巧的一部分,您將在下一節中進一步瞭解。
CSS
我們的內容已準備好進行樣式設定。讓我們將這些骨頭轉變成人們想互動的東西。我們今天會先處理行動版內容。
.stories
我們需要一個水平捲動的容器來放置 <div class="stories">
容器。我們可以透過以下方式達成這項目標:
格線會繼續將新的 100vw
寬度欄置於前一欄的右側,直到將所有 HTML 元素置入標記為止。
在 app/css/index.css
底部加入下列 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
時會發生什麼事。啟用後,每個水平捲動畫面都會固定在下一個短片。停用後,瀏覽器會使用預設的捲動行為。
這樣你就能瀏覽好友,但我們仍需解決 Stories 的問題。
.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
現在只需要為故事項目本身設定樣式即可。
我們先前提到,每個 <article>
元素的 style
屬性是預留位置載入技巧的一部分:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
我們將使用 CSS 的 background-image
屬性,這樣就能指定多個背景圖片。我們可以將這些圖片排序,讓使用者圖片置於頂端,並在載入完成時自動顯示。為啟用這項功能,我們會將圖片網址放入自訂屬性 (--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 個背景圖片後,我們就能使用名為「loading tombstone」的 CSS 網路技巧:
- 背景圖片 1 (
var(--bg)
) 是我們在 HTML 中傳遞的內嵌網址 - 背景圖片 2 (
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
類別新增至需要退出的故事。我從 Material Design 的緩和指南中取得自訂緩和函式 (cubic-bezier(0.4, 0.0, 1,1)
) (請捲動至「加速緩和」一節)。
如果你眼力敏銳,可能會注意到 pointer-events: none
宣告,並對此感到疑惑。我認為這是目前為止解決方案唯一的缺點。我們需要這麼做,因為 .seen.story
元素會置於頂端,並且會接收輕觸動作,即使它是不可見的。將 pointer-events
設為 none
後,我們就能將 Glass 故事變成視窗,不再竊取使用者互動。這項權衡並沒有太大問題,目前在 CSS 中也沒有太難管理。我們並未同時處理 z-index
。我仍然對此感到滿意。
JavaScript
使用者與 Stories 元件的互動方式相當簡單:輕觸右側可前進,輕觸左側則可返回。對使用者來說簡單的事情,對開發人員來說往往是艱鉅的挑戰。不過,我們會處理其中大部分內容。
設定
首先,我們會盡可能計算及儲存大量資訊。在 app/js/index.js
加入以下程式碼:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
第一行 JavaScript 會擷取並儲存主要 HTML 元素根目錄的參照。下一行會計算元素的中間位置,以便我們判斷輕觸動作是向前還是向後。
州
接下來,我們會建立一個小物件,其中包含與邏輯相關的狀態。在本例中,我們只想瞭解目前的故事。在 HTML 標記中,我們可以透過擷取第 1 位好友和他們最近的故事來存取。將醒目顯示的程式碼加入 app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
事件監聽器
我們現在有足夠的邏輯,可以開始監聽使用者事件並進行導向。
老鼠
我們先在 Stories 容器上監聽 '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')
})
動態消息瀏覽
接下來,我們將探討 Stories 的獨特商業邏輯,以及讓 Stories 聲名大噪的使用者體驗。這看起來很複雜,但我認為如果您逐行閱讀,就會發現它相當易懂。
我們會先儲存一些選取器,以便決定要捲動至好友,還是要顯示/隱藏故事。由於我們要處理 HTML,因此會查詢是否有朋友 (使用者) 或故事 (story)。
這些變數可協助我們回答以下問題:「在 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」。然後按下「Fullscreen」圖示 。
結論
以上就是我對元件的需求。歡迎您自由發揮,運用資料強化這項功能,並將其視為自己的功能!