0%

JavaScript学习笔记(16)

《JavaScript高级程序设计》第十六章,HTML5脚本编程。


之前讲过,HTML5定义了很多新标签,同时也定义了很多方便的JavaScript API。这一章差不多就是介绍下:使用跨文档消息传递、拖放API、音频与视频。

跨文档消息传递

cross-document messaging/XDM,是一种让不同域的页面之间传递消息的机制,譬如www.worx.com域中的页面与位于一个内嵌框架中的p2p.wrox.com域中的页面通信。在XDM诞生之前,要稳妥地实现这种通信比较麻烦,XDM则让我们可以稳妥而简单地实现。

利用XDM的postMessage(),可以向另一个地方传递数据(一般指包含在当前页面的<iframe>元素或者由当前页面弹出的窗口),它接收2个参数:消息内容、表示消息接收方所在域的字符串。

1
2
3
// 注:所有支持XDM的浏览器也支持iframe的contentWindow属性
const iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");

这个例子中,尝试向内嵌框架中发送一条消息,并指定框架中的文档必须来源于http.www.wrox.com域。如果来源匹配,消息会传递到内嵌框架中,否则什么也不做,使用"*"则表示可以把内容发送给来自任何域的文档,但不推荐这样做。

至于接收数据,则是会触发window对象的message事件,此事件为异步事件,相应的事件对象event包含3方面信息:

  • datapostMessage()第一个参数传入的字符串
  • origin:发送消息文档所在的域,如"http://www.wrox.com"
  • source:发送消息文档的window对象的代理(而非window对象),这个代理对象主要用于在发送上一条消息的窗口中调用postMessage()方法,如果发送消息的窗口来自同一个域,那这个对象就是window
1
2
3
4
5
6
7
8
9
10
11
12
window.addEventListener(window, "message", function(event) {

// 确保发送消息的域是已知的域
if(event.origin == "http://www.wrox.com") {

// 处理接收到的信息
processMessage(event.data);

// 可选:向来源窗口发送回执
event.source.postMessage("recieved", "http://p2p.wrox.com");
}
})

原生拖放

最早引入拖放的是IE4,当时只能拖放图像和某些文本,放置的位置也只能是文本框,后来功能逐渐扩展,直到HTML5以IE的实例为基础,制定了拖放规范。

拖放事件

拖动元素的时候会依次触发下列事件,这三个事件的目标都是被拖动的元素:

  • dragstart
    按下鼠标键并开始移动鼠标时触发。此时被拖动的元素会有一个半透明的副本跟随着鼠标,鼠标则会变成黑色的禁止符号(像这个🚫),表示不能把元素放到自己上面。
  • drag
    触发dragstart事件后随即触发,且会在拖动元素期间持续触发(类似mousemove事件在鼠标移动的全过程持续发生)。
  • dragend
    拖动停止时触发,无论元素是否被放到了有效的放置目标上。

当元素被拖动到一个有效的放置目标上时会依次触发下列事件,这四个事件的目标都是作为放置目标的元素:

  • dragenter
    有元素被拖动到放置目标上时触发(类似mouseover事件)。
  • dragover
    触发dragenter事件后随即触发,且会在被拖动元素在放置目标的范围内移动时持续触发。
  • dragleavedrop
    元素被拖出了放置目标时触发dragleave(类似mouseout事件),元素被放到了放置目标中时触发drop

dataTransfer对象

数据传输

只有简单的拖放而没有数据变化是没什么用的,为了在拖放操作时实现数据交换,IE5引入了dataTransfer对象,它是事件对象的一个属性(所以只能在拖放事件的handler里访问)。此对象有2个主要方法:getData()setData(),举例如下。

1
2
3
4
5
6
7
// 设置和接收文本数据
event.dataTransfer.setData("text", "some text");
const text = event.dataTransfer.getData("text");

// 设置和接收URL
event.dataTransfer.setData("URL", "http://www.wrox.com");
const url = event.dataTransfer.getData("URL");

