效果預覽
按下右側的“點擊預覽”按鈕可以在當前頁面預覽,點擊鏈接可以全屏預覽。
https://codepen.io/comehope/pen/byvRxB
可交互視頻
此視頻是可以交互的,你可以隨時暫停視頻,編輯視頻中的代碼。
請用 chrome, safari, edge 打開觀看。
視頻1: https://scrimba.com/p/pEgDAM/cR4gpGsa
視頻2: https://scrimba.com/p/pEgDAM/czNp3MUZ
源代碼下載
每日前端實戰系列的全部源代碼請從 github 下載:
https://github.com/comehope/front-end-daily-challenges
代碼解讀
大家是否見過 “i18n”、“a11y” 這樣的英文單詞?它們其實是一些單詞的縮寫,“i18n” 代表的是 “internationalization”(國際化),“a11y” 代表的是 “accessibility”(可訪問性),因爲包含的字母太多了,所以縮寫時只保留頭尾的字母,再把餘下的字符個數寫在中間,這種寫法稱爲“Numeronym”,我把它翻譯成“數略詞”。據說最長的單詞是 “pneumonoultramicroscopicsilicovolcanoconiosis”,由 45 個字母組成,意思是一種肺部疾病。
本項目將製作一個交互動畫效果,令其在單詞原詞和“數略詞”之間切換。整個項目分成二個步驟開發,第一步先實現一個固定單詞的交互動畫,第二步改寫爲能自動處理任意的單詞,然後擴展應用到多個單詞上。
一、一個固定單詞的交互動畫
dom 結構如下,最外側的容器名爲 .container
,其中包含一個名爲 .word
的 <div>
元素,它代表一個單詞,它的子元素是 4 個 <p>
元素,分別代表單詞的第1個字母、中間字符的個數(.middle.short
)、中間的若干字符(.middle.long
)、單詞的最後1個字母。因爲動畫時將交替顯示“中間字符的個數”和“中間的若干字符”,所以爲它們設置了特殊類名,以便在隨後的 css 代碼中引用它們,當要同時選擇它們時,就用它們共同的類名 middle
,當要分別選擇時,就指定它們各自的類名 short
和 long
:
<div class="container">
<div class="word">
<p>i</p>
<p class="middle short">
<span>18</span>
</p>
<p class="middle long">
<span>nternationalizatio</span>
</p>
<p>n</p>
</div>
</div>
令容器居於頁面正中:
body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image: linear-gradient(bisque, lightcyan);
}
讓 4 個 <p>
標籤包含的文字橫向排列在容器中部:
.container {
width: 100%;
}
.word {
font-size: 35px;
font-family: monospace;
display: flex;
justify-content: center;
}
把 2 箇中間的 <p>
元素的文字上色,突出顯示它們:
.middle {
color: tomato;
}
接下來製作交互動畫效果。
先把中間的若干字符隱藏起來,只顯示中間字符的個數,在 html 代碼中找到 .middle.long
元素,爲它增加一個 hide
樣式類:
<p class="middle long hide">
在 css 中將 .middle.hide
元素的寬度設置爲 0
,並且不顯示超出容器的部分:
.middle {
overflow: hidden;
}
.middle.hide {
width: 0;
}
令鼠標懸停在單詞上時,鼠標指針變成一隻手,提示用戶此時可以點擊:
.word:hover {
cursor: pointer;
}
爲 .word
元素增加鼠標點擊事件,當單詞被點擊時,2 箇中間元素分別切換 hide
類,交替顯示兩者中的一個元素,這些代碼寫在一個名爲 initWordElement()
的方法中。在頁面載入時將執行 init()
方法,再在其中調用 initWordElement()
方法。沒有讓 window.onload
直接執行 initWordElement()
方法,而是通過 init()
來調用,是因爲在頁面初始化階段還會要做一些其他操作,後面還會逐漸充實 init()
方法:
window.onload = init
function init() {
let el = document.querySelector('.word')
initWordElement(el)
}
function initWordElement(el) {
let middles = el.querySelectorAll('.middle')
el.onclick = () => middles.forEach(m => m.classList.toggle('hide'))
}
現在,在頁面上多次點擊單詞,能看到單詞的中間部分不斷切換了,不過這時還沒有動畫效果,接下來爲切換增加緩動效果。
先爲中間的 2 個元素設置寬度,這 2 個值是手工測量得到的,這不是最終的寫法,後面我們會改成用腳本自動測量得到元素的寬度,不過因爲現在我們要解決的是動畫效果,所以先臨時硬編碼一下:
加緩動:
.middle {
transition: 1s;
}
.middle.short {width: 42px;}
.middle.long {width: 378px;}
設置緩時長爲 1 秒:
.middle {
transition: 1s;
}
現在,點擊單詞時的切換效果,已經有了動畫過程,接下來細化動畫效果。
切換可以理解由 2 個動作組成:一箇中間元素消失,另一箇中間元素出現,通過增加緩動延時來實現這個效果:
.middle {
transition: 1s;
transition-delay: 1s;
}
.middle.hide {
transition: 1s;
}
現在,當改變元素寬度時,是以元素的左側爲起點改變寬度的,不夠漂亮,我們把它改成以中間爲中點改變寬度,這樣當元素變寬時,就向兩側延伸,當元素變窄時,就向中間收縮:
.middle {
position: relative;
}
.middle span {
position: absolute;
transform: translateX(0);
transition: 1s;
transition-delay: 1s;
}
.middle.hide span {
transform: translateX(-50%);
transition: 1s;
}
接下來修改緩動時長,由 1s
縮短爲 0.5s
,也就是令動畫速度加快一倍。爲了能方便調試和維護,我們把時長的值定義爲變量 --t
:
.word {
--t: 0.5s;
}
.middle {
transition: var(--t);
transition-delay: var(--t);
}
.middle span {
transition: var(--t);
transition-delay: var(--t);
}
.middle.hide {
transition: var(--t);
}
.middle.hide span {
transition: var(--t);
}
至此,動畫效果製作完成。
二、擴展應用到多個單詞
“數略詞”有很多,爲了能夠一次展示多個單詞,我們將對現有的程序進行擴展。
先引入 lodash 庫,我們將利用它提供的一個模板函數來處理 html 模板:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
擴展 dom 結構,.container
容器中將包含不止一個 .word
元素,而是多個 .word
元素了。
創建一個 html 模板,它的內容是 .word
元素的代碼,其中的第一個字母、中間字符個數、中間的若干字符、最後一個字母,這些內容在模板中分別用變量 first
、middleLength
、middle
、last
表示:
<script type="text/x-templ" id="template">
<p><%= first %></p>
<p class="middle short">
<span><%= middleLength %></span>
</p>
<p class="middle long hide">
<span><%= middle %></span>
</p>
<p><%= last %></p>
</script>
而原 .container
元素中的內容都要刪除掉,以便動態填充:
<div class="container"></div>
寫一個名爲 getWordObject()
的獲取單詞對象的函數,輸入是一個單詞,如“internationalization”,輸出是一個對象,這個對象的屬性與 html 模板中的變量相對應:
function getWordObject(w) {
return {
first: w.slice(0, 1),
last: w.slice(-1),
middle: w.slice(1, -1),
middleLength: w.slice(1, -1).length,
}
}
接下來寫一個名爲 createWordElement()
的方法,用於創建一個 .word
元素,在這個方法中使用了 lodash 的 _.template()
模板函數。該方法的輸入是一個單詞,將傳遞給 getWordObject()
函數:
function createWordElement(word) {
const TEMPLATE = document.querySelector('#template').innerHTML
let el = document.createElement('div')
el.className = 'word'
el.innerHTML = _.template(TEMPLATE)(getWordObject(word))
return el
}
在負責頁面初始化的 init()
方法中調用 createWordElement()
方法,整個流程改爲先創建一個元素,然後把該元素添加到 .container
容器中,再初始化這個元素:
function init() {
let word = 'internationalization'
let el = createWordElement(word)
document.querySelector('.container').appendChild(el)
initWordElement(el)
}
現在,運行一下頁面,雖然運行效果沒有任何變化,但是 css 的屬性、頁面元素都已經變成動態生成的了。如果把 init()
方法中的 word
變量值改爲其他單詞,如 “accessibility”,頁面中就會顯示 “a11y”
了。
不過,在單詞變爲 “a11y” 之後,中間元素佔據的寬度就不正確了,這是因爲此前中間元素的寬度是硬編碼的,需要把它們改爲用腳本賦值。先刪除掉 css 中的這 2 行代碼:
/* .middle.short {width: 42px;}
.middle.long {width: 378px;} */
然後爲 .middle
元素設置寬度屬性,屬性值是名爲 --w
的變量:
.middle {
width: var(--w);
}
然後在 initWordElement()
方法中增加一行,爲變量 --w
賦值:
function initWordElement(el) {
let middles = el.querySelectorAll('.middle')
middles.forEach(m =>
m.style.setProperty('--w',
window.getComputedStyle(m.querySelector('span')).width))
el.onclick = () => middles.forEach(m => m.classList.toggle('hide'))
}
好了,現在不論把單詞換成什麼,都能合適地展現了,至此,單個單詞的動態改造就完成了。
接下來請孫大聖拔下幾根毫毛,幫我們把一個單詞變成多個單詞吧。
修改 init() 方法,刪除掉 word
變量,定義一個名爲 WORDS
的數組,遍歷這個數組,爲數組中的每個單詞創建一個 .word
元素:
function init() {
const WORDS = [
'localization',
'accessibility',
'internationalization',
'supercalifragilisticexpialidocious',
'pneumonoultramicroscopicsilicovolcanoconiosis'
]
WORDS.forEach(word => {
let el = createWordElement(word)
document.querySelector('.container').appendChild(el)
initWordElement(el)
})
}
現在,頁面上已經有 5 個單詞了,點擊那個 “p43s” 看看世界上最長的單詞吧。
最後,因爲 <p>
元素的外邊距較大,把它調整得小一點,讓縱向的幾個單詞排列得緊湊一點:
.word p {
margin: 0.3em 0;
}
大功告成!