前言
JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks、No Compilers、No Libraries、No Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。
另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。
本日目標
透過監聽 “transitionend 事件” 和 “keydown 事件”,調整 CSS 設定以及播放音效,最終建立一組爵士鼓並依照按下按鍵的不同播放不同的音效伴隨網頁上的 CSS 動畫效果。

解析程式碼
HTML 部分
基本結構是由最外層的 “keys” 包住內層9個 “key” 的巢狀結構,內層的 “key” 都有 data-key 屬性並有相異的數值,這些相異值在接下來判斷要播放哪個音檔和套用 CSS 設定到哪個 “key” 時很重要。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 
 | <div class="keys"><div data-key="65" class="key">
 <kbd>A</kbd>
 <span class="sound">clap</span>
 </div>
 <div data-key="83" class="key">
 <kbd>S</kbd>
 <span class="sound">hihat</span>
 </div>
 <div data-key="68" class="key">
 <kbd>D</kbd>
 <span class="sound">kick</span>
 </div>
 <div data-key="70" class="key">
 <kbd>F</kbd>
 <span class="sound">openhat</span>
 </div>
 <div data-key="71" class="key">
 <kbd>G</kbd>
 <span class="sound">boom</span>
 </div>
 <div data-key="72" class="key">
 <kbd>H</kbd>
 <span class="sound">ride</span>
 </div>
 <div data-key="74" class="key">
 <kbd>J</kbd>
 <span class="sound">snare</span>
 </div>
 <div data-key="75" class="key">
 <kbd>K</kbd>
 <span class="sound">tom</span>
 </div>
 <div data-key="76" class="key">
 <kbd>L</kbd>
 <span class="sound">tink</span>
 </div>
 </div>
 
 | 
補充說明1:
HTML5 新增了 data-* 自定義屬性(data attributes),讓我們能以 data- 為開頭,建立自訂的屬性和值並隨時可以讀寫在元素上的資料數值,而不會影響到整個版面。
程式碼中的 data-key 就是一個不錯的例子。
補充說明2:
<kbd> 是一個行內元素 (inline element) ,用來標示鍵盤符號。
JS 部分
首先,我們先依照按下按鍵的 keyCode 取得特定的音檔和 div 標籤。
| 12
 3
 4
 5
 
 | window.addEventListener("keydown",function(){
 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
 })
 
 | 
因為實際上會有作用的按鍵只有9個,按到沒作用的按鍵時,理論上我們應該要終止執行方法避免錯誤,所以新增 if 判斷是否成功取得音檔,沒有取得就終止方法的執行。
| 12
 3
 4
 5
 6
 
 | window.addEventListener("keydown",function(){
 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
 if(!audio)return;
 })
 
 | 
接著是播放音檔的部分,如果只單用 audio.play()的話,則在連續按下同一按鍵時,會出現聲音不連貫的效果,此時需要將每次播放音檔的時間軸都設為0,讓每次播放都是從頭開始。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | window.addEventListener("keydown",function(){
 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
 if(!audio)return;
 
 audio.currentTime = 0;
 audio.play();
 })
 
 | 
最後處理 CSS 的動畫效果,我們會套用 .playing 的 CSS 設定到播放音檔的 div 標籤上。
| 12
 3
 4
 5
 6
 
 | .playing {
 transform: scale(1.1);
 border-color: #ffc600;
 box-shadow: 0 0 1rem #ffc600;
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | window.addEventListener("keydown",function(){
 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
 if(!audio)return;
 
 audio.currentTime = 0;
 audio.play();
 
 key.classList.add('playing');
 })
 
 | 
一定時間後,我們必須拿掉 .playing 的 CSS 設定,讓 div 標籤回到未被按鍵觸發的狀態。因此我們可以在每個 key 上都註冊 “transitionend 事件” 的監聽器,並用 removeTransition() 處理該事件。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | function removeTransition(e){
 this.classList.remove('playing');
 }
 
 
 const keys = document.querySelectorAll(`.key`);
 
 keys.forEach(key => key.addEventListener('transitionend',removeTransition));
 
 | 
到這裡基本上就完成了,但是如果把 removeTransition() 的 “transitionend 事件” 印到 console,可以發現 “transitionend 事件” 不只有一個。

我們可以選擇以 transform 結束觸發的那個 “transitionend 事件”,作為移除 CSS 設定的時機點。
| 12
 3
 4
 5
 6
 
 | 
 function removeTransition(e){
 if(e.propertyName != 'transform') return;
 this.classList.remove('playing');
 }
 
 | 
整理後的完整JS如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 | function playSound(e){
 const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
 const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
 if(!audio)return;
 audio.currentTime = 0;
 audio.play();
 key.classList.add('playing');
 }
 
 
 function removeTransition(e){
 if(e.propertyName != 'transform') return;
 this.classList.remove('playing');
 }
 
 const keys = document.querySelectorAll(`.key`);
 keys.forEach(key => key.addEventListener('transitionend',removeTransition));
 
 window.addEventListener("keydown",playSound)
 
 | 
補充說明:
上面 `.key` 的用法是 JavaScript ES6 中新增的模版字符串(template literals)。
在過去我們需要用以下寫法在JS 的字串中放入 HTML 內容:
| 12
 3
 4
 5
 6
 7
 
 | 
 let component_es5 = '<header>\n'+
 '<div class="banner">\n'+
 '<img src="img1.jpg"\n'+
 '</div>\n'+
 '</header>'
 
 | 
上面的寫法相當冗長,而且不具備閱讀性。在 ES6 中我們可以用反引號快速的解決這樣的狀況:
| 12
 3
 4
 5
 6
 7
 8
 
 | let component_es6 = `
 <header>
 <div class='banner'>
 <img src="img1.jpg>
 </div>
 </header>
 `
 
 | 
– 資料來源: [筆記] JavaScript ES6 中的模版字符串(template literals)和標籤模版(tagged template)