IE只定义了"text""URL";HTML5则扩展成了允许指定各种MIME类型,不过也考虑到了向后兼容,"text""URL"依旧可用,只是被映射成了"text/plain""text/uri-list"。实际上,dataTransfer对象可以为每种MIME类型都保存一个值,譬如这个对象里可以同时保存一段文本和一个URL。

对于dataTransfer,浏览器默认的行为是,在拖动文本/链接/图像时浏览器调用"text"/"URL"形式的setData()保存数据到其中。当然了,也可以手动修改一下默认行为,手工保存自己要传输的数据以便将来使用。

放置行为

还可以通过dataTransfer对象来确定被拖动的元素和放置目标元素能够接收什么操作,为此需要访问它的2个属性:

  • dropEffect
    被拖动元素能够执行的放置行为,有下面4种取值。对于每一种取值,浏览器只会改变光标的显示符号,而无实际行为。要使用此属性必须在ondragenter时,针对放置目标来使用。
    • "none":不能把拖动的元素放在这里,这是除文本框外所有元素的默认值
    • "move":应该把拖动的元素移动到放置目标
    • "copy":应该把拖动的元素复制到放置目标
    • "link":放置目标会打开拖动的元素(但拖动的必须是URL)
  • effectAllowed
    表示允许拖动元素的哪种dropEffect,有下面8种取值。要使用此属性必须在ondragstart时设置。
    • uninitialized:没有给被拖动元素设置任何放置行为
    • none:被拖动元素不能有任何行为
    • all:允许任意dropEffect
    • copy:只允许值为"copy"的dropEffect
    • link:只允许值为"link"的dropEffect
    • move:只允许值为"move"的dropEffect
    • copyLink:只允许值为"copy""link"的dropEffect
    • copyMove:只允许值为"copy""move"的dropEffect
    • linkMove:只允许值为"link""move"的dropEffect

举个例子解释一下。如果想允许用户把文本框的文本拖放到一个<div>中,首先要将dropEffecteffectAllowed设置为"move"。其次,由于<div>元素放置事件的默认行为是什么也不做,文本并不会自己移动,因此,我们可以自己写代码将文本插入到<div>中,即可。如果我们把dropEffecteffectAllowed设置为"copy",那就可以在上面的基础上,不移走原始的文本框中的文本(因为是copy嘛!)。

其它成员

dataTransfer还有一些方法和属性。

  • addElement(element)
    为拖动操作添加一个元素。添加这个元素只影响数据(即增加作为拖动源而响应回调的对象),不会影响拖动操作时页面元素的外观。
  • clearData(format)
    清除以特定格式保存的数据。
  • setDragImage(element, x, y)
    指定一幅图像,用于在拖动发生时,显示在光标下方。这里的3个参数分别为:要显示的HTML元素(可以是图像;也可以是其它元素,此时显示渲染后的元素)、光标在图像中的xy坐标。
  • types
    当前保存的数据类型,一个类似数组的集合,以"text"这样的字符串保存数据类型。

自定义放置目标

浏览器会默认有些元素是可放置目标,有些不是,前者鼠标会变成虚线矩形和一个加号,后者鼠标会变成禁止符号。不过我们可以把任何元素变成有效的放置目标,只要重写dragenterdragover事件的默认行为即可。下面例子中展示了如何把一个<div>变成放置目标,这样当把元素拖动到其上时,鼠标为虚线矩形和一个加号,释放鼠标时也会触发drop事件:

1
2
3
4
5
6
7
const droptarget = document.getElementById("droptarget");
droptarget.addEventHandler("dragover", function(event) {
event.preventDefault();
});
droptarget.addEventHandler("dragenter", function(event) {
event.preventDefault();
});

Firefox中,drop事件的默认行为是打开被放置的元素的URL,比如拖放一个图片,那页面就会转向图像文件;拖放一段文本,则会导致无效URL错误。因此为了让Firefox支持正常的拖放,还要取消drop事件的默认行为。

1
2
3
droptarget.addEventHandler("drop", function(event) {
event.preventDefault();
});

自定义拖动元素

默认图像、链接、文本可拖动,不过可以自己用draggable属性来将不可拖动元素设置为可拖动的,也可以将原本可拖动的元素设置为不可拖动的。

