<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                <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> ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看