🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
<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> ```