1
2
3
4
5
<!-- 让此图像不可拖动 -->
<img src="smile.gif" draggable="false" alt="smile face">

<!-- 让此元素可拖动 -->
<div draggable="true">...</div>

媒体元素

在以前,富媒体内容为了保证跨浏览器兼容性,需要选择flash,现在HTML5新增了<audio><video>,让开发人员不必依赖任何插件就能在网页中嵌入跨浏览器的音频和视频。基本用法如下:

1
2
3
<video src="conference.mpg" id="myVideo">Video player not available</video>

<audio src="song.mp3" id="myAudio">Audio player not available</audio>

因为并非所有浏览器都支持所有媒体格式,所以可以指定多个不同的媒体来源。为此,不在标签中用src属性,而是用一个或多个<source>元素,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
<video id="myVideo">
<source src="conference.webm" type="video/webm; codecs='vp8, vorbis'">
<source src="conference.ogv" type="video/ogg; codecs='theora, vorbis'">
<source src="conference.mpg">
Video player not available
</video>

<audio id="myAudio">
<source src="song.ogg" type="audio/ogg">
<source src="song.mp3" type="audio/mpeg">
Audio player not available
</audio>

属性

列举它俩共有的属性。

一些显示属性:

  • videoWidth:表示视频(不一定是元素)宽度的整数,只适用于<video>
  • videoHeight:表示视频(不一定是元素)高度的整数,只适用于<video>
  • src:表示媒体文件来源的字符串,任何时候都可重写
  • currentSrc:表示媒体文件URL的字符串
  • totalBytes:表示当前资源所需总字节数的整数
  • controls:是否显示浏览器内置控件

一些播放属性:

  • autoplay:是否自动播放
  • loop:是否在播放完成后从头开始播放
  • paused:播放器是否暂停
  • ended:是否播放完成
  • muted:是否静音
  • volume:表示音量的浮点数,值为0.01.0
  • defaultPlaybackRate:表示默认播放速度的浮点数,默认值为1.0
  • playbackRate:表示当前的播放速度的浮点数,用户可以改变此值来让媒体播放速度变快或变慢(defaultPlaybackRate只能由开发人员修改)
  • start:表示开始播放位置的浮点数,以秒表示
  • bufferingThrottled:浏览器是否对缓冲进行了节流
  • seekable:可以搜索的时间范围
  • seeking:播放器是否正移动到媒体文件中的新位置

一些准备状态:

  • networkState:表示网络连接状态的整数(0表示空、1表示正加载、2表示正加载元数据、3表示已加载第一帧、4表示加载完成)
  • readyState:表示媒体是否已经就绪的整数(0表示数据不可用、1表示可以显示当前帧、2表示可以开始播放、3表示可以从头到尾播放)

一些统计数据:

  • played:表示已播放的时间范围
  • currentTime:表示已播放秒数的浮点数
  • duration:表示媒体的总播放时间(秒数)的浮点数
  • currentLoop:表示媒体文件已循环次数的整数
  • buffered:表示已下载的缓冲的时间范围
  • bufferedBytes:表示已下载的缓冲的字节范围
  • bufferingRate:下载过程中平均每秒接收到的位数

事件

列举共有的事件。

和加载相关:

  • dataunavailable:因为没有数据而不能播放,readyState0
  • canshowcurrentframe:当前帧已下载完,readyState1
  • canplay:可以播放,readyState2
  • canplaythrough:播放可继续且不会中断,readyState3
  • loadstart:下载已开始
  • loadedmetadata:媒体元数据已加载完成
  • loadeddata:媒体第一帧已加载完成
  • load:已加载完成(可能被废弃,建议用canplaythrough)
  • progress:正在下载
  • stalled:浏览器尝试下载,但未接收到数据

和播放相关:

  • play:媒体已接收到指令开始播放
  • playing:媒体已实际开始播放
  • pause:播放已暂停
  • waiting:播放暂停,等待下载更多数据
  • ended:已播放完成并停止
  • seeked:搜索结束
  • seeking:正移动到新位置
  • timeupdatecurrentTime被以不合理或意外的方式更新
  • durationchangeduration属性改变
  • volumechangevolumemuted改变
  • ratechange:播放速度改变

