事情是這樣的,其實我都是事先寫好code,當天才寫文章,本來昨天有三個重要的點要額外拉出來講的,就這麼被我忘了!擔心有人真的很認真照著步驟跟著做了,卻在上回踩到坑,只因為我昨天趕著去吃飯忘了補充XD(抱歉抱歉)。
首先就是,如果你沒有運行一個server的話,你很有可能看到以下這段訊息:
MediaElementAudioSource outputs zeroes due to CORS access restrictions
什麼意思呢?還記得我們前天提到的這個source吧!這段訊息就是跟你說「你想要使用這段音樂當作音源,開始混音有的沒的,但是根據CORS,限制你使用這段音源」,因此這個節點的音訊輸出為零,等同沒有。
這幾天我特意沒有提到的CORS(跨來源資源共用),就是為了一次講清楚,簡單來講就是瀏覽器不準你跑到別人的地盤偷人家的東西,只允許你借,這個比喻就是說,本來為了瀏覽器運行方便,你可以透過連結的方式,去借人家的東西,比方說圖片、音樂,然而這些檔案只能讓你瀏覽用,不會真的給你完整的檔案,假如想要得到原始的檔案(偷的概念),瀏覽器會阻擋你這個行為。
而瀏覽器們共同遵守的規則,就是CORS,規範並提供你方法去"合法"的取得跨來源(cross-origin)的資源,讓大家彼此分享共用,因此它需要你情我願,除了你要送出一個正式的請求以外,對方還要同意分享,進一步的內容跟方法這邊就不多聊了(跟本次主題無關&又是一大篇幅)。
對,也不對,還記得我們有預設音樂的路徑,用的是相對路徑嗎?如下所示
<audio id="Music" controls>
<source src="../music/Brothers Unite.mp3">
</audio>
其實這會被算是cross-origin,假設這個mp3檔案在D槽好了,這個動作就好像是你去跟D槽拿檔案,但是D槽沒有同意這件事,因為D槽沒有開一個server做CORS的相關設定,表明說願意分享檔案。也就是說,你在嘗試偷你自己電腦裡的東西XD,這樣講或許會有點混淆,其實問題在於什麼叫做"別人的地盤"(跨來源),主要的依據是用domain來看,如果你有運行一個server,那顯然會有一個domain(比如127.0.0.1:5050/),檔案全部放在裡面(同來源same-origin),就不會有上述問題,這些就是你自己的檔案,可以自由使用。
或者你也可以這樣想,有個駭客設計了一個網頁,而你用公司的電腦打開了,然後這位駭客恰巧還知道你公司機密文件放在D槽的某某處,那是不是就能直接被他偷走上傳到雲端?答案是否定的,因為對於駭客的網頁來說,你的D槽是"別人的地盤"cross-origin,所以瀏覽器根據CORS政策,限制他直接取得你的檔案。
這樣講就很好懂了吧?就明白瀏覽器對於這方面的設計原理,他就像色情守門員一樣,會給予一些限制,當你熟悉它之後,就不會覺得是限制了,那這個例子中,駭客如何拿到你的檔案呢?他可以設計一個我們開篇設計的上傳按鈕,然後讓你親自上傳檔案,由於這是瀏覽器內建的API,而你也照正常操作上傳了,那他就可以取得那份檔案,複製一個帶走,不過,駭客是不會知道你從哪個資料夾上傳檔案的,它只會拿到一個開頭為blob的鑰匙,來向你取用那個你上傳的檔案,說到這,是不是銜接回開篇講的 URL.createObejectURL 了呢?這樣觀念更清楚了吧!
近年來隨著google對使用者體驗的重視,不只是安全性的要求、網頁讀取速度等等,也包括了給予使用者預期的回饋,像是靜音的autoplay呀,或是同意使用cookie,都是為了照顧使用者,如果前四篇文章你很認真地照著我的教學走了,你肯定會在console看到這段訊息。
The AudioContext was not allowed to start.
It must be resumed (or created) after a user gesture on the page.
解決方法也很簡單,就是在當初設計的Play按鈕的點擊事件中,加入resume:
let audioControl = function(){
// 取得該元件ID的值
let ID = this.attributes.id.nodeValue; // 'Play' or 'Pause'
if(ID == "Play"){
audioCtx.resume(); // AudioContext
audio.play();
}
else{
audio.pause();
}
}
稍微去了解一下,就可以知道,其實就是關於AudioContext這個物件的建立,還是需要使用者有動作,才能給予回饋,不能說網頁一進來,我就直接放音樂開party讓畫面嗨起來,還是得讓使用者跟頁面上的元件做互動後,有認知到會有回饋,如此一來,才能優化整個體驗,因此如果AudioContext在使用者未做互動的階段建立的,它會暫時陷入一個懸停狀態(suspend),需要讓他回復(resume)才能使用。
上回談到,用requestAnimeFrame來做一個很簡單的動畫循環機制,然而,用那樣的動畫結構雖然很直觀,卻有一個致命的問題,當時我們沒有設計恰當的中斷機制(實際上我們不希望中斷,讓使用者沒得體驗)和報錯方法,雖然這是第八章(最後一章)才會深入討論的內容,我們還是先稍微補強一下這個結構吧!
function AnimationLoop(){
try{
Resize("#game-box", canvas, context, '#000');
Redraw();
}catch(e){
window.myErrorMessage = e.message;
}
requestAnimationFrame(AnimationLoop);
}
在上次的AnimationLoop中用try、catch的方法,原因是如果一不小心有代碼寫錯,不管是少了括號、資料型別弄錯、語法寫錯、名稱寫錯,這個requestAnimationFrame都會一直無情的呼叫,然後導致瀏覽器的console以每秒60次的方式報錯,因為瀏覽器throw error的機制很吃效能,你會發現一秒根本處理不完60次,最後這個請求就會一直不斷堆積,直到瀏覽器當掉,當你要重新整理或關掉瀏覽器,都要等所有的報錯訊息跑完為止(短則數秒長則數十秒),因此直接在catch階段把它攔截下來,就能有效解決這個問題,這邊為了方便除錯,則是將錯誤訊息存到了window物件下,這樣直接在console裡就可以確認有沒有最新的錯誤訊息了。
window.myErrorMessage // undefined
- 當try裡面的程式碼正常執行時,就不會進到catch裡面對該變數賦值,因此會顯示undefined。
- 為了避免覆寫到window物件的屬性,影響原本的結構,這樣也可以確保該名稱沒被使用。
也可能有人覺得不夠嚴謹,可以改成用一個陣列去裝一連串的錯誤訊息:
window.myErrorMessage = new Array(30).fill("OK");
try{
//略
}catch(e){
window.myErrorMessage.shift();
window.myErrorMessage.push(e.message);
}
本來這邊想寫正文開始,但沒想到已經3000多字了XD,看來第二章要明天開始了!FIGHTING~~
昨天晚上8:30就早早打完文章,所以很開心地去吃了滷味、回來整理了一下第二章的大綱,早早的去睡了,結果剛剛驚醒,想起我第一章節留下了這三個大坑,就直接收尾了,怕不是要害大家debug到瘋掉,實在是不應該如此誤人子弟,就加緊腳步趕了這篇出來,看在我這麼努力的份上,給一個讚應該,要求不高吧?XD