Chapter1 - 补充 CORS + autoplay政策 + requestAnimeFrame致命缺点

前言

事情是这样的,其实我都是事先写好code,当天才写文章,本来昨天有三个重要的点要额外拉出来讲的,就这麽被我忘了!担心有人真的很认真照着步骤跟着做了,却在上回踩到坑,只因为我昨天赶着去吃饭忘了补充XD(抱歉抱歉)。

CORS

首先就是,如果你没有运行一个server的话,你很有可能看到以下这段讯息:

MediaElementAudioSource outputs zeroes due to CORS access restrictions

什麽意思呢?还记得我们前天提到的这个source吧!这段讯息就是跟你说「你想要使用这段音乐当作音源,开始混音有的没的,但是根据CORS,限制你使用这段音源」,因此这个节点的音讯输出为零,等同没有。

为什麽不允许?

这几天我特意没有提到的CORS(跨来源资源共用),就是为了一次讲清楚,简单来讲就是浏览器不准你跑到别人的地盘偷人家的东西,只允许你借,这个比喻就是说,本来为了浏览器运行方便,你可以透过连结的方式,去借人家的东西,比方说图片、音乐,然而这些档案只能让你浏览用,不会真的给你完整的档案,假如想要得到原始的档案(偷的概念),浏览器会阻挡你这个行为。

而浏览器们共同遵守的规则,就是CORS,规范并提供你方法去"合法"的取得跨来源(cross-origin)的资源,让大家彼此分享共用,因此它需要你情我愿,除了你要送出一个正式的请求以外,对方还要同意分享,进一步的内容跟方法这边就不多聊了(跟本次主题无关&又是一大篇幅)。

  • 同源 same-origin
  • 跨来源 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 除错机制

上回谈到,用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


<<:  一分钟的思考,远胜於一个小时的谈话。

>>:  [重构倒数第18天] - 我如何再Vue里面使用axios有效管理API

[30天 Vue学好学满 DAY6] 计算属性(Computed)

计算属性(Computed) 无传入值 具回传值(return) 对来源属性进行操作-> 触发...

Ruby on Rails 方法的存取控制

如果你曾经在别的程序语言写过OOP,你也许对类别的方法存取限制不会太陌生。类别的方法存取限制常见的主...

Reactive programming

在上一篇中我们完成了 StickyNote 的 UI 跟 Model 的部分,後面的章节将有很大的一...

[Day7] Flutter - 堆叠布局 ( Stack、Positioned )

前言 Hi, 我是鱼板伯爵今天要教大家 Stack(堆叠) 和 Positioned(位子),Sta...