<h2 id="7.1">HTML網頁元素</h2>
## image元素
### alt屬性,src屬性
alt屬性返回image元素的HTML標簽的alt屬性值,src屬性返回image元素的HTML標簽的src屬性值。
```javascript
// 方法一:HTML5構造函數Image
var img1 = new Image();
img1.src = 'image1.png';
img1.alt = 'alt';
document.body.appendChild(img1);
// 方法二:DOM HTMLImageElement
var img2 = document.createElement('img');
img2.src = 'image2.jpg';
img2.alt = 'alt text';
document.body.appendChild(img2);
document.images[0].src
// image1.png
```
### complete屬性
complete屬性返回一個布爾值,true表示當前圖像屬于瀏覽器支持的圖形類型,并且加載完成,解碼過程沒有出錯,否則就返回false。
### height屬性,width屬性
這兩個屬性返回image元素被瀏覽器渲染后的高度和寬度。
### naturalWidth屬性,naturalHeight屬性
這兩個屬性只讀,表示image對象真實的寬度和高度。
```javascript
myImage.addEventListener('onload', function() {
console.log('My width is: ', this.naturalWidth);
console.log('My height is: ', this.naturalHeight);
});
```
## audio元素,video元素
audio元素和video元素加載音頻和視頻時,以下事件按次序發生。
- loadstart:開始加載音頻和視頻。
- durationchange:音頻和視頻的duration屬性(時長)發生變化時觸發,即已經知道媒體文件的長度。如果沒有指定音頻和視頻文件,duration屬性等于NaN。如果播放流媒體文件,沒有明確的結束時間,duration屬性等于Inf(Infinity)。
- loadedmetadata:媒體文件的元數據加載完畢時觸發,元數據包括duration(時長)、dimensions(大小,視頻獨有)和文字軌。
- loadeddata:媒體文件的第一幀加載完畢時觸發,此時整個文件還沒有加載完。
- progress:瀏覽器正在下載媒體文件,周期性觸發。下載信息保存在元素的buffered屬性中。
- canplay:瀏覽器準備好播放,即使只有幾幀,readyState屬性變為CAN_PLAY。
- canplaythrough:瀏覽器認為可以不緩沖(buffering)播放時觸發,即當前下載速度保持不低于播放速度,readyState屬性變為CAN_PLAY_THROUGH。
除了上面這些事件,audio元素和video元素還支持以下事件。
事件|觸發條件
----|--------
abort|播放中斷
emptied|媒體文件加載后又被清空,比如加載后又調用load方法重新加載。
ended|播放結束
error|發生錯誤。該元素的error屬性包含更多信息。
pause|播放暫停
play|暫停后重新開始播放
playing|開始播放,包括第一次播放、暫停后播放、結束后重新播放。
ratechange|播放速率改變
seeked|搜索操作結束
seeking|搜索操作開始
stalled|瀏覽器開始嘗試讀取媒體文件,但是沒有如預期那樣獲取數據
suspend|加載文件停止,有可能是播放結束,也有可能是其他原因的暫停
timeupdate|網頁元素的currentTime屬性改變時觸發。
volumechange|音量改變時觸發(包括靜音)。
waiting|由于另一個操作(比如搜索)還沒有結束,導致當前操作(比如播放)不得不等待。
<h2 id="7.2">Canvas</h2>
## 概述
Canvas API(畫布)用于在網頁實時生成圖像,并且可以操作圖像內容,基本上它是一個可以用JavaScript操作的位圖(bitmap)。
使用前,首先需要新建一個canvas網頁元素。
```html
<canvas id="myCanvas" width="400" height="200">
您的瀏覽器不支持canvas!
</canvas>
```
上面代碼中,如果瀏覽器不支持這個API,則就會顯示canvas標簽中間的文字——“您的瀏覽器不支持canvas!”。
每個canvas元素都有一個對應的context對象(上下文對象),Canvas API定義在這個context對象上面,所以需要獲取這個對象,方法是使用getContext方法。
```javascript
var canvas = document.getElementById('myCanvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
}
```
上面代碼中,getContext方法指定參數2d,表示該canvas對象用于生成2D圖案(即平面圖案)。如果參數是`webgl`,就表示用于生成3D圖像(即立體圖案),這部分實際上單獨叫做WebGL API(本書不涉及)。
## 繪圖方法
canvas畫布提供了一個用來作圖的平面空間,該空間的每個點都有自己的坐標,x表示橫坐標,y表示豎坐標。原點(0, 0)位于圖像左上角,x軸的正向是原點向右,y軸的正向是原點向下。
**(1)繪制路徑**
beginPath方法表示開始繪制路徑,moveTo(x, y)方法設置線段的起點,lineTo(x, y)方法設置線段的終點,stroke方法用來給透明的線段著色。
```javascript
ctx.beginPath(); // 開始路徑繪制
ctx.moveTo(20, 20); // 設置路徑起點,坐標為(20,20)
ctx.lineTo(200, 20); // 繪制一條到(200,20)的直線
ctx.lineWidth = 1.0; // 設置線寬
ctx.strokeStyle = "#CC0000"; // 設置線的顏色
ctx.stroke(); // 進行線的著色,這時整條線才變得可見
```
moveto和lineto方法可以多次使用。最后,還可以使用closePath方法,自動繪制一條當前點到起點的直線,形成一個封閉圖形,省卻使用一次lineto方法。
**(2)繪制矩形**
fillRect(x, y, width, height)方法用來繪制矩形,它的四個參數分別為矩形左上角頂點的x坐標、y坐標,以及矩形的寬和高。fillStyle屬性用來設置矩形的填充色。
```javascript
ctx.fillStyle = 'yellow';
ctx.fillRect(50, 50, 200, 100);
```
strokeRect方法與fillRect類似,用來繪制空心矩形。
```javascript
ctx.strokeRect(10,10,200,100);
```
clearRect方法用來清除某個矩形區域的內容。
```javascript
ctx.clearRect(100,50,50,50);
```
**(3)繪制文本**
fillText(string, x, y) 用來繪制文本,它的三個參數分別為文本內容、起點的x坐標、y坐標。使用之前,需用font設置字體、大小、樣式(寫法類似與CSS的font屬性)。與此類似的還有strokeText方法,用來添加空心字。
```javascript
// 設置字體
ctx.font = "Bold 20px Arial";
// 設置對齊方式
ctx.textAlign = "left";
// 設置填充顏色
ctx.fillStyle = "#008600";
// 設置字體內容,以及在畫布上的位置
ctx.fillText("Hello!", 10, 50);
// 繪制空心字
ctx.strokeText("Hello!", 10, 100);
```
fillText方法不支持文本斷行,即所有文本出現在一行內。所以,如果要生成多行文本,只有調用多次fillText方法。
**(4)繪制圓形和扇形**
arc方法用來繪制扇形。
```javascript
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
```
arc方法的x和y參數是圓心坐標,radius是半徑,startAngle和endAngle則是扇形的起始角度和終止角度(以弧度表示),anticlockwise表示做圖時應該逆時針畫(true)還是順時針畫(false)。
下面是如何繪制實心的圓形。
```javascript
ctx.beginPath();
ctx.arc(60, 60, 50, 0, Math.PI*2, true);
ctx.fillStyle = "#000000";
ctx.fill();
```
繪制空心圓形的例子。
```javascript
ctx.beginPath();
ctx.arc(60, 60, 50, 0, Math.PI*2, true);
ctx.lineWidth = 1.0;
ctx.strokeStyle = "#000";
ctx.stroke();
```
**(5)設置漸變色**
createLinearGradient方法用來設置漸變色。
```javascript
var myGradient = ctx.createLinearGradient(0, 0, 0, 160);
myGradient.addColorStop(0, "#BABABA");
myGradient.addColorStop(1, "#636363");
```
createLinearGradient方法的參數是(x1, y1, x2, y2),其中x1和y1是起點坐標,x2和y2是終點坐標。通過不同的坐標值,可以生成從上至下、從左到右的漸變等等。
使用方法如下:
```javascript
ctx.fillStyle = myGradient;
ctx.fillRect(10,10,200,100);
```
**(6)設置陰影**
一系列與陰影相關的方法,可以用來設置陰影。
```javascript
ctx.shadowOffsetX = 10; // 設置水平位移
ctx.shadowOffsetY = 10; // 設置垂直位移
ctx.shadowBlur = 5; // 設置模糊度
ctx.shadowColor = "rgba(0,0,0,0.5)"; // 設置陰影顏色
ctx.fillStyle = "#CC0000";
ctx.fillRect(10,10,200,100);
```
## 圖像處理方法
### drawImage方法
canvas允許將圖像文件插入畫布,做法是讀取圖片后,使用drawImage方法在畫布內進行重繪。
```javascript
var img = new Image();
img.src = "image.png";
ctx.drawImage(img, 0, 0); // 設置對應的圖像對象,以及它在畫布上的位置
```
上面代碼將一個PNG圖像載入canvas。
由于圖像的載入需要時間,drawImage方法只能在圖像完全載入后才能調用,因此上面的代碼需要改寫。
```javascript
var image = new Image();
image.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
}
image.src = "image.png";
```
drawImage()方法接受三個參數,第一個參數是圖像文件的DOM元素(即img標簽),第二個和第三個參數是圖像左上角在Canvas元素中的坐標,上例中的(0, 0)就表示將圖像左上角放置在Canvas元素的左上角。
### getImageData方法,putImageData方法
getImageData方法可以用來讀取Canvas的內容,返回一個對象,包含了每個像素的信息。
```javascript
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
```
imageData對象有一個data屬性,它的值是一個一維數組。該數組的值,依次是每個像素的紅、綠、藍、alpha通道值,因此該數組的長度等于 圖像的像素寬度 x 圖像的像素高度 x 4,每個值的范圍是0–255。這個數組不僅可讀,而且可寫,因此通過操作這個數組的值,就可以達到操作圖像的目的。修改這個數組以后,使用putImageData方法將數組內容重新繪制在Canvas上。
```javascript
context.putImageData(imageData, 0, 0);
```
### toDataURL方法
對圖像數據做出修改以后,可以使用toDataURL方法,將Canvas數據重新轉化成一般的圖像文件形式。
```javascript
function convertCanvasToImage(canvas) {
var image = new Image();
image.src = canvas.toDataURL("image/png");
return image;
}
```
上面的代碼將Canvas數據,轉化成PNG data URI。
### save方法,restore方法
save方法用于保存上下文環境,restore方法用于恢復到上一次保存的上下文環境。
```javascript
ctx.save();
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 5;
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.fillStyle = "#CC0000";
ctx.fillRect(10,10,150,100);
ctx.restore();
ctx.fillStyle = "#000000";
ctx.fillRect(180,10,150,100);
```
上面代碼先用save方法,保存了當前設置,然后繪制了一個有陰影的矩形。接著,使用restore方法,恢復了保存前的設置,繪制了一個沒有陰影的矩形。
## 動畫
利用JavaScript,可以在canvas元素上很容易地產生動畫效果。
```javascript
var posX = 20,
posY = 100;
setInterval(function() {
context.fillStyle = "black";
context.fillRect(0,0,canvas.width, canvas.height);
posX += 1;
posY += 0.25;
context.beginPath();
context.fillStyle = "white";
context.arc(posX, posY, 10, 0, Math.PI*2, true);
context.closePath();
context.fill();
}, 30);
```
上面代碼會產生一個小圓點,每隔30毫秒就向右下方移動的效果。setInterval函數的一開始,之所以要將畫布重新渲染黑色底色,是為了抹去上一步的小圓點。
通過設置圓心坐標,可以產生各種運動軌跡。
先上升后下降。
```javascript
var vx = 10,
vy = -10,
gravity = 1;
setInterval(function() {
posX += vx;
posY += vy;
vy += gravity;
// ...
});
```
上面代碼中,x坐標始終增大,表示持續向右運動。y坐標先變小,然后在重力作用下,不斷增大,表示先上升后下降。
小球不斷反彈后,逐步趨于靜止。
```javascript
var vx = 10,
vy = -10,
gravity = 1;
setInterval(function() {
posX += vx;
posY += vy;
if (posY > canvas.height * 0.75) {
vy *= -0.6;
vx *= 0.75;
posY = canvas.height * 0.75;
}
vy += gravity;
// ...
});
```
上面代碼表示,一旦小球的y坐標處于屏幕下方75%的位置,向x軸移動的速度變為原來的75%,而向y軸反彈上一次反彈高度的40%。
## 像素處理
通過getImageData方法和putImageData方法,可以處理每個像素,進而操作圖像內容。
假定filter是一個處理像素的函數,那么整個對Canvas的處理流程,可以用下面的代碼表示。
```javascript
if (canvas.width > 0 && canvas.height > 0) {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
filter(imageData);
context.putImageData(imageData, 0, 0);
}
```
以下是幾種常見的處理方法。
### 灰度效果
灰度圖(grayscale)就是取紅、綠、藍三個像素值的算術平均值,這實際上將圖像轉成了黑白形式。假定d[i]是像素數組中一個象素的紅色值,則d[i+1]為綠色值,d[i+2]為藍色值,d[i+3]就是alpha通道值。轉成灰度的算法,就是將紅、綠、藍三個值相加后除以3,再將結果寫回數組。
```javascript
grayscale = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = d[i + 1] = d[i + 2] = (r+g+b)/3;
}
return pixels;
};
```
### 復古效果
復古效果(sepia)則是將紅、綠、藍三個像素,分別取這三個值的某種加權平均值,使得圖像有一種古舊的效果。
```javascript
sepia = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = (r * 0.393)+(g * 0.769)+(b * 0.189); // red
d[i + 1] = (r * 0.349)+(g * 0.686)+(b * 0.168); // green
d[i + 2] = (r * 0.272)+(g * 0.534)+(b * 0.131); // blue
}
return pixels;
};
```
### 紅色蒙版效果
紅色蒙版指的是,讓圖像呈現一種偏紅的效果。算法是將紅色通道設為紅、綠、藍三個值的平均值,而將綠色通道和藍色通道都設為0。
```javascript
red = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = (r+g+b)/3; // 紅色通道取平均值
d[i + 1] = d[i + 2] = 0; // 綠色通道和藍色通道都設為0
}
return pixels;
};
```
### 亮度效果
亮度效果(brightness)是指讓圖像變得更亮或更暗。算法將紅色通道、綠色通道、藍色通道,同時加上一個正值或負值。
```javascript
brightness = function (pixels, delta) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
d[i] += delta; // red
d[i + 1] += delta; // green
d[i + 2] += delta; // blue
}
return pixels;
};
```
### 反轉效果
反轉效果(invert)是指圖片呈現一種色彩顛倒的效果。算法為紅、綠、藍通道都取各自的相反值(255-原值)。
```javascript
invert = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
d[i] = 255 - d[i];
d[i+1] = 255 - d[i + 1];
d[i+2] = 255 - d[i + 2];
}
return pixels;
};
```
<h2 id="7.3">SVG圖像</h2>
SVG是“可縮放矢量圖”(Scalable Vector Graphics)的縮寫,是一種描述向量圖形的XML格式的標記化語言。也就是說,SVG本質上是文本文件,格式采用XML,可以在瀏覽器中顯示出矢量圖像。由于結構是XML格式,使得它可以插入HTML文檔,成為DOM的一部分,然后用JavaScript和CSS進行操作。
相比傳統的圖像文件格式(比如JPG和PNG),SVG圖像的優勢就是文件體積小,并且放大多少倍都不會失真,因此非常合適用于網頁。
SVG圖像可以用Adobe公司的Illustrator軟件、開源軟件Inkscape等生成。目前,所有主流瀏覽器都支持,對于低于IE 9的瀏覽器,可以使用第三方的[polyfills函數庫](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills#svg)。
## 插入SVG文件
SVG插入網頁的方法有多種,可以用在img、object、embed、iframe等標簽,以及CSS的background-image屬性。
```html
<img src="circle.svg">
<object id="object" data="circle.svg" type="image/svg+xml"></object>
<embed id="embed" src="icon.svg" type="image/svg+xml">
<iframe id="iframe" src="icon.svg"></iframe>
```
上面是四種在網頁中插入SVG圖像的方式。
此外,SVG文件還可以插入其他DOM元素,比如div元素,請看下面的例子(使用了jQuery函數庫)。
```html
<div id="stage"></div>
<script>
$("#stage").load('icon.svg',function(response){
$(this).addClass("svgLoaded");
if(!response){
// 加載失敗的處理代碼
}
});
</script>
```
## svg格式
SVG文件采用XML格式,就是普通的文本文件。
```xml
<svg width="300" height="180">
<circle cx="30" cy="50" r="25" />
<circle cx="90" cy="50" r="25" class="red" />
<circle cx="150" cy="50" r="25" class="fancy" />
</svg>
```
上面的svg文件,定義了三個圓,它們的cx、cy、r屬性分別為x坐標、y坐標和半徑。利用class屬性,可以為這些圓指定樣式。
```css
.red {
fill: red; /* not background-color! */
}
.fancy {
fill: none;
stroke: black; /* similar to border-color */
stroke-width: 3pt; /* similar to border-width */
}
```
上面代碼中,fill屬性表示填充色,stroke屬性表示描邊色,stroke-width屬性表示邊線寬度。
除了circle標簽表示圓,SVG文件還可以使用表示其他形狀的標簽。
```html
<svg>
<line x1="0" y1="0" x2="200" y2="0" style="stroke:rgb(0,0,0);stroke-width:1"/></line>
<rect x="0" y="0" height="100" width="200" style="stroke: #70d5dd; fill: #dd524b" />
<ellipse cx="60" cy="60" ry="40" rx="20" stroke="black" stroke-width="5" fill="silver"/></ellipse>
<polygon fill="green" stroke="orange" stroke-width="10" points="350, 75 379,161 469,161 397,215 423,301 350,250 277,301 303,215 231,161 321,161"/><polygon>
<path id="path1" d="M160.143,196c0,0,62.777-28.033,90-17.143c71.428,28.572,73.952-25.987,84.286-21.428" style="fill:none;stroke:2;"></path>
</svg>
```
上面代碼中,line、rect、ellipse、polygon和path標簽,分別表示線條、矩形、橢圓、多邊形和路徑。
g標簽用于將多個形狀組成一組,表示group。
```xml
<svg width="300" height="180">
<g transform="translate(5, 15)">
<text x="0" y="0">Howdy!</text>
<path d="M0,50 L50,0 Q100,0 100,50"
fill="none" stroke-width="3" stroke="black" />
</g>
</svg>
```
## SVG文件的JavaScript操作
### 獲取SVG DOM
如果使用img標簽插入SVG文件,則無法獲取SVG DOM。使用object、iframe、embed標簽,可以獲取SVG DOM。
```javascript
var svgObject = document.getElementById("object").contentDocument;
var svgIframe = document.getElementById("iframe").contentDocument;
var svgEmbed = document.getElementById("embed").getSVGDocument();
```
由于svg文件就是一般的XML文件,因此可以用DOM方法,選取頁面元素。
```javascript
// 改變填充色
document.getElementById("theCircle").style.fill = "red";
// 改變元素屬性
document.getElementById("theCircle").setAttribute("class", "changedColors");
// 綁定事件回調函數
document.getElementById("theCircle").addEventListener("click", function() {
console.log("clicked")
});
```
### 讀取svg源碼
由于svg文件就是一個XML代碼的文本文件,因此可以通過讀取XML代碼的方式,讀取svg源碼。
假定網頁中有一個svg元素。
```html
<div id="svg-container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="500" height="440">
<!-- svg code -->
</svg>
</div>
```
使用XMLSerializer實例的serializeToString方法,獲取svg元素的代碼。
```javascript
var svgString = new XMLSerializer().serializeToString(document.querySelector('svg'));
```
### 將svg圖像轉為canvas圖像
首先,需要新建一個img對象,將svg圖像指定到該img對象的src屬性。
```javascript
var img = new Image();
var svg = new Blob([svgString], {type: "image/svg+xml;charset=utf-8"});
var DOMURL = self.URL || self.webkitURL || self;
var url = DOMURL.createObjectURL(svg);
img.src = url;
```
然后,當圖像加載完成后,再將它繪制到canvas元素。
```javascript
img.onload = function() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
};
```
## 實例
假定我們要將下面的表格畫成圖形。
Date |Amount
-----|------
2014-01-01 | $10
2014-02-01 | $20
2014-03-01 | $40
2014-04-01 | $80
上面的圖形,可以畫成一個坐標系,Date作為橫軸,Amount作為縱軸,四行數據畫成一個數據點。
```xml
<svg width="350" height="160">
<g class="layer" transform="translate(60,10)">
<circle r="5" cx="0" cy="105" />
<circle r="5" cx="90" cy="90" />
<circle r="5" cx="180" cy="60" />
<circle r="5" cx="270" cy="0" />
<g class="y axis">
<line x1="0" y1="0" x2="0" y2="120" />
<text x="-40" y="105" dy="5">$10</text>
<text x="-40" y="0" dy="5">$80</text>
</g>
<g class="x axis" transform="translate(0, 120)">
<line x1="0" y1="0" x2="270" y2="0" />
<text x="-30" y="20">January 2014</text>
<text x="240" y="20">April</text>
</g>
</g>
</svg>
```
<h2 id="7.4">表單</h2>
## 表單元素
`input`、`textarea`、`password`、`select`等元素都可以通過`value`屬性取到它們的值。
### select
`select`是下拉列表元素。
```html
<div>
<label for="os">Operating System</label>
<select name="os" id="os">
<option>Choose</option>
<optgroup label="Windows">
<option value="7 Home Basic">7 Home Basic</option>
<option value="7 Home Premium">7 Home Premium</option>
<option value="7 Professional">7 Professional</option>
<option value="7 Ultimate">7 Ultimate</option>
<option value="Vista">Vista</option>
<option value="XP">XP</option>
</optgroup>
<select>
</div>
```
可以通過`value`屬性取到用戶選擇的值。
```javascript
var data = document.getElementById('selectMenu').value;
```
`selectedIndex`可以設置選中的項目(從0開始)。如果用戶沒有選中任何一項,`selectedIndex`等于`-1`。
```javascript
document.getElementById('selectMenu').selectedIndex = 1;
```
`select`元素也可以設置為多選。
```html
<select name="categories" id="categories" multiple>
```
設為多選時,`value`只返回選中的第一個選項。要取出所有選中的值,就必須遍歷`select`的所有選項,檢查每一項的`selected`屬性。
```javascript
var selected = [];
for (var i = 0, count = elem.options.length; i < count; i++) {
if (elem.options[i].selected) {
selected.push(elem.options[i].value);
}
}
```
### checkbox
`checkbox`是多選框控件,每個選擇框只有選中和不選中兩種狀態。
```html
<input type="checkbox" name="toggle" id="toggle" value="toggle">
```
`checked`屬性返回一個布爾值,表示用戶是否選中。
```javascript
var which = document.getElementById('someCheckbox').checked;
```
`checked`屬性是可寫的。
```javascript
which.checked = true;
```
`value`屬性可以獲取單選框的值。
```javascript
if (which.checked) {
var value = document.getElementById('someCheckbox').value;
}
```
### radio
radio是單選框控件,同一組選擇框同時只能選中一個,選中元素的`checked`屬性為`true`。由于同一組選擇框的`name`屬性都相同,所以只有通過遍歷,才能獲得用戶選中的那個選擇框的`value`。
```html
<input type="radio" name="gender" value="Male"> Male </input>
<input type="radio" name="gender" value="Female"> Female </input>
<script>
var radios = document.getElementsByName('gender');
var selected;
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
selected = radios[i].value;
break;
}
}
if (selected) {
// 用戶選中了某個選項
}
</script>
```
上面代碼中,要求用戶選擇“性別”。通過遍歷所有選項,獲取用戶選中的項。如果用戶未做任何選擇,則`selected`就為`undefined`。
## 表單的驗證
### HTML 5表單驗證
所謂“表單驗證”,指的是檢查用戶提供的數據是否符合要求,比如Email地址的格式。
檢查用戶是否在`input`輸入框之中填入值。
```javascript
if (inputElem.value === inputElem.defaultValue) {
// 用戶沒有填入內容
}
```
HTML 5原生支持表單驗證,不需要JavaScript。
```html
<input type="date" >
```
上面代碼指定該input輸入框只能填入日期,否則瀏覽器會報錯。
但有時,原生的表單驗證不完全符合需要,而且出錯信息無法指定樣式。這時,可能需要使用表單對象的noValidate屬性,將原生的表單驗證關閉。
```javascript
var form = document.getElementById("myform");
form.noValidate = true;
form.onsubmit = validateForm;
```
上面代碼先關閉原生的表單驗證,然后指定submit事件時,讓JavaScript接管表單驗證。
此外,還可以只針對單個的input輸入框,關閉表單驗證。
```javascript
form.field.willValidate = false;
```
每個input輸入框都有willValidate屬性,表示是否開啟表單驗證。對于那些不支持的瀏覽器(比如IE8),該屬性等于undefined。
麻煩的地方在于,即使willValidate屬性為true,也不足以表示瀏覽器支持所有種類的表單驗證。比如,Firefox 29不支持date類型的輸入框,會自動將其改為text類型,而此時它的willValidate屬性為true。為了解決這個問題,必須確認input輸入框的類型(type)未被瀏覽器改變。
```javascript
if (field.nodeName === "INPUT" && field.type !== field.getAttribute("type")) {
// 瀏覽器不支持該種表單驗證,需自行部署JavaScript驗證
}
```
### checkValidity方法,setCustomValidity方法,validity對象
checkValidity方法表示執行原生的表單驗證,如果驗證通過返回true。如果驗證失敗,則會觸發一個invalid事件。使用該方法以后,會設置validity對象的值。
每一個表單元素都有一個validity對象,它有以下屬性。
- valid:如果該元素通過驗證,則返回true。
- valueMissing:如果用戶沒填必填項,則返回true。
- typeMismatch:如果填入的格式不正確(比如Email地址),則返回true。
- patternMismatch:如果不匹配指定的正則表達式,則返回true。
- tooLong:如果超過最大長度,則返回true。
- tooShort:如果小于最短長度,則返回true。
- rangeUnderFlow:如果小于最小值,則返回true。
- rangeOverflow:如果大于最大值,則返回true。
- stepMismatch:如果不匹配步長(step),則返回true。
- badInput:如果不能轉為值,則返回true。
- customError:如果該欄有自定義錯誤,則返回true。
setCustomValidity方法用于自定義錯誤信息,該提示信息也反映在該輸入框的validationMessage屬性中。如果將setCustomValidity設為空字符串,則意味該項目驗證通過。
<h2 id="7.5">文件與二進制數據的操作</h2>
歷史上,JavaScript無法處理二進制數據。如果一定要處理的話,只能使用charCodeAt()方法,一個個字節地從文字編碼轉成二進制數據,還有一種辦法是將二進制數據轉成Base64編碼,再進行處理。這兩種方法不僅速度慢,而且容易出錯。ECMAScript 5引入了Blob對象,允許直接操作二進制數據。
Blob對象是一個代表二進制數據的基本對象,在它的基礎上,又衍生出一系列相關的API,用來操作文件。
- File對象:負責處理那些以文件形式存在的二進制數據,也就是操作本地文件;
- FileList對象:File對象的網頁表單接口;
- FileReader對象:負責將二進制數據讀入內存內容;
- URL對象:用于對二進制數據生成URL。
## Blob對象
Blob(Binary Large Object)對象代表了一段二進制數據,提供了一系列操作接口。其他操作二進制數據的API(比如File對象),都是建立在Blob對象基礎上的,繼承了它的屬性和方法。
生成Blob對象有兩種方法:一種是使用Blob構造函數,另一種是對現有的Blob對象使用slice方法切出一部分。
(1)Blob構造函數,接受兩個參數。第一個參數是一個包含實際數據的數組,第二個參數是數據的類型,這兩個參數都不是必需的。
```javascript
var htmlParts = ["<a id=\"a\"><b id=\"b\">hey!<\/b><\/a>"];
var myBlob = new Blob(htmlParts, { "type" : "text\/xml" });
```
下面是一個利用Blob對象,生成可下載文件的例子。
```javascript
var blob = new Blob(["Hello World"]);
var a = document.createElement("a");
a.href = window.URL.createObjectURL(blob);
a.download = "hello-world.txt";
a.textContent = "Download Hello World!";
body.appendChild(a);
```
上面的代碼生成了一個超級鏈接,點擊后提示下載文本文件hello-world.txt,文件內容為“Hello World”。
(2)Blob對象的slice方法,將二進制數據按照字節分塊,返回一個新的Blob對象。
```javascript
var newBlob = oldBlob.slice(startingByte, endindByte);
```
下面是一個使用XMLHttpRequest對象,將大文件分割上傳的例子。
```javascript
function upload(blobOrFile) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
xhr.onload = function(e) { ... };
xhr.send(blobOrFile);
}
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
var blob = this.files[0];
const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
const SIZE = blob.size;
var start = 0;
var end = BYTES_PER_CHUNK;
while(start < SIZE) {
upload(blob.slice(start, end));
start = end;
end = start + BYTES_PER_CHUNK;
}
}, false);
})();
```
(3)Blob對象有兩個只讀屬性:
- size:二進制數據的大小,單位為字節。
- type:二進制數據的MIME類型,全部為小寫,如果類型未知,則該值為空字符串。
在Ajax操作中,如果xhr.responseType設為blob,接收的就是二進制數據。
## FileList對象
FileList對象針對表單的file控件。當用戶通過file控件選取文件后,這個控件的files屬性值就是FileList對象。它在結構上類似于數組,包含用戶選取的多個文件。
```html
<input type="file" id="input" onchange="console.log(this.files.length)" multiple />
```
當用戶選取文件后,就可以讀取該文件。
```javascript
var selected_file = document.getElementById('input').files[0];
```
采用拖放方式,也可以得到FileList對象。
```javascript
var dropZone = document.getElementById('drop_zone');
dropZone.addEventListener('drop', handleFileSelect, false);
function handleFileSelect(evt) {
evt.stopPropagation();
evt.preventDefault();
var files = evt.dataTransfer.files; // FileList object.
// ...
}
```
上面代碼的 handleFileSelect 是拖放事件的回調函數,它的參數evt是一個事件對象,該參數的dataTransfer.files屬性就是一個FileList對象,里面包含了拖放的文件。
## File API
File API提供`File`對象,它是`FileList`對象的成員,包含了文件的一些元信息,比如文件名、上次改動時間、文件大小和文件類型。
```javascript
var selected_file = document.getElementById('input').files[0];
var fileName = selected_file.name;
var fileSize = selected_file.size;
var fileType = selected_file.type;
```
`File`對象的屬性值如下。
- `name`:文件名,該屬性只讀。
- `size`:文件大小,單位為字節,該屬性只讀。
- `type`:文件的MIME類型,如果分辨不出類型,則為空字符串,該屬性只讀。
- `lastModified`:文件的上次修改時間,格式為時間戳。
- `lastModifiedDate`:文件的上次修改時間,格式為`Date`對象實例。
```javascript
$('#upload-file').files[0]
// {
// lastModified: 1449370355682,
// lastModifiedDate: Sun Dec 06 2015 10:52:35 GMT+0800 (CST),
// name: "HTTP 2 is here Goodbye SPDY Not quite yet.png",
// size: 17044,
// type: "image/png"
// }
```
## FileReader API
FileReader API用于讀取文件,即把文件內容讀入內存。它的參數是`File`對象或`Blob`對象。
對于不同類型的文件,FileReader提供不同的方法讀取文件。
- `readAsBinaryString(Blob|File)`:返回二進制字符串,該字符串每個字節包含一個0到255之間的整數。
- `readAsText(Blob|File, opt_encoding)`:返回文本字符串。默認情況下,文本編碼格式是'UTF-8',可以通過可選的格式參數,指定其他編碼格式的文本。
- `readAsDataURL(Blob|File)`:返回一個基于Base64編碼的data-uri對象。
- `readAsArrayBuffer(Blob|File)`:返回一個ArrayBuffer對象。
`readAsText`方法用于讀取文本文件,它的第一個參數是`File`或`Blob`對象,第二個參數是前一個參數的編碼方法,如果省略就默認為`UTF-8`編碼。該方法是異步方法,一般監聽`onload`件,用來確定文件是否加載結束,方法是判斷`FileReader`實例的`result`屬性是否有值。其他三種讀取方法,用法與`readAsText`方法類似。
```javascript
var reader = new FileReader();
reader.onload = function(e) {
var text = reader.result;
}
reader.readAsText(file, encoding);
```
`readAsDataURL`方法返回一個data URL,它的作用基本上是將文件數據進行Base64編碼。你可以將返回值設為圖像的`src`屬性。
```javascript
var file = document.getElementById('destination').files[0];
if(file.type.indexOf('image') !== -1) {
var reader = new FileReader();
reader.onload = function (e) {
var dataURL = reader.result;
}
reader.readAsDataURL(file);
}
```
`readAsBinaryString`方法可以讀取任意類型的文件,而不僅僅是文本文件,返回文件的原始的二進制內容。這個方法與XMLHttpRequest.sendAsBinary方法結合使用,就可以使用JavaScript上傳任意文件到服務器。
```javascript
var reader = new FileReader();
reader.onload = function(e) {
var rawData = reader.result;
}
reader.readAsBinaryString(file);
```
`readAsArrayBuffer`方法讀取文件,返回一個類型化數組(ArrayBuffer),即固定長度的二進制緩存數據。在文件操作時(比如將JPEG圖像轉為PNG圖像),這個方法非常方便。
```javascript
var reader = new FileReader();
reader.onload = function(e) {
var arrayBuffer = reader.result;
}
reader.readAsArrayBuffer(file);
```
除了以上四種不同的讀取文件方法,FileReader API還有一個`abort`方法,用于中止文件上傳。
```javascript
var reader = new FileReader();
reader.abort();
```
FileReader對象采用異步方式讀取文件,可以為一系列事件指定回調函數。
- onabort方法:讀取中斷或調用reader.abort()方法時觸發。
- onerror方法:讀取出錯時觸發。
- onload方法:讀取成功后觸發。
- onloadend方法:讀取完成后觸發,不管是否成功。觸發順序排在 onload 或 onerror 后面。
- onloadstart方法:讀取將要開始時觸發。
- onprogress方法:讀取過程中周期性觸發。
下面的代碼是如何展示文本文件的內容。
```javascript
var reader = new FileReader();
reader.onload = function(e) {
console.log(e.target.result);
}
reader.readAsText(blob);
```
`onload`事件的回調函數接受一個事件對象,該對象的`target.result`就是文件的內容。
下面是一個使用`readAsDataURL`方法,為`img`元素添加`src`屬性的例子。
```javascript
var reader = new FileReader();
reader.onload = function(e) {
document.createElement('img').src = e.target.result;
};
reader.readAsDataURL(f);
```
下面是一個`onerror`事件回調函數的例子。
```javascript
var reader = new FileReader();
reader.onerror = errorHandler;
function errorHandler(evt) {
switch(evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
alert('File Not Found!');
break;
case evt.target.error.NOT_READABLE_ERR:
alert('File is not readable');
break;
case evt.target.error.ABORT_ERR:
break;
default:
alert('An error occurred reading this file.');
};
}
```
下面是一個`onprogress`事件回調函數的例子,主要用來顯示讀取進度。
```javascript
var reader = new FileReader();
reader.onprogress = updateProgress;
function updateProgress(evt) {
if (evt.lengthComputable) {
var percentLoaded = Math.round((evt.loaded / evt.totalEric Bidelman) * 100);
var progress = document.querySelector('.percent');
if (percentLoaded < 100) {
progress.style.width = percentLoaded + '%';
progress.textContent = percentLoaded + '%';
}
}
}
```
讀取大文件的時候,可以利用`Blob`對象的`slice`方法,將大文件分成小段,逐一讀取,這樣可以加快處理速度。
## 綜合實例:顯示用戶選取的本地圖片
假設有一個表單,用于用戶選取圖片。
```html
<input type="file" name="picture" accept="image/png, image/jpeg"/>
```
一旦用戶選中圖片,將其顯示在canvas的函數可以這樣寫:
```javascript
document.querySelector('input[name=picture]').onchange = function(e){
readFile(e.target.files[0]);
}
function readFile(file){
var reader = new FileReader();
reader.onload = function(e){
applyDataUrlToCanvas( reader.result );
};
reader.reaAsDataURL(file);
}
```
還可以在canvas上面定義拖放事件,允許用戶直接拖放圖片到上面。
```javascript
// stop FireFox from replacing the whole page with the file.
canvas.ondragover = function () { return false; };
// Add drop handler
canvas.ondrop = function (e) {
e.stopPropagation();
e.preventDefault();
e = e || window.event;
var files = e.dataTransfer.files;
if(files){
readFile(files[0]);
}
};
```
所有的拖放事件都有一個dataTransfer屬性,它包含拖放過程涉及的二進制數據。
還可以讓canvas顯示剪貼板中的圖片。
```javascript
document.onpaste = function(e){
e.preventDefault();
if(e.clipboardData&&e.clipboardData.items){
// pasted image
for(var i=0, items = e.clipboardData.items;i<items.length;i++){
if( items[i].kind==='file' && items[i].type.match(/^image/) ){
readFile(items[i].getAsFile());
break;
}
}
}
return false;
};
```
## URL對象
URL對象用于生成指向File對象或Blob對象的URL。
```javascript
var objecturl = window.URL.createObjectURL(blob);
```
上面的代碼會對二進制數據生成一個URL,類似于“blob:http%3A//test.com/666e6730-f45c-47c1-8012-ccc706f17191”。這個URL可以放置于任何通常可以放置URL的地方,比如img標簽的src屬性。需要注意的是,即使是同樣的二進制數據,每調用一次URL.createObjectURL方法,就會得到一個不一樣的URL。
這個URL的存在時間,等同于網頁的存在時間,一旦網頁刷新或卸載,這個URL就失效。除此之外,也可以手動調用URL.revokeObjectURL方法,使URL失效。
```javascript
window.URL.revokeObjectURL(objectURL);
```
下面是一個利用URL對象,在網頁插入圖片的例子。
```javascript
var img = document.createElement("img");
img.src = window.URL.createObjectURL(files[0]);
img.height = 60;
img.onload = function(e) {
window.URL.revokeObjectURL(this.src);
}
body.appendChild(img);
var info = document.createElement("span");
info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
body.appendChild(info);
```
還有一個本機視頻預覽的例子。
```javascript
var video = document.getElementById('video');
var obj_url = window.URL.createObjectURL(blob);
video.src = obj_url;
video.play()
window.URL.revokeObjectURL(obj_url);
```
<h2 id="7.6">Web Worker</h2>
## 概述
JavaScript語言采用的是單線程模型,也就是說,所有任務排成一個隊列,一次只能做一件事。隨著電腦計算能力的增強,尤其是多核CPU的出現,這一點帶來很大的不便,無法充分發揮JavaScript的潛力。
Web Worker的目的,就是為JavaScript創造多線程環境,允許主線程將一些任務分配給子線程。在主線程運行的同時,子線程在后臺運行,兩者互不干擾。等到子線程完成計算任務,再把結果返回給主線程。因此,每一個子線程就好像一個“工人”(worker),默默地完成自己的工作。這樣做的好處是,一些高計算量或高延遲的工作,被worker線程負擔了,所以主進程(通常是UI進程)就會很流暢,不會被阻塞或拖慢。
Worker線程分成好幾種。
- 普通的Worker:只能與創造它們的主進程通信。
- Shared Worker:能被所有同源的進程獲取(比如來自不同的瀏覽器窗口、iframe窗口和其他Shared worker),它們必須通過一個端口通信。
- ServiceWorker:實際上是一個在網絡應用與瀏覽器或網絡層之間的代理層。它可以攔截網絡請求,使得離線訪問成為可能。
Web Worker有以下幾個特點:
- **同域限制**。子線程加載的腳本文件,必須與主線程的腳本文件在同一個域。
- **DOM限制**。子線程所在的全局對象,與主進程不一樣,它無法讀取網頁的DOM對象,即`document`、`window`、`parent`這些對象,子線程都無法得到。(但是,`navigator`對象和`location`對象可以獲得。)
- **腳本限制**。子線程無法讀取網頁的全局變量和函數,也不能執行alert和confirm方法,不過可以執行setInterval和setTimeout,以及使用XMLHttpRequest對象發出AJAX請求。
- **文件限制**。子線程無法讀取本地文件,即子線程無法打開本機的文件系統(file://),它所加載的腳本,必須來自網絡。
使用之前,檢查瀏覽器是否支持這個API。
```javascript
if (window.Worker) {
// 支持
} else {
// 不支持
}
```
## 新建和啟動子線程
主線程采用`new`命令,調用`Worker`構造函數,可以新建一個子線程。
```javascript
var worker = new Worker('work.js');
```
Worker構造函數的參數是一個腳本文件,這個文件就是子線程所要完成的任務,上面代碼中是`work.js`。由于子線程不能讀取本地文件系統,所以這個腳本文件必須來自網絡端。如果下載沒有成功,比如出現404錯誤,這個子線程就會默默地失敗。
子線程新建之后,并沒有啟動,必需等待主線程調用`postMessage`方法,即發出信號之后才會啟動。`postMessage`方法的參數,就是主線程傳給子線程的信號。它可以是一個字符串,也可以是一個對象。
```javascript
worker.postMessage("Hello World");
worker.postMessage({method: 'echo', args: ['Work']});
```
只要符合父線程的同源政策,Worker線程自己也能新建Worker線程。Worker線程可以使用XMLHttpRequest進行網絡I/O,但是`XMLHttpRequest`對象的`responseXML`和`channel`屬性總是返回`null`。
## 子線程的事件監聽
在子線程內,必須有一個回調函數,監聽message事件。
```javascript
/* File: work.js */
self.addEventListener('message', function(e) {
self.postMessage('You said: ' + e.data);
}, false);
```
self代表子線程自身,self.addEventListener表示對子線程的message事件指定回調函數(直接指定onmessage屬性的值也可)。回調函數的參數是一個事件對象,它的data屬性包含主線程發來的信號。self.postMessage則表示,子線程向主線程發送一個信號。
根據主線程發來的不同的信號值,子線程可以調用不同的方法。
```javascript
/* File: work.js */
self.onmessage = function(event) {
var method = event.data.method;
var args = event.data.args;
var reply = doSomething(args);
self.postMessage({method: method, reply: reply});
};
```
## 主線程的事件監聽
主線程也必須指定message事件的回調函數,監聽子線程發來的信號。
```javascript
/* File: main.js */
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
```
## 錯誤處理
主線程可以監聽子線程是否發生錯誤。如果發生錯誤,會觸發主線程的error事件。
```javascript
worker.onerror(function(event) {
console.log(event);
});
// or
worker.addEventListener('error', function(event) {
console.log(event);
});
```
## 關閉子線程
使用完畢之后,為了節省系統資源,我們必須在主線程調用terminate方法,手動關閉子線程。
```javascript
worker.terminate();
```
也可以子線程內部關閉自身。
```javascript
self.close();
```
## 主線程與子線程的數據通信
前面說過,主線程與子線程之間的通信內容,可以是文本,也可以是對象。需要注意的是,這種通信是拷貝關系,即是傳值而不是傳址,子線程對通信內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通信內容串行化,然后把串行化后的字符串發給子線程,后者再將它還原。
主線程與子線程之間也可以交換二進制數據,比如File、Blob、ArrayBuffer等對象,也可以在線程之間發送。
但是,用拷貝方式發送二進制數據,會造成性能問題。比如,主線程向子線程發送一個500MB文件,默認情況下瀏覽器會生成一個原文件的拷貝。為了解決這個問題,JavaScript允許主線程把二進制數據直接轉移給子線程,但是一旦轉移,主線程就無法再使用這些二進制數據了,這是為了防止出現多個線程同時修改數據的麻煩局面。這種轉移數據的方法,叫做[Transferable Objects](http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#transferable-objects)。
如果要使用該方法,postMessage方法的最后一個參數必須是一個數組,用來指定前面發送的哪些值可以被轉移給子線程。
```javascript
worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);
```
## 同頁面的Web Worker
通常情況下,子線程載入的是一個單獨的JavaScript文件,但是也可以載入與主線程在同一個網頁的代碼。假設網頁代碼如下:
```html
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function() {
postMessage('Im reading Tech.pro');
}, false);
</script>
</body>
</html>
```
我們可以讀取頁面中的script,用worker來處理。
```javascript
var blob = new Blob([document.querySelector('#worker').textContent]);
```
這里需要把代碼當作二進制對象讀取,所以使用Blob接口。然后,這個二進制對象轉為URL,再通過這個URL創建worker。
```javascript
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
```
部署事件監聽代碼。
```javascript
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
```
最后,啟動worker。
```javascript
worker.postMessage('');
```
整個頁面的代碼如下:
```html
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function() {
postMessage('Work done!');
}, false);
</script>
<script>
(function() {
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
worker.postMessage('');
})();
</script>
</body>
</html>
```
可以看到,主線程和子線程的代碼都在同一個網頁上面。
上面所講的Web Worker都是專屬于某個網頁的,當該網頁關閉,worker就自動結束。除此之外,還有一種共享式的Web Worker,允許多個瀏覽器窗口共享同一個worker,只有當所有網口關閉,它才會結束。這種共享式的Worker用SharedWorker對象來建立,因為適用場合不多,這里就省略了。
## Service Worker
Service worker是一個在瀏覽器后臺運行的腳本,與網頁不相干,專注于那些不需要網頁或用戶互動就能完成的功能。它主要用于操作離線緩存。
Service Worker有以下特點。
- 屬于JavaScript Worker,不能直接接觸DOM,通過`postMessage`接口與頁面通信。
- 不需要任何頁面,就能執行。
- 不用的時候會終止執行,需要的時候又重新執行,即它是事件驅動的。
- 有一個精心定義的升級策略。
- 只在HTTPs協議下可用,這是因為它能攔截網絡請求,所以必須保證請求是安全的。
- 可以攔截發出的網絡請求,從而控制頁面的網路通信。
- 內部大量使用Promise。
Service worker的常見用途。
- 通過攔截網絡請求,使得網站運行得更快,或者在離線情況下,依然可以執行。
- 作為其他后臺功能的基礎,比如消息推送和背景同步。
使用Service Worker有以下步驟。
首先,需要向瀏覽器登記Service Worker。
```javascript
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
// 登記成功
console.log('ServiceWorker登記成功,范圍為', registration.scope);
}).catch(function(err) {
// 登記失敗
console.log('ServiceWorker登記失敗:', err);
});
}
```
上面代碼向瀏覽器登記`sw.js`腳本,實質就是瀏覽器加載`sw.js`。這段代碼可以多次調用,瀏覽器會自行判斷`sw.js`是否登記過,如果已經登記過,就不再重復執行了。注意,Service worker腳本必須與頁面在同一個域,且必須在HTTPs協議下正常運行。
`sw.js`位于域名的根目錄下,這表明這個Service worker的范圍(scope)是整個域,即會接收整個域下面的`fetch`事件。如果腳本的路徑是`/example/sw.js`,那么Service worker只對`/example/`開頭的URL有效(比如`/example/page1/`、`/example/page2/`)。如果腳本不在根目錄下,但是希望對整個域都有效,可以指定`scope`屬性。
```javascript
navigator.serviceWorker.register('/path/to/serviceworker.js', {
scope: '/'
});
```
一旦登記完成,這段腳本就會用戶的瀏覽器之中長期存在,不會隨著用戶離開你的網站而消失。
`.register`方法返回一個Promise對象。
登記成功后,瀏覽器執行下面步驟。
1. 下載資源(Download)
2. 安裝(Install)
3. 激活(Activate)
安裝和激活,主要通過事件來判斷。
```javascript
self.addEventListener('install', function(event) {
event.waitUntil(
fetchStuffAndInitDatabases()
);
});
self.addEventListener('activate', function(event) {
// You're good to go!
});
```
Service worker一旦激活,就開始控制頁面。網頁加載的時候,可以選擇一個Service worker作為自己的控制器。不過,頁面第一次加載的時候,它不受Service worker控制,因為這時還沒有一個Service worker在運行。只有重新加載頁面后,Service worker才會生效,控制加載它的頁面。
你可以查看`navigator.serviceWorker.controller`,了解當前哪個ServiceWorker掌握控制權。如果后臺沒有任何Service worker,`navigator.serviceWorker.controller`返回`null`。
Service worker激活以后,就能監聽`fetch`事件。
```javascript
self.addEventListener('fetch', function(event) {
console.log(event.request);
});
```
`fetch`事件會在兩種情況下觸發。
- 用戶訪問Service worker范圍內的網頁。
- 這些網頁發出的任何網絡請求(頁面本身、CSS、JS、圖像、XHR等等),即使這些請求是發向另一個域。但是,`iframe`和`<object>`標簽發出的請求不會被攔截。
`fetch`事件的`event`對象的`request`屬性,返回一個對象,包含了所攔截的網絡請求的所有信息,比如URL、請求方法和HTTP頭信息。
Service worker的強大之處,在于它會攔截請求,并會返回一個全新的回應。
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(new Response("Hello world!"));
});
```
`respondWith`方法的參數是一個Response對象實例,或者一個Promise對象(resolved以后返回一個Response實例)。上面代碼手動創造一個Response實例。
下面是完整的[代碼](https://github.com/jakearchibald/isserviceworkerready/tree/gh-pages/demos/manual-response)。
先看網頁代碼`index.html`。
```html
<!DOCTYPE html>
<html>
<head>
<style>
body {
white-space: pre-line;
font-family: monospace;
font-size: 14px;
}
</style>
</head>
<body><script>
function log() {
document.body.appendChild(document.createTextNode(Array.prototype.join.call(arguments, ", ") + '\n'));
console.log.apply(console, arguments);
}
window.onerror = function(err) {
log("Error", err);
};
navigator.serviceWorker.register('sw.js', {
scope: './'
}).then(function(sw) {
log("Registered!", sw);
log("You should get a different response when you refresh");
}).catch(function(err) {
log("Error", err);
});
</script></body>
</html>
```
然后是Service worker腳本`sw.js`。
```javascript
// The SW will be shutdown when not in use to save memory,
// be aware that any global state is likely to disappear
console.log("SW startup");
self.addEventListener('install', function(event) {
console.log("SW installed");
});
self.addEventListener('activate', function(event) {
console.log("SW activated");
});
self.addEventListener('fetch', function(event) {
console.log("Caught a fetch!");
event.respondWith(new Response("Hello world!"));
});
```
每一次瀏覽器向服務器要求一個文件的時候,就會觸發`fetch`事件。Service worker可以在發出這個請求之前,前攔截它。
```javascript
self.addEventListener('fetch', function (event) {
var request = event.request;
...
});
```
實際應用中,我們使用`fetch`方法去抓取資源,該方法返回一個Promise對象。
```javascript
self.addEventListener('fetch', function(event) {
if (/\.jpg$/.test(event.request.url)) {
event.respondWith(
fetch('//www.google.co.uk/logos/example.gif', {
mode: 'no-cors'
})
);
}
});
```
上面代碼中,如果網頁請求JPG文件,就會被Service worker攔截,轉而返回一個Google的Logo圖像。`fetch`方法默認會加上CORS信息頭,,上面設置了取消這個頭。
下面的代碼是一個將所有JPG、PNG圖片請求,改成WebP格式返回的例子。
```javascript
"use strict";
// Listen to fetch events
self.addEventListener('fetch', function(event) {
// Check if the image is a jpeg
if (/\.jpg$|.png$/.test(event.request.url)) {
// Inspect the accept header for WebP support
var supportsWebp = false;
if (event.request.headers.has('accept')){
supportsWebp = event.request.headers.get('accept').includes('webp');
}
// If we support WebP
if (supportsWebp) {
// Clone the request
var req = event.request.clone();
// Build the return URL
var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";
event.respondWith(fetch(returnUrl, {
mode: 'no-cors'
}));
}
}
});
```
如果請求失敗,可以通過Promise的`catch`方法處理。
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request).catch(function() {
return new Response("Request failed!");
})
);
});
```
登記成功后,可以在Chrome瀏覽器訪問`chrome://inspect/#service-workers`,查看整個瀏覽器目前正在運行的Service worker。訪問`chrome://serviceworker-internals`,可以查看瀏覽器目前安裝的所有Service worker。
一個已經登記過的Service worker腳本,如果發生改動,瀏覽器就會重新安裝,這被稱為“升級”。
Service worker有一個Cache API,用來緩存外部資源。
```javascript
self.addEventListener('install', function(event) {
// pre cache a load of stuff:
event.waitUntil(
caches.open('myapp-static-v1').then(function(cache) {
return cache.addAll([
'/',
'/styles/all.css',
'/styles/imgs/bg.png',
'/scripts/all.js'
]);
})
)
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
```
上面代碼中,`caches.open`方法用來建立緩存,然后使用`addAll`方法添加資源。`caches.match`方法則用來建立緩存以后,匹配當前請求是否在緩存之中,如果命中就取出緩存,否則就正常發出這個請求。一旦一個資源進入緩存,它原來指定是否過期的HTTP信息頭,就會被忽略。緩存之中的資源,只在你移除它們的時候,才會被移除。
單個資源可以使用`cache.put(request, response)`方法添加。
下面是一個在安裝階段緩存資源的例子。
```javascript
var staticCacheName = 'static';
var version = 'v1::';
self.addEventListener('install', function (event) {
event.waitUntil(updateStaticCache());
});
function updateStaticCache() {
return caches.open(version + staticCacheName)
.then(function (cache) {
return cache.addAll([
'/path/to/javascript.js',
'/path/to/stylesheet.css',
'/path/to/someimage.png',
'/path/to/someotherimage.png',
'/',
'/offline'
]);
});
};
```
上面代碼將JavaScript腳本、CSS樣式表、圖像文件、網站首頁、離線頁面,存入瀏覽器緩存。這些資源都要等全部進入緩存之后,才會安裝。
安裝以后,就需要激活。
```javascript
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys()
.then(function (keys) {
return Promise.all(keys
.filter(function (key) {
return key.indexOf(version) !== 0;
})
.map(function (key) {
return caches.delete(key);
})
);
})
);
});
```
<h2 id="7.7">SSE:服務器發送事件</h2>
## 概述
傳統的網頁都是瀏覽器向服務器“查詢”數據,但是很多場合,最有效的方式是服務器向瀏覽器“發送”數據。比如,每當收到新的電子郵件,服務器就向瀏覽器發送一個“通知”,這要比瀏覽器按時向服務器查詢(polling)更有效率。
服務器發送事件(Server-Sent Events,簡稱SSE)就是為了解決這個問題,而提出的一種新API,部署在EventSource對象上。目前,除了IE,其他主流瀏覽器都支持。
簡單說,所謂SSE,就是瀏覽器向服務器發送一個HTTP請求,然后服務器不斷單向地向瀏覽器推送“信息”(message)。這種信息在格式上很簡單,就是“信息”加上前綴“data: ”,然后以“\n\n”結尾。
```bash
$ curl http://example.com/dates
data: 1394572346452
data: 1394572347457
data: 1394572348463
^C
```
SSE與WebSocket有相似功能,都是用來建立瀏覽器與服務器之間的通信渠道。兩者的區別在于:
- WebSocket是全雙工通道,可以雙向通信,功能更強;SSE是單向通道,只能服務器向瀏覽器端發送。
- WebSocket是一個新的協議,需要服務器端支持;SSE則是部署在HTTP協議之上的,現有的服務器軟件都支持。
- SSE是一個輕量級協議,相對簡單;WebSocket是一種較重的協議,相對復雜。
- SSE默認支持斷線重連,WebSocket則需要額外部署。
- SSE支持自定義發送的數據類型。
從上面的比較可以看出,兩者各有特點,適合不同的場合。
## 客戶端代碼
### 概述
首先,使用下面的代碼,檢測瀏覽器是否支持SSE。
```javascript
if (!!window.EventSource) {
// ...
}
```
然后,部署SSE大概如下。
```javascript
var source = new EventSource('/dates');
source.onmessage = function(e){
console.log(e.data);
};
// 或者
source.addEventListener('message', function(e){})
```
### 建立連接
首先,瀏覽器向服務器發起連接,生成一個EventSource的實例對象。
```javascript
var source = new EventSource(url);
```
參數url就是服務器網址,必須與當前網頁的網址在同一個網域(domain),而且協議和端口都必須相同。
下面是一個建立連接的實例。
```javascript
if (!!window.EventSource) {
var source = new EventSource('http://127.0.0.1/sses/');
}
```
新生成的EventSource實例對象,有一個readyState屬性,表明連接所處的狀態。
```javascript
source.readyState
```
它可以取以下值:
- 0,相當于常量EventSource.CONNECTING,表示連接還未建立,或者連接斷線。
- 1,相當于常量EventSource.OPEN,表示連接已經建立,可以接受數據。
- 2,相當于常量EventSource.CLOSED,表示連接已斷,且不會重連。
### open事件
連接一旦建立,就會觸發open事件,可以定義相應的回調函數。
```javascript
source.onopen = function(event) {
// handle open event
};
// 或者
source.addEventListener("open", function(event) {
// handle open event
}, false);
```
### message事件
收到數據就會觸發message事件。
```javascript
source.onmessage = function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
};
// 或者
source.addEventListener("message", function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);
```
參數對象event有如下屬性:
- data:服務器端傳回的數據(文本格式)。
- origin: 服務器端URL的域名部分,即協議、域名和端口。
- lastEventId:數據的編號,由服務器端發送。如果沒有編號,這個屬性為空。
### error事件
如果發生通信錯誤(比如連接中斷),就會觸發error事件。
```javascript
source.onerror = function(event) {
// handle error event
};
// 或者
source.addEventListener("error", function(event) {
// handle error event
}, false);
```
### 自定義事件
服務器可以與瀏覽器約定自定義事件。這種情況下,發送回來的數據不會觸發message事件。
```javascript
source.addEventListener("foo", function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);
```
上面代碼表示,瀏覽器對foo事件進行監聽。
### close方法
close方法用于關閉連接。
```javascript
source.close();
```
## 數據格式
### 概述
服務器端發送的數據的HTTP頭信息如下:
```html
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
```
后面的行都是如下格式:
```html
field: value\n
```
field可以取四個值:“data”, “event”, “id”, or “retry”,也就是說有四類頭信息。每次HTTP通信可以包含這四類頭信息中的一類或多類。\n代表換行符。
以冒號開頭的行,表示注釋。通常,服務器每隔一段時間就會向瀏覽器發送一個注釋,保持連接不中斷。
```html
: This is a comment
```
下面是一些例子。
```html
: this is a test stream\n\n
data: some text\n\n
data: another message\n
data: with two lines \n\n
```
### data:數據欄
數據內容用data表示,可以占用一行或多行。如果數據只有一行,則像下面這樣,以“\n\n”結尾。
```html
data: message\n\n
```
如果數據有多行,則最后一行用“\n\n”結尾,前面行都用“\n”結尾。
```html
data: begin message\n
data: continue message\n\n
```
總之,最后一行的data,結尾要用兩個換行符號,表示數據結束。
以發送JSON格式的數據為例。
```html
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
```
### id:數據標識符
數據標識符用id表示,相當于每一條數據的編號。
```html
id: msg1\n
data: message\n\n
```
瀏覽器用lastEventId屬性讀取這個值。一旦連接斷線,瀏覽器會發送一個HTTP頭,里面包含一個特殊的“Last-Event-ID”頭信息,將這個值發送回來,用來幫助服務器端重建連接。因此,這個頭信息可以被視為一種同步機制。
### event欄:自定義信息類型
event頭信息表示自定義的數據類型,或者說數據的名字。
```html
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: bar\n
data: a bar event\n\n
```
上面的代碼創造了三條信息。第一條是foo,觸發瀏覽器端的foo事件;第二條未取名,表示默認類型,觸發瀏覽器端的message事件;第三條是bar,觸發瀏覽器端的bar事件。
### retry:最大間隔時間
服務器端可以用`retry`字段,指定瀏覽器重新發起連接的時間間隔。
```html
retry: 10000\n
```
兩種情況會導致瀏覽器重新發起連接:一種是瀏覽器開始正常完畢服務器發來的信息,二是由于網絡錯誤等原因,導致連接出錯。
## 服務器代碼
服務器端發送事件,要求服務器與瀏覽器保持連接。對于不同的服務器軟件來說,所消耗的資源是不一樣的。Apache服務器,每個連接就是一個線程,如果要維持大量連接,勢必要消耗大量資源。Node.js則是所有連接都使用同一個線程,因此消耗的資源會小得多,但是這要求每個連接不能包含很耗時的操作,比如磁盤的IO讀寫。
下面是Node.js的服務器發送事件的[代碼實例](http://cjihrig.com/blog/server-sent-events-in-node-js/)。
```javascript
var http = require("http");
http.createServer(function (req, res) {
var fileName = "." + req.url;
if (fileName === "./stream") {
res.writeHead(200, {"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive"});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");
interval = setInterval(function() {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(80, "127.0.0.1");
```
PHP代碼實例。
```php
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // 建議不要緩存SSE數據
/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
*/
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$serverTime = time();
sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
```
<h2 id="7.8">Page Visibility</h2>
PageVisibility API用于判斷頁面是否處于瀏覽器的當前窗口,即是否可見。
使用這個API,可以幫助開發者根據用戶行為調整程序。比如,如果頁面處于當前窗口,可以讓程序每隔15秒向服務器請求數據;如果不處于當前窗口,則讓程序每隔幾分鐘請求一次數據。
## 屬性
這個API部署在document對象上,提供以下兩個屬性。
- **document.hidden**:返回一個布爾值,表示當前是否被隱藏。
- **document.visibilityState**:表示頁面當前的狀態,可以取三個值,分別是visibile(頁面可見)、hidden(頁面不可見)、prerender(頁面正處于渲染之中,不可見)。
這兩個屬性都帶有瀏覽器前綴。使用的時候,必須進行前綴識別。
```javascript
function getHiddenProp(){
var prefixes = ['webkit','moz','ms','o'];
// if 'hidden' is natively supported just return it
if ('hidden' in document) return 'hidden';
// otherwise loop over all the known prefixes until we find one
for (var i = 0; i < prefixes.length; i++){
if ((prefixes[i] + 'Hidden') in document)
return prefixes[i] + 'Hidden';
}
// otherwise it's not supported
return null;
}
```
## VisibilityChange事件
當頁面的可見狀態發生變化時,會觸發VisibilityChange事件(帶有瀏覽器前綴)。
```javascript
document.addEventListener("visibilitychange", function() {
console.log( document.visibilityState );
});
```
<h2 id="7.9">Fullscreen API:全屏操作</h2>
全屏API可以控制瀏覽器的全屏顯示,讓一個Element節點(以及子節點)占滿用戶的整個屏幕。目前各大瀏覽器的最新版本都支持這個API(包括IE11),但是使用的時候需要加上瀏覽器前綴。
## 方法
### requestFullscreen()
Element節點的requestFullscreen方法,可以使得這個節點全屏。
```javascript
function launchFullscreen(element) {
if(element.requestFullscreen) {
element.requestFullscreen();
} else if(element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if(element.msRequestFullscreen){
element.msRequestFullscreen();
} else if(element.webkitRequestFullscreen) {
element.webkitRequestFullScreen();
}
}
launchFullscreen(document.documentElement);
launchFullscreen(document.getElementById("videoElement"));
```
放大一個節點時,Firefox和Chrome在行為上略有不同。Firefox自動為該節點增加一條CSS規則,將該元素放大至全屏狀態,`width: 100%; height: 100%`,而Chrome則是將該節點放在屏幕的中央,保持原來大小,其他部分變黑。為了讓Chrome的行為與Firefox保持一致,可以自定義一條CSS規則。
```css
:-webkit-full-screen #myvideo {
width: 100%;
height: 100%;
}
```
### exitFullscreen()
document對象的exitFullscreen方法用于取消全屏。該方法也帶有瀏覽器前綴。
```javascript
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
exitFullscreen();
```
用戶手動按下ESC鍵或F11鍵,也可以退出全屏鍵。此外,加載新的頁面,或者切換tab,或者從瀏覽器轉向其他應用(按下Alt-Tab),也會導致退出全屏狀態。
## 屬性
### document.fullscreenElement
fullscreenElement屬性返回正處于全屏狀態的Element節點,如果當前沒有節點處于全屏狀態,則返回null。
```javascript
var fullscreenElement =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement;
```
### document.fullscreenEnabled
fullscreenEnabled屬性返回一個布爾值,表示當前文檔是否可以切換到全屏狀態。
```javascript
var fullscreenEnabled =
document.fullscreenEnabled ||
document.mozFullScreenEnabled ||
document.webkitFullscreenEnabled ||
document.msFullscreenEnabled;
if (fullscreenEnabled) {
videoElement.requestFullScreen();
} else {
console.log('瀏覽器當前不能全屏');
}
```
## 全屏事件
以下事件與全屏操作有關。
- fullscreenchange事件:瀏覽器進入或離開全屏時觸發。
- fullscreenerror事件:瀏覽器無法進入全屏時觸發,可能是技術原因,也可能是用戶拒絕。
```javascript
document.addEventListener("fullscreenchange", function( event ) {
if (document.fullscreenElement) {
console.log('進入全屏');
} else {
console.log('退出全屏');
}
});
```
上面代碼在發生fullscreenchange事件時,通過fullscreenElement屬性判斷,到底是進入全屏還是退出全屏。
## 全屏狀態的CSS
全屏狀態下,大多數瀏覽器的CSS支持`:full-screen`偽類,只有IE11支持`:fullscreen`偽類。使用這個偽類,可以對全屏狀態設置單獨的CSS屬性。
```css
:-webkit-full-screen {
/* properties */
}
:-moz-full-screen {
/* properties */
}
:-ms-fullscreen {
/* properties */
}
:full-screen { /*pre-spec */
/* properties */
}
:fullscreen { /* spec */
/* properties */
}
/* deeper elements */
:-webkit-full-screen video {
width: 100%;
height: 100%;
}
```
<h2 id="7.10">Web Speech</h2>
## 概述
這個API用于瀏覽器接收語音輸入。
它最早是由Google提出的,目的是讓用戶直接進行語音搜索,即對著麥克風說出你所要搜索的詞,搜索結果就自動出現。Google首先部署的是input元素的speech屬性(加上瀏覽器前綴x-webkit)。
```html
<input id="query" type="search" class="k-input k-textbox"
x-webkit-speech speech />
```
加上這個屬性以后,輸入框的右端會出現了一個麥克風標志,點擊該標志,就會跳出語音輸入窗口。
由于這個操作過于簡單,Google又在它的基礎上提出了Web Speech API,使得JavaScript可以操作語音輸入。
目前,只有Chrome瀏覽器支持該API。
## SpeechRecognition對象
這個API部署在SpeechRecognition對象之上。
```javascript
var SpeechRecognition = window.SpeechRecognition ||
window.webkitSpeechRecognition ||
window.mozSpeechRecognition ||
window.oSpeechRecognition ||
window.msSpeechRecognition;
```
為了將來的兼容性考慮,上面的代碼列出了所有瀏覽器的前綴。但是實際上,目前只有window.webkitSpeechRecognition是可用的。
確定瀏覽器支持以后,新建一個SpeechRecognition的實例對象。
```javascript
if (SpeechRecognition) {
var recognition = new SpeechRecognition();
recognition.maxAlternatives = 5;
}
```
maxAlternatives屬性等于5,表示最多返回5個語音匹配結果。
## 事件
目前,該API部署了11個事件。下面對其中的3個定義回調函數(假定speak是語音輸入框)。
```javascript
var speak = $('#speak');
recognition.onaudiostart = function() {
speak.val("Speak now...");
};
recognition.onnomatch = function() {
speak.val("Try again please...");
};
recognition.onerror = function() {
speak.val("Error. Try Again...");
};
```
首先,瀏覽器會詢問用戶是否許可瀏覽器獲取麥克風數據。如果用戶許可,就會觸發audiostart事件,準備接收語音輸入。如果找不到與語音匹配的值,就會觸發nomatch事件;如果發生錯誤,則會觸發error事件。
如果得到與語音匹配的值,則會觸發result事件。
```javascript
recognition.onresult = function(event) {
if (event.results.length > 0) {
var results = event.results[0],
topResult = results[0];
if (topResult.confidence > 0.5) {
speechSearch(results, topResult);
} else {
speak.val("Try again please...");
}
}
};
```
result事件回調函數的參數,是一個SpeechRecognitionEvent對象。它的results屬性就是語音匹配的結果,是一個數組,按照匹配度排序,最匹配的結果排在第一位。該數組的每一個成員是SpeechRecognitionResult對象,該對象的transcript屬性是實際匹配的文本,confidence屬性是可信度(在0與1之間)。
<h2 id="7.11">requestAnimationFrame</h2>
## 概述
requestAnimationFrame是瀏覽器用于定時循環操作的一個接口,類似于setTimeout,主要用途是按幀對網頁進行重繪。
設置這個API的目的是為了讓各種網頁動畫效果(DOM動畫、Canvas動畫、SVG動畫、WebGL動畫)能夠有一個統一的刷新機制,從而節省系統資源,提高系統性能,改善視覺效果。代碼中使用這個API,就是告訴瀏覽器希望執行一個動畫,讓瀏覽器在下一個動畫幀安排一次網頁重繪。
requestAnimationFrame的優勢,在于充分利用顯示器的刷新機制,比較節省系統資源。顯示器有固定的刷新頻率(60Hz或75Hz),也就是說,每秒最多只能重繪60次或75次,requestAnimationFrame的基本思想就是與這個刷新頻率保持同步,利用這個刷新頻率進行頁面重繪。此外,使用這個API,一旦頁面不處于瀏覽器的當前標簽,就會自動停止刷新。這就節省了CPU、GPU和電力。
不過有一點需要注意,requestAnimationFrame是在主線程上完成。這意味著,如果主線程非常繁忙,requestAnimationFrame的動畫效果會大打折扣。
requestAnimationFrame使用一個回調函數作為參數。這個回調函數會在瀏覽器重繪之前調用。
```javascript
requestID = window.requestAnimationFrame(callback);
```
目前,主要瀏覽器Firefox 23 / IE 10 / Chrome / Safari)都支持這個方法。可以用下面的方法,檢查瀏覽器是否支持這個API。如果不支持,則自行模擬部署該方法。
```javascript
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
```
上面的代碼按照1秒鐘60次(大約每16.7毫秒一次),來模擬requestAnimationFrame。
使用requestAnimationFrame的時候,只需反復調用它即可。
```javascript
function repeatOften() {
// Do whatever
requestAnimationFrame(repeatOften);
}
requestAnimationFrame(repeatOften);
```
## cancelAnimationFrame方法
cancelAnimationFrame方法用于取消重繪。
```javascript
window.cancelAnimationFrame(requestID);
```
它的參數是requestAnimationFrame返回的一個代表任務ID的整數值。
```javascript
var globalID;
function repeatOften() {
$("<div />").appendTo("body");
globalID = requestAnimationFrame(repeatOften);
}
$("#start").on("click", function() {
globalID = requestAnimationFrame(repeatOften);
});
$("#stop").on("click", function() {
cancelAnimationFrame(globalID);
});
```
上面代碼持續在body元素下添加div元素,直到用戶點擊stop按鈕為止。
## 實例
下面,舉一個實例。
假定網頁中有一個動畫區塊。
```html
<div id="anim">點擊運行動畫</div>
```
然后,定義動畫效果。
```javascript
var elem = document.getElementById("anim");
var startTime = undefined;
function render(time) {
if (time === undefined)
time = Date.now();
if (startTime === undefined)
startTime = time;
elem.style.left = ((time - startTime)/10 % 500) + "px";
}
```
最后,定義click事件。
```javascript
elem.onclick = function() {
(function animloop(){
render();
requestAnimFrame(animloop);
})();
};
```
運行效果可查看[jsfiddle](http://jsfiddle.net/paul/rjbGw/3/)。
<h2 id="7.12">WebSocket</h2>
## 概述
HTTP協議是一種無狀態協議,服務器端本身不具有識別客戶端的能力,必須借助外部機制,比如session和cookie,才能與特定客戶端保持對話。這多多少少帶來一些不便,尤其在服務器端與客戶端需要持續交換數據的場合(比如網絡聊天),更是如此。為了解決這個問題,HTML5提出了瀏覽器的[WebSocket API](http://dev.w3.org/html5/websockets/)。
WebSocket的主要作用是,允許服務器端與客戶端進行全雙工(full-duplex)的通信。舉例來說,HTTP協議有點像發電子郵件,發出后必須等待對方回信;WebSocket則是像打電話,服務器端和客戶端可以同時向對方發送數據,它們之間存著一條持續打開的數據通道。
WebSocket協議完全可以取代Ajax方法,用來向服務器端發送文本和二進制數據,而且還沒有“同域限制”。
WebSocket不使用HTTP協議,而是使用自己的協議。瀏覽器發出的WebSocket請求類似于下面的樣子:
```http
GET / HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
```
上面的頭信息顯示,有一個HTTP頭是Upgrade。HTTP1.1協議規定,Upgrade頭信息表示將通信協議從HTTP/1.1轉向該項所指定的協議。“Connection: Upgrade”就表示瀏覽器通知服務器,如果可以,就升級到webSocket協議。Origin用于驗證瀏覽器域名是否在服務器許可的范圍內。Sec-WebSocket-Key則是用于握手協議的密鑰,是base64編碼的16字節隨機字符串。
服務器端的WebSocket回應則是
```http
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
```
服務器端同樣用“Connection: Upgrade”通知瀏覽器,需要改變協議。Sec-WebSocket-Accept是服務器在瀏覽器提供的Sec-WebSocket-Key字符串后面,添加“258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 字符串,然后再取sha-1的hash值。瀏覽器將對這個值進行驗證,以證明確實是目標服務器回應了webSocket請求。Sec-WebSocket-Location表示進行通信的WebSocket網址。
> 請注意,WebSocket協議用ws表示。此外,還有wss協議,表示加密的WebSocket協議,對應HTTPs協議。
完成握手以后,WebSocket協議就在TCP協議之上,開始傳送數據。
WebSocket協議需要服務器支持,目前比較流行的實現是基于node.js的[socket.io](http://socket.io/),更多的實現可參閱[Wikipedia](http://en.wikipedia.org/wiki/WebSocket#Server_side)。至于瀏覽器端,目前主流瀏覽器都支持WebSocket協議(包括IE 10+),僅有的例外是手機端的Opera Mini和Android Browser。
## 客戶端
瀏覽器端對WebSocket協議的處理,無非就是三件事:
- 建立連接和斷開連接
- 發送數據和接收數據
- 處理錯誤
### 建立連接和斷開連接
首先,客戶端要檢查瀏覽器是否支持WebSocket,使用的方法是查看window對象是否具有WebSocket屬性。
```javascript
if(window.WebSocket != undefined) {
// WebSocket代碼
}
```
然后,開始與服務器建立連接(這里假定服務器就是本機的1740端口,需要使用ws協議)。
```javascript
if(window.WebSocket != undefined) {
var connection = new WebSocket('ws://localhost:1740');
}
```
建立連接以后的WebSocket實例對象(即上面代碼中的connection),有一個readyState屬性,表示目前的狀態,可以取4個值:
- **0**: 正在連接
- **1**: 連接成功
- **2**: 正在關閉
- **3**: 連接關閉
握手協議成功以后,readyState就從0變為1,并觸發open事件,這時就可以向服務器發送信息了。我們可以指定open事件的回調函數。
```javascript
connection.onopen = wsOpen;
function wsOpen (event) {
console.log('Connected to: ' + event.currentTarget.URL);
}
```
關閉WebSocket連接,會觸發close事件。
```javascript
connection.onclose = wsClose;
function wsClose () {
console.log("Closed");
}
connection.close();
```
### 發送數據和接收數據
連接建立后,客戶端通過send方法向服務器端發送數據。
```javascript
connection.send(message);
```
除了發送字符串,也可以使用 Blob 或 ArrayBuffer 對象發送二進制數據。
```javascript
// 使用ArrayBuffer發送canvas圖像數據
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
connection.send(binary.buffer);
// 使用Blob發送文件
var file = document.querySelector('input[type="file"]').files[0];
connection.send(file);
```
客戶端收到服務器發送的數據,會觸發message事件。可以通過定義message事件的回調函數,來處理服務端返回的數據。
```javascript
connection.onmessage = wsMessage;
function wsMessage (event) {
console.log(event.data);
}
```
上面代碼的回調函數wsMessage的參數為事件對象event,該對象的data屬性包含了服務器返回的數據。
如果接收的是二進制數據,需要將連接對象的格式設為blob或arraybuffer。
```javascript
connection.binaryType = 'arraybuffer';
connection.onmessage = function(e) {
console.log(e.data.byteLength); // ArrayBuffer對象有byteLength屬性
};
```
### 處理錯誤
如果出現錯誤,瀏覽器會觸發WebSocket實例對象的error事件。
```javascript
connection.onerror = wsError;
function wsError(event) {
console.log("Error: " + event.data);
}
```
## 服務器端
服務器端需要單獨部署處理WebSocket的代碼。下面用node.js搭建一個服務器環境。
```javascript
var http = require('http');
var server = http.createServer(function(request, response) {});
```
假設監聽1740端口。
```javascript
server.listen(1740, function() {
console.log((new Date()) + ' Server is listening on port 1740');
});
```
接著啟動WebSocket服務器。這需要加載websocket庫,如果沒有安裝,可以先使用npm命令安裝。
```javascript
var WebSocketServer = require('websocket').server;
var wsServer = new WebSocketServer({
httpServer: server
});
```
WebSocket服務器建立request事件的回調函數。
```javascript
var connection;
wsServer.on('request', function(req){
connection = req.accept('echo-protocol', req.origin);
});
```
上面代碼的回調函數接受一個參數req,表示request請求對象。然后,在回調函數內部,建立WebSocket連接connection。接著,就要對connection的message事件指定回調函數。
```javascript
wsServer.on('request', function(r){
connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
var msgString = message.utf8Data;
connection.sendUTF(msgString);
});
});
```
最后,監聽用戶的disconnect事件。
```javascript
connection.on('close', function(reasonCode, description) {
console.log(connection.remoteAddress + ' disconnected.');
});
```
使用[ws](https://github.com/einaros/ws)模塊,部署一個簡單的WebSocket服務器非常容易。
```javascript
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
```
## Socket.io簡介
[Socket.io](http://socket.io/)是目前最流行的WebSocket實現,包括服務器和客戶端兩個部分。它不僅簡化了接口,使得操作更容易,而且對于那些不支持WebSocket的瀏覽器,會自動降為Ajax連接,最大限度地保證了兼容性。它的目標是統一通信機制,使得所有瀏覽器和移動設備都可以進行實時通信。
第一步,在服務器端的項目根目錄下,安裝socket.io模塊。
```bash
$ npm install socket.io
```
第二步,在根目錄下建立`app.js`,并寫入以下代碼(假定使用了Express框架)。
```javascript
var app = require('express')();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
server.listen(80);
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
```
上面代碼表示,先建立并運行HTTP服務器。Socket.io的運行建立在HTTP服務器之上。
第三步,將Socket.io插入客戶端網頁。
```html
<script src="/socket.io/socket.io.js"></script>
```
然后,在客戶端腳本中,建立WebSocket連接。
```javascript
var socket = io.connect('http://localhost');
```
由于本例假定WebSocket主機與客戶端是同一臺機器,所以connect方法的參數是`http://localhost`。接著,指定news事件(即服務器端發送news)的回調函數。
```javascript
socket.on('news', function (data){
console.log(data);
});
```
最后,用emit方法向服務器端發送信號,觸發服務器端的anotherNews事件。
```javascript
socket.emit('anotherNews');
```
> 請注意,emit方法可以取代Ajax請求,而on方法指定的回調函數,也等同于Ajax的回調函數。
第四步,在服務器端的app.js,加入以下代碼。
```javascript
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('anotherNews', function (data) {
console.log(data);
});
});
```
上面代碼的io.sockets.on方法指定connection事件(WebSocket連接建立)的回調函數。在回調函數中,用emit方法向客戶端發送數據,觸發客戶端的news事件。然后,再用on方法指定服務器端anotherNews事件的回調函數。
不管是服務器還是客戶端,socket.io提供兩個核心方法:emit方法用于發送消息,on方法用于監聽對方發送的消息。
<h2 id="7.13">WebRTC</h2>
## 概述
WebRTC是“網絡實時通信”(Web Real Time Communication)的縮寫。它最初是為了解決瀏覽器上視頻通話而提出的,即兩個瀏覽器之間直接進行視頻和音頻的通信,不經過服務器。后來發展到除了音頻和視頻,還可以傳輸文字和其他數據。
Google是WebRTC的主要支持者和開發者,它最初在Gmail上推出了視頻聊天,后來在2011年推出了Hangouts,語序在瀏覽器中打電話。它推動了WebRTC標準的確立。
WebRTC主要讓瀏覽器具備三個作用。
- 獲取音頻和視頻
- 進行音頻和視頻通信
- 進行任意數據的通信
WebRTC共分成三個API,分別對應上面三個作用。
- MediaStream (又稱getUserMedia)
- RTCPeerConnection
- RTCDataChannel
## getUserMedia
### 概述
navigator.getUserMedia方法目前主要用于,在瀏覽器中獲取音頻(通過麥克風)和視頻(通過攝像頭),將來可以用于獲取任意數據流,比如光盤和傳感器。
下面的代碼用于檢查瀏覽器是否支持getUserMedia方法。
```javascript
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (navigator.getUserMedia) {
// 支持
} else {
// 不支持
}
```
Chrome 21, Opera 18和Firefox 17,支持該方法。目前,IE還不支持,上面代碼中的msGetUserMedia,只是為了確保將來的兼容。
getUserMedia方法接受三個參數。
```javascript
navigator.getUserMedia({
video: true,
audio: true
}, onSuccess, onError);
```
getUserMedia的第一個參數是一個對象,表示要獲取哪些多媒體設備,上面的代碼表示獲取攝像頭和麥克風;onSuccess是一個回調函數,在獲取多媒體設備成功時調用;onError也是一個回調函數,在取多媒體設備失敗時調用。
下面是一個例子。
```javascript
var constraints = {video: true};
function onSuccess(stream) {
var video = document.querySelector("video");
video.src = window.URL.createObjectURL(stream);
}
function onError(error) {
console.log("navigator.getUserMedia error: ", error);
}
navigator.getUserMedia(constraints, onSuccess, onError);
```
如果網頁使用了getUserMedia方法,瀏覽器就會詢問用戶,是否同意瀏覽器調用麥克風或攝像頭。如果用戶同意,就調用回調函數onSuccess;如果用戶拒絕,就調用回調函數onError。
onSuccess回調函數的參數是一個數據流對象stream。`stream.getAudioTracks`方法和`stream.getVideoTracks`方法,分別返回一個數組,其成員是數據流包含的音軌和視軌(track)。使用的聲音源和攝影頭的數量,決定音軌和視軌的數量。比如,如果只使用一個攝像頭獲取視頻,且不獲取音頻,那么視軌的數量為1,音軌的數量為0。每個音軌和視軌,有一個kind屬性,表示種類(video或者audio),和一個label屬性(比如FaceTime HD Camera (Built-in))。
onError回調函數接受一個Error對象作為參數。Error對象的code屬性有如下取值,說明錯誤的類型。
- **PERMISSION_DENIED**:用戶拒絕提供信息。
- **NOT_SUPPORTED_ERROR**:瀏覽器不支持硬件設備。
- **MANDATORY_UNSATISFIED_ERROR**:無法發現指定的硬件設備。
### 范例:獲取攝像頭
下面通過getUserMedia方法,將攝像頭拍攝的圖像展示在網頁上。
首先,需要先在網頁上放置一個video元素。圖像就展示在這個元素中。
```html
<video id="webcam"></video>
```
然后,用代碼獲取這個元素。
```javascript
function onSuccess(stream) {
var video = document.getElementById('webcam');
}
```
接著,將這個元素的src屬性綁定數據流,攝影頭拍攝的圖像就可以顯示了。
```javascript
function onSuccess(stream) {
var video = document.getElementById('webcam');
if (window.URL) {
video.src = window.URL.createObjectURL(stream);
} else {
video.src = stream;
}
video.autoplay = true;
// 或者 video.play();
}
if (navigator.getUserMedia) {
navigator.getUserMedia({video:true}, onSuccess);
} else {
document.getElementById('webcam').src = 'somevideo.mp4';
}
```
在Chrome和Opera中,URL.createObjectURL方法將媒體數據流(MediaStream)轉為一個二進制對象的URL(Blob URL),該URL可以作為video元素的src屬性的值。 在Firefox中,媒體數據流可以直接作為src屬性的值。Chrome和Opera還允許getUserMedia獲取的音頻數據,直接作為audio或者video元素的值,也就是說如果還獲取了音頻,上面代碼播放出來的視頻是有聲音的。
獲取攝像頭的主要用途之一,是讓用戶使用攝影頭為自己拍照。Canvas API有一個ctx.drawImage(video, 0, 0)方法,可以將視頻的一個幀轉為canvas元素。這使得截屏變得非常容易。
```html
<video autoplay></video>
<img src="">
<canvas style="display:none;"></canvas>
<script>
var video = document.querySelector('video');
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var localMediaStream = null;
function snapshot() {
if (localMediaStream) {
ctx.drawImage(video, 0, 0);
// “image/webp”對Chrome有效,
// 其他瀏覽器自動降為image/png
document.querySelector('img').src = canvas.toDataURL('image/webp');
}
}
video.addEventListener('click', snapshot, false);
navigator.getUserMedia({video: true}, function(stream) {
video.src = window.URL.createObjectURL(stream);
localMediaStream = stream;
}, errorCallback);
</script>
```
### 范例:捕獲麥克風聲音
通過瀏覽器捕獲聲音,需要借助Web Audio API。
```javascript
window.AudioContext = window.AudioContext ||
window.webkitAudioContext;
var context = new AudioContext();
function onSuccess(stream) {
var audioInput = context.createMediaStreamSource(stream);
audioInput.connect(context.destination);
}
navigator.getUserMedia({audio:true}, onSuccess);
```
### 捕獲的限定條件
getUserMedia方法的第一個參數,除了指定捕獲對象之外,還可以指定一些限制條件,比如限定只能錄制高清(或者VGA標準)的視頻。
```javascript
var hdConstraints = {
video: {
mandatory: {
minWidth: 1280,
minHeight: 720
}
}
};
navigator.getUserMedia(hdConstraints, onSuccess, onError);
var vgaConstraints = {
video: {
mandatory: {
maxWidth: 640,
maxHeight: 360
}
}
};
navigator.getUserMedia(vgaConstraints, onSuccess, onError);
```
### MediaStreamTrack.getSources()
如果本機有多個攝像頭/麥克風,這時就需要使用MediaStreamTrack.getSources方法指定,到底使用哪一個攝像頭/麥克風。
```javascript
MediaStreamTrack.getSources(function(sourceInfos) {
var audioSource = null;
var videoSource = null;
for (var i = 0; i != sourceInfos.length; ++i) {
var sourceInfo = sourceInfos[i];
if (sourceInfo.kind === 'audio') {
console.log(sourceInfo.id, sourceInfo.label || 'microphone');
audioSource = sourceInfo.id;
} else if (sourceInfo.kind === 'video') {
console.log(sourceInfo.id, sourceInfo.label || 'camera');
videoSource = sourceInfo.id;
} else {
console.log('Some other kind of source: ', sourceInfo);
}
}
sourceSelected(audioSource, videoSource);
});
function sourceSelected(audioSource, videoSource) {
var constraints = {
audio: {
optional: [{sourceId: audioSource}]
},
video: {
optional: [{sourceId: videoSource}]
}
};
navigator.getUserMedia(constraints, onSuccess, onError);
}
```
上面代碼表示,MediaStreamTrack.getSources方法的回調函數,可以得到一個本機的攝像頭和麥克風的列表,然后指定使用最后一個攝像頭和麥克風。
## RTCPeerConnectionl,RTCDataChannel
### RTCPeerConnectionl
RTCPeerConnection的作用是在瀏覽器之間建立數據的“點對點”(peer to peer)通信,也就是將瀏覽器獲取的麥克風或攝像頭數據,傳播給另一個瀏覽器。這里面包含了很多復雜的工作,比如信號處理、多媒體編碼/解碼、點對點通信、數據安全、帶寬管理等等。
不同客戶端之間的音頻/視頻傳遞,是不用通過服務器的。但是,兩個客戶端之間建立聯系,需要通過服務器。服務器主要轉遞兩種數據。
- 通信內容的元數據:打開/關閉對話(session)的命令、媒體文件的元數據(編碼格式、媒體類型和帶寬)等。
- 網絡通信的元數據:IP地址、NAT網絡地址翻譯和防火墻等。
WebRTC協議沒有規定與服務器的通信方式,因此可以采用各種方式,比如WebSocket。通過服務器,兩個客戶端按照Session Description Protocol(SDP協議)交換雙方的元數據。
下面是一個示例。
```javascript
var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;
// run start(true) to initiate a call
function start(isCaller) {
pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};
// get the local stream, show it in the local video element and send it
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer(gotDescription);
else
pc.createAnswer(pc.remoteDescription, gotDescription);
function gotDescription(desc) {
pc.setLocalDescription(desc);
signalingChannel.send(JSON.stringify({ "sdp": desc }));
}
});
}
signalingChannel.onmessage = function (evt) {
if (!pc)
start(false);
var signal = JSON.parse(evt.data);
if (signal.sdp)
pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else
pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};
```
RTCPeerConnection帶有瀏覽器前綴,Chrome瀏覽器中為webkitRTCPeerConnection,Firefox瀏覽器中為mozRTCPeerConnection。Google維護一個函數庫[adapter.js](https://apprtc.appspot.com/js/adapter.js),用來抽象掉瀏覽器之間的差異。
### RTCDataChannel
RTCDataChannel的作用是在點對點之間,傳播任意數據。它的API與WebSockets的API相同。
下面是一個示例。
```javascript
var pc = new webkitRTCPeerConnection(servers,
{optional: [{RtpDataChannels: true}]});
pc.ondatachannel = function(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
document.querySelector("div#receive").innerHTML = event.data;
};
};
sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});
document.querySelector("button#send").onclick = function (){
var data = document.querySelector("textarea#send").value;
sendChannel.send(data);
};
```
Chrome 25、Opera 18和Firefox 22支持RTCDataChannel。
### 外部函數庫
由于這兩個API比較復雜,一般采用外部函數庫進行操作。目前,視頻聊天的函數庫有[SimpleWebRTC](https://github.com/henrikjoreteg/SimpleWebRTC)、[easyRTC](https://github.com/priologic/easyrtc)、[webRTC.io](https://github.com/webRTC/webRTC.io),點對點通信的函數庫有[PeerJS](http://peerjs.com/)、[Sharefest](https://github.com/peer5/sharefest)。
下面是SimpleWebRTC的示例。
```javascript
var webrtc = new WebRTC({
localVideoEl: 'localVideo',
remoteVideosEl: 'remoteVideos',
autoRequestMedia: true
});
webrtc.on('readyToCall', function () {
webrtc.joinRoom('My room name');
});
```
下面是PeerJS的示例。
```javascript
var peer = new Peer('someid', {key: 'apikey'});
peer.on('connection', function(conn) {
conn.on('data', function(data){
// Will print 'hi!'
console.log(data);
});
});
// Connecting peer
var peer = new Peer('anotherid', {key: 'apikey'});
var conn = peer.connect('someid');
conn.on('open', function(){
conn.send('hi!');
});
```
<h2 id="7.14">Web Components</h2>
## 概述
各種網站往往需要一些相同的模塊,比如日歷、調色板等等,這種模塊就被稱為“組件”(component)。Web Component就是網頁組件式開發的技術規范。
采用組件進行網站開發,有很多優點。
(1)管理和使用非常容易。加載或卸載組件,只要添加或刪除一行代碼就可以了。
```html
<link rel="import" href="my-dialog.htm">
<my-dialog heading="A Dialog">Lorem ipsum</my-dialog>
```
上面代碼加載了一個對話框組件。
(2)定制非常容易。組件往往留出接口,供使用者設置常見屬性,比如上面代碼的heading屬性,就是用來設置對話框的標題。
(3)組件是模塊化編程思想的體現,非常有利于代碼的重用。標準格式的模塊,可以跨平臺、跨框架使用,構建、部署和與其他UI元素互動都有統一做法。
(4)組件提供了HTML、CSS、JavaScript封裝的方法,實現了與同一頁面上其他代碼的隔離。
未來的網站開發,可以像搭積木一樣,把組件合在一起,就組成了一個網站。這是非常誘人的。
Web Components不是單一的規范,而是一系列的技術組成,包括Template、Custom Element、Shadow DOM、HTML Import四種技術規范。使用時,并不一定這四者都要用到。其中,Custom Element和Shadow DOM最重要,Template和HTML Import只起到輔助作用。
## template標簽
### 基本用法
template標簽表示網頁中某些重復出現的部分的代碼模板。它存在于DOM之中,但是在頁面中不可見。
下面的代碼用來檢查,瀏覽器是否支持template標簽。
```javascript
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// 支持
} else {
// 不支持
}
```
下面是一個模板的例子。
```html
<template id="profileTemplate">
<div class="profile">
<img src="" class="profile__img">
<div class="profile__name"></div>
<div class="profile__social"></div>
</div>
</template>
```
使用的時候,需要用JavaScript在模板中插入內容,然后將其插入DOM。
```javascript
var template = document.querySelector('#profileTemplate');
template.content.querySelector('.profile__img').src = 'profile.jpg';
template.content.querySelector('.profile__name').textContent = 'Barack Obama';
template.content.querySelector('.profile__social').textContent = 'Follow me on Twitter';
document.body.appendChild(template.content);
```
上面的代碼是將模板直接插入DOM,更好的做法是克隆template節點,然后將克隆的節點插入DOM。這樣做可以多次使用模板。
```javascript
var clone = document.importNode(template.content, true);
document.body.appendChild(clone);
```
接受template插入的元素,叫做宿主元素(host)。在template之中,可以對宿主元素設置樣式。
```html
<template>
<style>
:host {
background: #f8f8f8;
}
:host(:hover) {
background: #ccc;
}
</style>
</template>
```
### document.importNode()
document.importNode方法用于克隆外部文檔的DOM節點。
```javascript
var iframe = document.getElementsByTagName("iframe")[0];
var oldNode = iframe.contentWindow.document.getElementById("myNode");
var newNode = document.importNode(oldNode, true);
document.getElementById("container").appendChild(newNode);
```
上面例子是將iframe窗口之中的節點oldNode,克隆進入當前文檔。
注意,克隆節點之后,還必須用appendChild方法將其加入當前文檔,否則不會顯示。換個角度說,這意味著插入外部文檔節點之前,必須用document.importNode方法先將這個節點準備好。
document.importNode方法接受兩個參數,第一個參數是外部文檔的DOM節點,第二個參數是一個布爾值,表示是否連同子節點一起克隆,默認為false。大多數情況下,必須顯式地將第二個參數設為true。
## Custom Element
HTML預定義的網頁元素,有時并不符合我們的需要,這時可以自定義網頁元素,這就叫做Custom Element。它是Web component技術的核心。舉例來說,你可以自定義一個叫做super-button的網頁元素。
```html
<super-button></super-button>
```
注意,自定義網頁元素的標簽名必須含有連字符(-),一個或多個都可。這是因為瀏覽器內置的的HTML元素標簽名,都不含有連字符,這樣可以做到有效區分。
下面的代碼用于測試瀏覽器是否支持自定義元素。
```javascript
if ('registerElement' in document) {
// 支持
} else {
// 不支持
}
```
### document.registerElement()
使用自定義元素前,必須用document對象的registerElement方法登記該元素。該方法返回一個自定義元素的構造函數。
```javascript
var SuperButton = document.registerElement('super-button');
document.body.appendChild(new SuperButton());
```
上面代碼生成自定義網頁元素的構造函數,然后通過構造函數生成一個實例,將其插入網頁。
可以看到,document.registerElement方法的第一個參數是一個字符串,表示自定義的網頁元素標簽名。該方法還可以接受第二個參數,表示自定義網頁元素的原型對象。
```javascript
var MyElement = document.registerElement('user-profile', {
prototype: Object.create(HTMLElement.prototype)
});
```
上面代碼注冊了自定義元素user-profile。第二個參數指定該元素的原型為HTMLElement.prototype(瀏覽器內部所有Element節點的原型)。
但是,如果寫成上面這樣,自定義網頁元素就跟普通元素沒有太大區別。自定義元素的真正優勢在于,可以自定義它的API。
```javascript
var buttonProto = Object.create(HTMLElement.prototype);
buttonProto.print = function() {
console.log('Super Button!');
}
var SuperButton = document.registerElement('super-button', {
prototype: buttonProto
});
var supperButton = document.querySelector('super-button');
supperButton.print();
```
上面代碼在原型對象上定義了一個print方法,然后將其指定為super-button元素的原型。因此,所有supper-button實例都可以調用print這個方法。
如果想讓自定義元素繼承某種特定的網頁元素,就要指定extends屬性。比如,想讓自定義元素繼承h1元素,需要寫成下面這樣。
```javascript
var MyElement = document.registerElement('another-heading', {
prototype: Object.create(HTMLElement.prototype),
extends: 'h1'
});
```
另一個是自定義按鈕(button)元素的例子。
```javascript
var MyButton = document.registerElement('super-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
```
如果要繼承一個自定義元素(比如`x-foo-extended`繼承`x-foo`),也是采用extends屬性。
```javascript
var XFooExtended = document.registerElement('x-foo-extended', {
prototype: Object.create(HTMLElement.prototype),
extends: 'x-foo'
});
```
定義了自定義元素以后,使用的時候,有兩種方法。一種是直接使用,另一種是間接使用,指定為某個現有元素是自定義元素的實例。
```html
<!-- 直接使用 -->
<supper-button></supper-button>
<!-- 間接使用 -->
<button is="supper-button"></button>
```
總之,如果A元素繼承了B元素。那么,B元素的is屬性,可以指定B元素是A元素的一個實例。
### 添加屬性和方法
自定義元素的強大之處,就是可以在它上面定義新的屬性和方法。
```javascript
var XFooProto = Object.create(HTMLElement.prototype);
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
```
上面代碼注冊了一個x-foo標簽,并且指明原型繼承HTMLElement.prototype。現在,我們就可以在原型上面,添加新的屬性和方法。
```javascript
// 添加屬性
Object.defineProperty(XFooProto, "bar", {value: 5});
// 添加方法
XFooProto.foo = function() {
console.log('foo() called');
};
// 另一種寫法
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype, {
bar: {
get: function() { return 5; }
},
foo: {
value: function() {
console.log('foo() called');
}
}
})
});
```
### 回調函數
自定義元素的原型有一些屬性,用來指定回調函數,在特定事件發生時觸發。
- **createdCallback**:實例生成時觸發
- **attachedCallback**:實例插入HTML文檔時觸發
- **detachedCallback**:實例從HTML文檔移除時觸發
- **attributeChangedCallback(attrName, oldVal, newVal)**:實例的屬性發生改變時(添加、移除、更新)觸發
下面是一個例子。
```javascript
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
console.log('created');
this.innerHTML = 'This is a my-demo element!';
};
proto.attachedCallback = function() {
console.log('attached');
};
var XFoo = document.registerElement('x-foo', {prototype: proto});
```
利用回調函數,可以方便地在自定義元素中插入HTML語句。
```javascript
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
};
var XFoo = document.registerElement('x-foo-with-markup',
{prototype: XFooProto});
```
上面代碼定義了createdCallback回調函數,生成實例時,該函數運行,插入如下的HTML語句。
```html
<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
```
## Shadow DOM
所謂Shadow DOM指的是,瀏覽器將模板、樣式表、屬性、JavaScript代碼等,封裝成一個獨立的DOM元素。外部的設置無法影響到其內部,而內部的設置也不會影響到外部,與瀏覽器處理原生網頁元素(比如`<video>`元素)的方式很像。Shadow DOM最大的好處有兩個,一是可以向用戶隱藏細節,直接提供組件,二是可以封裝內部樣式表,不會影響到外部。Chrome 35+支持Shadow DOM。
Shadow DOM元素必須依存在一個現有的DOM元素之下,通過`createShadowRoot`方法創造,然后將其插入該元素。
```javascript
var shadowRoot = element.createShadowRoot();
document.body.appendChild(shadowRoot);
```
上面代碼創造了一個`shadowRoot`元素,然后將其插入HTML文檔。
下面的例子是指定網頁中某個現存的元素,作為Shadom DOM的根元素。
```html
<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = '你好';
</script>
```
上面代碼指定現存的`button`元素,為Shadow DOM的根元素,并將`button`的文字從英文改為中文。
通過innerHTML屬性,可以為Shadow DOM指定內容。
```javascript
var shadow = document.querySelector('#hostElement').createShadowRoot();
shadow.innerHTML = '<p>Here is some new text</p>';
shadow.innerHTML += '<style>p { color: red };</style>';
```
下面的例子是為Shadow DOM加上獨立的模板。
```html
<div id="nameTag">張三</div>
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid brown;
}
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
</template>
```
上面代碼是一個`div`元素和模板。接下來,就是要把模板應用到`div`元素上。
```javascript
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
shadow.appendChild(template.content.cloneNode(true));
```
上面代碼先用`createShadowRoot`方法,對`div`創造一個根元素,用來指定Shadow DOM,然后把模板元素添加為`Shadow`的子元素。
## HTML Import
### 基本操作
長久以來,網頁可以加載外部的樣式表、腳本、圖片、多媒體,卻無法方便地加載其他網頁,iframe和ajax都只能提供部分的解決方案,且有很大的局限。HTML Import就是為了解決加載外部網頁這個問題,而提出來的。
下面代碼用于測試當前瀏覽器是否支持HTML Import。
```javascript
function supportsImports() {
return 'import' in document.createElement('link');
}
if (supportsImports()) {
// 支持
} else {
// 不支持
}
```
HTML Import用于將外部的HTML文檔加載進當前文檔。我們可以將組件的HTML、CSS、JavaScript封裝在一個文件里,然后使用下面的代碼插入需要使用該組件的網頁。
```html
<link rel="import" href="dialog.html">
```
上面代碼在網頁中插入一個對話框組件,該組建封裝在`dialog.html`文件。注意,dialog.html文件中的樣式和JavaScript腳本,都對所插入的整個網頁有效。
假定A網頁通過HTML Import加載了B網頁,即B是一個組件,那么B網頁的樣式表和腳本,對A網頁也有效(準確得說,只有style標簽中的樣式對A網頁有效,link標簽加載的樣式表對A網頁無效)。所以可以把多個樣式表和腳本,都放在B網頁中,都從那里加載。這對大型的框架,是很方便的加載方法。
如果B與A不在同一個域,那么A所在的域必須打開CORS。
```html
<!-- example.com必須打開CORS -->
<link rel="import" href="http://example.com/elements.html">
```
除了用link標簽,也可以用JavaScript調用link元素,完成HTML Import。
```javascript
var link = document.createElement('link');
link.rel = 'import';
link.href = 'file.html'
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);
```
HTML Import加載成功時,會在link元素上觸發load事件,加載失敗時(比如404錯誤)會觸發error事件,可以對這兩個事件指定回調函數。
```html
<script async>
function handleLoad(e) {
console.log('Loaded import: ' + e.target.href);
}
function handleError(e) {
console.log('Error loading import: ' + e.target.href);
}
</script>
<link rel="import" href="file.html"
onload="handleLoad(event)" onerror="handleError(event)">
```
上面代碼中,handleLoad和handleError函數的定義,必須在link元素的前面。因為瀏覽器元素遇到link元素時,立刻解析并加載外部網頁(同步操作),如果這時沒有對這兩個函數定義,就會報錯。
HTML Import是同步加載,會阻塞當前網頁的渲染,這主要是為了樣式表的考慮,因為外部網頁的樣式表對當前網頁也有效。如果想避免這一點,可以為link元素加上async屬性。當然,這也意味著,如果外部網頁定義了組件,就不能立即使用了,必須等HTML Import完成,才能使用。
```html
<link rel="import" href="/path/to/import_that_takes_5secs.html" async>
```
但是,HTML Import不會阻塞當前網頁的解析和腳本執行(即阻塞渲染)。這意味著在加載的同時,主頁面的腳本會繼續執行。
最后,HTML Import支持多重加載,即被加載的網頁同時又加載其他網頁。如果這些網頁都重復加載同一個外部腳本,瀏覽器只會抓取并執行一次該腳本。比如,A網頁加載了B網頁,它們各自都需要加載jQuery,瀏覽器只會加載一次jQuery。
### 腳本的執行
外部網頁的內容,并不會自動顯示在當前網頁中,它只是儲存在瀏覽器中,等到被調用的時候才加載進入當前網頁。為了加載網頁網頁,必須用DOM操作獲取加載的內容。具體來說,就是使用link元素的import屬性,來獲取加載的內容。這一點與iframe完全不同。
```javascript
var content = document.querySelector('link[rel="import"]').import;
```
發生以下情況時,link.import屬性為null。
- 瀏覽器不支持HTML Import
- link元素沒有聲明`rel="import"`
- link元素沒有被加入DOM
- link元素已經從DOM中移除
- 對方域名沒有打開CORS
下面代碼用于從加載的外部網頁選取id為template的元素,然后將其克隆后加入當前網頁的DOM。
```javascript
var el = linkElement.import.querySelector('#template');
document.body.appendChild(el.cloneNode(true));
```
當前網頁可以獲取外部網頁,反過來也一樣,外部網頁中的腳本,不僅可以獲取本身的DOM,還可以獲取link元素所在的當前網頁的DOM。
```javascript
// 以下代碼位于被加載(import)的外部網頁
// importDoc指向被加載的DOM
var importDoc = document.currentScript.ownerDocument;
// mainDoc指向主文檔的DOM
var mainDoc = document;
// 將子頁面的樣式表添加主文檔
var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));
```
上面代碼將所加載的外部網頁的樣式表,添加進當前網頁。
被加載的外部網頁的腳本是直接在當前網頁的上下文執行,因為它的`window.document`指的是當前網頁的document,而且它定義的函數可以被當前網頁的腳本直接引用。
### Web Component的封裝
對于Web Component來說,HTML Import的一個重要應用是在所加載的網頁中,自動登記Custom Element。
```html
<script>
// 定義并登記<say-hi>
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.innerHTML = 'Hello, <b>' +
(this.getAttribute('name') || '?') + '</b>';
};
document.registerElement('say-hi', {prototype: proto});
</script>
<template id="t">
<style>
::content > * {
color: red;
}
</style>
<span>I'm a shadow-element using Shadow DOM!</span>
<content></content>
</template>
<script>
(function() {
var importDoc = document.currentScript.ownerDocument; //指向被加載的網頁
// 定義并登記<shadow-element>
var proto2 = Object.create(HTMLElement.prototype);
proto2.createdCallback = function() {
var template = importDoc.querySelector('#t');
var clone = document.importNode(template.content, true);
var root = this.createShadowRoot();
root.appendChild(clone);
};
document.registerElement('shadow-element', {prototype: proto2});
})();
</script>
```
上面代碼定義并登記了兩個元素:\<say-hi\>和\<shadow-element\>。在主頁面使用這兩個元素,非常簡單。
```html
<head>
<link rel="import" href="elements.html">
</head>
<body>
<say-hi name="Eric"></say-hi>
<shadow-element>
<div>( I'm in the light dom )</div>
</shadow-element>
</body>
```
不難想到,這意味著HTML Import使得Web Component變得可分享了,其他人只要拷貝`elements.html`,就可以在自己的頁面中使用了。
## Polymer.js
Web Components是非常新的技術,為了讓老式瀏覽器也能使用,Google推出了一個函數庫[Polymer.js](http://www.polymer-project.org/)。這個庫不僅可以幫助開發者,定義自己的網頁元素,還提供許多預先制作好的組件,可以直接使用。
### 直接使用的組件
Polymer.js提供的組件,可以直接插入網頁,比如下面的google-map。。
```html
<script src="components/platform/platform.js"></script>
<link rel="import" href="google-map.html">
<google-map lat="37.790" long="-122.390"></google-map>
```
再比如,在網頁中插入一個時鐘,可以直接使用下面的標簽。
```html
<polymer-ui-clock></polymer-ui-clock>
```
自定義標簽與其他標簽的用法完全相同,也可以使用CSS指定它的樣式。
```css
polymer-ui-clock {
width: 320px;
height: 320px;
display: inline-block;
background: url("../assets/glass.png") no-repeat;
background-size: cover;
border: 4px solid rgba(32, 32, 32, 0.3);
}
```
### 安裝
如果使用bower安裝,至少需要安裝platform和core components這兩個核心部分。
```bash
bower install --save Polymer/platform
bower install --save Polymer/polymer
```
你還可以安裝所有預先定義的界面組件。
```bash
bower install Polymer/core-elements
bower install Polymer/polymer-ui-elements
```
還可以只安裝單個組件。
```bash
bower install Polymer/polymer-ui-accordion
```
這時,組件根目錄下的bower.json,會指明該組件的依賴的模塊,這些模塊會被自動安裝。
```javascript
{
"name": "polymer-ui-accordion",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#0.2.0",
"polymer-selector": "Polymer/polymer-selector#0.2.0",
"polymer-ui-collapsible": "Polymer/polymer-ui-collapsible#0.2.0"
},
"version": "0.2.0"
}
```
### 自定義組件
下面是一個最簡單的自定義組件的例子。
```html
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="lorem-element">
<template>
<p>Lorem ipsum</p>
</template>
</polymer-element>
```
上面代碼定義了lorem-element組件。它分成三個部分。
**(1)import命令**
import命令表示載入核心模塊
**(2)polymer-element標簽**
polymer-element標簽定義了組件的名稱(注意,組件名稱中必須包含連字符)。它還可以使用extends屬性,表示組件基于某種網頁元素。
```html
<polymer-element name="w3c-disclosure" extends="button">
```
**(3)template標簽**
template標簽定義了網頁元素的模板。
### 組件的使用方法
在調用組件的網頁中,首先加載polymer.js庫和組件文件。
```html
<script src="components/platform/platform.js"></script>
<link rel="import" href="w3c-disclosure.html">
```
然后,分成兩種情況。如果組件不基于任何現有的HTML網頁元素(即定義的時候沒有使用extends屬性),則可以直接使用組件。
```html
<lorem-element></lorem-element>
```
這時網頁上就會顯示一行字“Lorem ipsum”。
如果組件是基于(extends)現有的網頁元素,則必須在該種元素上使用is屬性指定組件。
```
<button is="w3c-disclosure">Expand section 1</button>
```