和出错相关:

  • abort:下载中断
  • empty:发生错误阻止了媒体下载
  • emptied:网络连接关闭
  • error:下载期间发生网络错误

自定义媒体播放器

使用play()pause()方法可以手工控制媒体文件的播放。组合使用属性、方法、事件可以创建一个自定义的媒体播放器。

1
2
3
4
5
6
7
8
9
10
11
<div class="mediaplayer">
<div class="video">
<video id="player" src="movie.mov" poster="mymovie.jpg" width="300" height="200">
Video player not available
</video>
</div>
<div class="controls">
<input type="button" value="Play" id="video-btn">
<span id="curtime">0</span> / <span id="duration">0</span>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const player = document.getElementById("player"),
btn = document.getElementById("video-btn"),
curtime = document.getElementById("curtime"),
duration = document.getElementById("duration"),

// 更新播放时间
duration.innerHTML = player.duration;

// 为按钮添加事件处理程序
btn.addEventListener("click", function(event) {
if(player.paused) {
player.play();
btn.value = "Pause";
} else {
player.pause();
btn.value = "Play";
}
});

// 定时更新当前时间
setInterval(function() {
curtime.innerHTML = player.currentTime;
}, 250);

当然啦,这只是个简单的例子!

检测格式支持情况

方法canPlayType()接受一种格式/编解码器字符串,返回"probably""maybe""",来检测浏览器是否支持某种格式和编解码器,可能性依次降低。

1
2
3
4
5
6
7
8
9
10
11
// 只传入MIME类型,大概率返回"maybe"
// 因为媒体文件本身只不过是音频或视频的一个容器,真正决定文件能否播放的还是编码的格式
if(audio.canPlayType("audio/mpeg")) {
...
}

// 传入MIME类型和解码器,大概率返回"probably"
// 因为格式更加确定了,可能性会增加
if(audio.canPlayType("audio/ogg; codecs=\"vorbis\"")) {
...
}

Audio类型

<audio>有一个原生的JavaScript构造函数Audio()(类比的话就是<img>Image(),不过不同之处在于Audio不用像Image那样必须插入到文档中),可以在任何时候播放音频。

1
2
3
4
const audio = new Audio("sound.mp3"); // 创建即可开始下载
audio.addEventHandler("canplaythrough", function(event) {
audio.play();
});

历史状态管理

因为用户的操作不一定会打开新页面,所以“前进”“后退”就失去了作用,导致用户很难在不同状态间切换。解决这个问题的首选方法是hashchange事件(第13章讨论过),它可以直到URL的参数变化,与之匹配的是history对象。通过这俩,能够在不加载新页面的情况下改变浏览器的URL。

  • pushState

history.pushState()可以将新的状态信息加入历史状态栈,浏览器地址栏也会变成新的相对URL。不过浏览器并不会真的向服务器发送请求(也就相当于创造了假URL,服务器中没有实际的URL与之对应,这时刷新会404),即使状态改变后查询location.href也会返回与地址栏中相同的地址。它接收3个参数:状态对象、新状态的标题、可选的相对URL。

1
history.pushState({name: "Nicholas"}, "Nicholas page", "nicholas.html");
  • popstate

因为pushState()会创建新的历史状态,所以“后退”按钮可以使用了。点击“后退”后,会触发window对象的popstate事件。这个事件的state属性就包含着pushState()的第一个参数。

1
2
3
4
5
6
window.addEventListener("popstate", function(event) {
const state = event.state;
if(state) { // 第一个页面加载时state为空
processState(state);
}
})

得到这个状态对象后,必须把页面重置为这个状态对象中的数据表示的状态(因为浏览器不会自动为我们做这些)。且浏览器加载的第一个页面没有状态,因此单击“后退”按钮返回浏览器加载的第一个页面时,event.state值为null

  • replaceState

用来更新当前状态,传入的参数与pushState()的前两个参数相同。此方法不会在历史状态栈中创建新状态,只会重写当前状态。

1
history.replaceState({name: "Greg"}, "Greg's page");