本程式碼研究室將說明如何在網頁上建構 Instagram 限時動態等體驗。建立元件的過程中,我們將先從 HTML、CSS 和 JavaScript 開始建構。
請參閱我的網誌文章「建構故事元件」,瞭解建構這個元件時所進行的漸進式強化項目。
設定
- 按一下「Remix to Edit」,讓專案可供編輯。
- 開啟
app/index.html
。
HTML
我一律希望使用語意 HTML。由於每位好友可以擁有任意數量的故事,所以我認為對每位好友使用 <section>
元素,並為每個故事使用 <article>
元素很有意義。讓我們從頭開始吧!首先,我們需要一個用於精選故事元件的容器
在 <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 元素放入標記中為止。
將以下 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 捲動貼齊點」和「滑鼠遊標懸停行為」一節。
這會讓父項容器和子項同意捲動貼齊,因此現在要處理這個部分。請將下列程式碼加入 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
現在只要設定故事項目本身的樣式即可。
我們先前提過,每個 <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
,我們會將玻璃故事轉換為視窗,並竊取沒有其他的使用者互動行為。選在利弊之後
還得在 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
}
}
}
立即體驗
- 如要預覽網站,請按下「查看應用程式」,然後按下「全螢幕」圖示 。
結語
這個單元已完整滿足我對這項元件的需求。您不妨根據這些資料建構服務,運用資料發揮資料價值,大致上就是由您做主!