<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>
```