這系列文章會記錄 JS30 當次挑戰時使用到的語法和相關知識。
JS30 官方有提供所有製作網頁的資源,不需要任何事前準備,就能無痛開始撰寫 JS,寫完之後還有 JS30 作者提供的解答,不知道怎麼下手時可以參考、寫完之後也能了解更多解法,改善自己的寫法。
本次的挑戰是「JavaScript Drum Kit」,需要達成的目標是:
「使用者按下鍵盤特定按鍵,按鍵會發光、同時播放相對應的音效。」
根據上面這個目標,大略知道該挑戰牽涉到以下面向:
這個挑戰有個小細節,就是每次按下鍵盤後,都必須將音效時間軸歸零,這樣在每次觸發時才會從頭開始播放音效。
網頁中引入音效可以使用 <audio>
元素,原來 audio 本身有 API 可以進行操作,相當方便,以下就來了解看看吧。
<audio> 標籤
<audio src="filePath"></audio>
常見屬性
autoplay
:自動播放音檔controls
:出現控制條loop
:循環播放有看到一些文章會建議搭配 source
標籤,避免某些音檔格式瀏覽器不支援,而發生預期外的效果。如果 source 的第一個格式不支援,就會向下讀取其他項目,通常 mp3 會是最普及的格式,可以放在最後。
<audio autoplay>
<source src="xxx.ogg">
<source src="xxx.mp3">
你的瀏覽器不支援 HTML5 音訊格式。
</audio>
audio 相關 API
const audio = document.querySelector("audio")
//開始播放
audio.play();
//暫停播放
audio.pause();
//將音檔秒數歸零
//audio 物件還有許多屬性,幾乎完全可以透過 JS 控制 audio
audio.currentTime = 0;
語意標籤,表示「鍵盤輸入按鍵」,預設樣式就會將字型變為等寬字(monospace)
附上此次實作結果
參考作者的寫法,發現在取得 DOM 元素時就先篩選出符合的元素,但筆者在這邊都使用 forEach 迴圈做篩選。
下方比較兩者寫法,可以看出筆者寫法較為冗長,可讀性較低,且跑迴圈會佔較多效能。
const keys = document.querySelectorAll(".key");
const filter = function (list, userKeyboard) {
let el;
list.forEach((item) => {
if (item.dataset.key === userKeyboard) {
el = item;
}
});
return el;
};
const playAudio = function (e) {
const audios = document.querySelectorAll("audio");
const userKeyboard = String(e.keyCode);
if (filter(keys, userKeyboard)) {
filter(keys, userKeyboard).classList.add("playing");
filter(audios, userKeyboard).currentTime = 0;
filter(audios, userKeyboard).play();
}
};
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
console.log(e.keyCode);
if (!audio) return;
key.classList.add("playing");
audio.currentTime = 0;
audio.play();
}
製作完成後,發現有以下幾點可以做調整:
keydown
,當使用者按著按鍵不放時,事件會連續觸發,到某個程度時按鈕樣式就不再變動(簡言之「看起來」就像壞掉了XD);若鍵盤事件使用 keyup
,雖然解決了事件重複觸發的問題,但筆者認為使用起來不符合預期效果,一般使用者應該會預期按下去的瞬間就發出音效,變成放開按鍵才發出音效似乎不夠即時一、重複點擊按鈕時,每次都會改變按鈕樣式
這部分是參考作者提供的解法,是使用transitionend
事件,監聽帶有 transition 屬性的元素,當 transitioned 結束時會觸發事件,有兩個情況下無法觸發該事件:
transition-property
屬性function removeTransition(e) {
console.log(e.propertyName);
if (e.propertyName !== "transform") return;
e.target.classList.remove("playing");
}
作者是透過 propertyName 找到 transform 屬性,但似乎會導致重複觸發時按鈕樣式就不會再變動,不確定原因為何,目前想到的解決方式可往下看第二點。
二、在使用 keydown 的情況下,可以連續觸發樣式,且保持運作正常
解法1: 直接在 keyup 事件上綁定「清除按鈕樣式」的處理器,有點像買保險的感覺XD
let keys = document.querySelectorAll(".key");
const removeSound = function (e) {
keys.forEach((item) => {
item.classList.remove("playing");
});
};
window.addEventListener("keyup", removeSound);
解法2: transitionend
事件的 propertyName,找到除了 transform 以外的其他屬性,就能正常運作,這部分原因尚待釐清。
目前想到的解法是這樣,如果有更好的再補上。
發現下載後的檔案都有 favicon(瀏覽器頁籤會顯示的 icon),意外發現這個網站,可以自行替換掉後面的表情符號,蠻有趣的XD
今天就介紹到這裡,若有錯誤歡迎指正,也歡迎大家分享自己的看法。