HTML5-权威指南-十-

80 阅读53分钟

HTML5 权威指南(十)

原文:The Definitive Guide to HTML5

协议:CC BY-NC-SA 4.0

三十三、使用 Ajax——第二部分

在这一章中,我将继续描述 Ajax 是如何工作的,向您展示如何向客户端发送数据。发送表单和文件是 Ajax 的两种常见用途,它允许 web 应用为用户创建更丰富的体验。我还将向您展示如何在向服务器发送数据时监控进度,以及如何处理服务器在响应 Ajax 请求时发回的不同响应格式。表 33-1 对本章进行了总结。前三个清单设置了其他示例中使用的服务器和 HTML 文档。

Image

Image

准备向服务器发送数据

Ajax 最常见的用途之一是向服务器发送数据。最典型的情况是,客户端发送表单数据——输入到包含在form元素中的input元素中的值。清单 33-1 显示了一个简单的表格,这将是本章这一部分的基础。我把这个 HTML 保存到一个名为example.html的文件中。

清单 33-1。基本形式

`

             Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}                                         
                
                    
Bananas:
                    
                
                
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
Total:
                    
0 items
                
            
            Submit Form                 `

本例中的表单包含三个input元素和一个提交buttoninput元素允许用户指定订购三种不同水果中的多少种,然后button将表单提交给服务器。有关这些元素的更多信息,请参见第十二章、第十三章和第十四章。

定义服务器

对于示例,您需要创建处理请求的服务器。我再次使用了Node.js,很大程度上是因为它很简单并且使用了 JavaScript。获取Node.js的详细信息参见第二章。我不会深入研究这个脚本是如何工作的,但是因为它是用 JavaScript 编写的,所以您应该能够对发生的事情有一个大致的了解。也就是说,理解服务器脚本对于理解 Ajax 来说并不重要,如果愿意,您可以将服务器视为一个黑盒。清单 33-2 显示了fruitcalc.js脚本。

清单 33-2。Node.js 的 fruitcalc.js 脚本

`var http = require('http'); var querystring = require('querystring'); var multipart = require('multipart');

function writeResponse(res, data) {     var total = 0;     for (fruit in data) {         total += Number(data[fruit]);     }     res.writeHead(200, "OK", {         "Content-Type": "text/html",         "Access-Control-Allow-Origin": "http://titan"});     res.write('Fruit Total');     res.write('

' + total + ' items ordered

');     res.end(); }

http.createServer(function (req, res) {     console.log("[200] " + req.method + " to " + req.url);     if (req.method == 'OPTIONS') {         res.writeHead(200, "OK", {             "Access-Control-Allow-Headers": "Content-Type",             "Access-Control-Allow-Methods": "",             "Access-Control-Allow-Origin": ""             });         res.end();     } else if (req.url == '/form' && req.method == 'POST') {         var dataObj = new Object();         var contentType = req.headers["content-type"];         var fullBody = '';

        if (contentType) {             if (contentType.indexOf("application/x-www-form-urlencoded") > -1) {  

                req.on('data', function(chunk) { fullBody += chunk.toString();});                 req.on('end', function() {                     var dBody = querystring.parse(fullBody);                     dataObj.bananas = dBody["bananas"];                     dataObj.apples = dBody["apples"];                     dataObj.cherries= dBody["cherries"];                     writeResponse(res, dataObj);                 });

            } else if (contentType.indexOf("application/json") > -1) {                 req.on('data', function(chunk) { fullBody += chunk.toString();});                 req.on('end', function() {                     dataObj = JSON.parse(fullBody);                     writeResponse(res, dataObj);                 });

            } else if (contentType.indexOf("multipart/form-data") > -1) {                 var partName;                 var partType;                 var parser = new multipart.parser();                 parser.boundary = "--" + req.headers["content-type"].substring(30);

                parser.onpartbegin = function(part) {                     partName = part.name; partType = part.contentType};                 parser.ondata = function(data) {                     if (partName != "file") {                         dataObj[partName] = data;                     }                 };                 req.on('data', function(chunk) { parser.write(chunk);});                 req.on('end', function() { writeResponse(res, dataObj);});             }         }     } }).listen(8080);`

我突出显示了脚本中需要注意的部分:writeResponse函数。这个函数在从请求中提取表单值后被调用,它负责生成对浏览器的响应。目前,这个函数产生一个简单的 HTML 文档,如清单 33-3 所示,但是我们将在本章后面处理不同的格式时改变和增强这个函数。

清单 33-3。由 writeResponse 函数生成的简单 HTML 文档

`              Fruit Total                   

27 items ordered

    

`

这是一个简单的回答,但却是一个良好的开端。效果是服务器通过form中的input元素合计用户订购的水果数量。服务器端脚本的其余部分负责解码客户端可能使用 Ajax 发送的各种数据格式。您可以像这样启动服务器:


bin\node.exe fruitcalc.js


该脚本仅供本章使用。它不是通用服务器,我不建议您将它的任何部分用于生产服务。许多假设和快捷方式与本章后面的例子联系在一起,该脚本不适合任何严肃的用途。

理解问题

我想用 Ajax 解决的问题在图 33-1 中有清晰的说明。

Image

图 33-1。提交一份简单的表格

当您提交表单时,浏览器会将结果显示为新页面。这有两层含义:

  • 用户必须等待服务器处理数据并生成响应。
  • 任何文档上下文都将丢失,因为结果显示为新文档。

这是应用 Ajax 的理想情况。您可以异步发出请求,这样用户就可以在处理表单的同时继续与文档进行交互。

发送表单数据

向服务器发送数据的最基本方式是自己收集和格式化数据。清单 33-4 展示了使用这种方法向example.html文档添加一个脚本。

清单 33-4。手动收集和发送数据

`

             Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}                                         
                
                    
Bananas:
                    
                
                
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
Total:
                    
0 items
                
            
            Submit Form                 **

            function handleButtonPress(e) {                 e.preventDefault();

                var form = document.getElementById("fruitform");

                var formData = "";                 var inputElements = document.getElementsByTagName("input");                 for (var i = 0; i < inputElements.length; i++) {                     formData += inputElements[i].name + "="                         +  inputElements[i].value + "&";                 }

                httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("POST", form.action);                 httpRequest.setRequestHeader('Content-Type',                                              'application/x-www-form-urlencoded');                 httpRequest.send(formData);             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("results").innerHTML                         = httpRequest.responseText;                 }             }              

`

这个脚本看起来比实际更复杂。为了解释,我将分解各个步骤。所有的动作都发生在handleButtonPress函数中,该函数被调用以响应button元素的 click 事件。

我做的第一件事是在浏览器已经分派给函数的Event对象上调用preventDefault方法。我在第三十章中描述了这种方法,当时我解释了一些事件有与之相关的默认动作。对于表单中的button元素,默认的操作是使用常规的非 Ajax 方法提交表单。我不希望这种情况发生——因此调用了preventDefault方法。

Image 提示我喜欢将对preventDefault方法的调用放在我的事件处理函数的开始,因为这使得调试更容易。如果我在函数末尾调用这个方法,脚本中任何未被捕获的错误都会导致执行终止并执行默认操作。这种情况发生得如此之快,以至于无法从浏览器脚本控制台读取错误的详细信息。

下一步是收集并格式化input元素的值,如下所示:

var formData = ""; var inputElements = document.getElementsByTagName("input"); for (var i = 0; i < inputElements.length; i++) {     formData += inputElements[i].name + "=" +  inputElements[i].value + "&";     }

我使用 DOM 获得一组input元素,并创建一个包含每个元素的namevalue属性的字符串。namevalue由等号(=)分隔,关于每个input元素的信息由&符号(&)分隔。结果看起来像这样:

bananas=2&apples=5&cherries=20&

如果你回头看看第十二章,你会发现这是表单数据的默认编码方式——即application/x-www-form-urlencoded编码。尽管这是form元素使用的默认编码,但它不是 Ajax 使用的默认编码,所以我需要添加一个头来告诉服务器预期的数据格式,如下所示:

httpRequest.setRequestHeader('Content-Type','application/x-www-form-urlencoded');

脚本的其余部分是一个普通的 Ajax 请求,就像前一章中的请求一样,只是有一些例外。

首先,当我在XMLHttpRequest对象上调用open方法时,我使用 HTTP POST方法。通常,数据总是使用POST方法而不是GET方法发送到服务器。对于发出请求的 URL,我读取了HTMLFormElementaction属性:

httpRequest.open("POST", form.action);

form动作将导致一个跨源请求,我使用前一章描述的 CORS 技术在服务器上处理这个请求。

第二点需要注意的是,我将我想发送给服务器的字符串作为参数传递给send方法,如下所示:

httpRequest.send(formData);

当我从服务器得到响应时,我使用 DOM 用resultsid设置div元素的内容。你可以在图 33-2 中看到效果。

Image

图 33-2。使用 Ajax 发布表单

服务器为响应表单 post 而返回的 HTML 文档显示在同一页面上,并且请求是异步执行的。这是一个比我们开始时更好的效果。

使用 Form Data 对象发送表单数据

收集表单数据的一种更简洁的方式是使用一个FormData对象,它被定义为XMLHttpRequest Level 2 规范的一部分。

Image 注意当我写这篇文章时,Chrome、Safari 和 Firefox 支持FormData对象,但是 Opera 和 Internet Explorer 不支持。

创建表单数据对象

当你创建一个FormData对象时,你可以传递一个HTMLFormElement对象(在第三十一章中有描述),表单中所有元素的值都会被自动收集起来。清单 33-5 给出了一个例子。清单只显示了脚本,因为 HTML 保持不变。

清单 33-5。使用表单数据对象

`...

...`

当然,关键的变化是使用了FormData对象:

var formData = new FormData(form);

需要注意的另一个变化是,我不再设置Content-Type头的值。当使用FormData对象时,数据总是被编码为multipart/form-data(如第十二章所述)。

修改表单数据对象

FormData对象定义了一个方法,允许您将名称/值对添加到将被发送到服务器的数据中。该方法在表 33-2 中描述。

Image

您可以使用append方法来补充从表单中收集的数据,但是您也可以不使用HTMLFormElement来创建FormData对象。这意味着您可以使用append方法来选择将哪些数据值发送给客户端。清单 33-6 提供了一个演示。我再次只显示了 script 元素,因为其他 HTML 元素没有改变。

清单 33-6。使用 FormData 对象有选择地向服务器发送数据

`...

...`

在这个脚本中,我创建了一个FormData对象,但没有提供一个HTMLFormElement对象。然后,我使用 DOM 查找文档中所有的input元素,并为所有那些name属性没有值cherries的元素添加名称/值对。你可以在图 33-3 中看到效果,服务器返回的总值不包括用户为樱桃提供的值。

Image

图 33-3。使用 FormData 对象有选择地发送数据

发送 JSON 数据

使用 Ajax,您不仅限于发送表单数据。您可以发送几乎任何东西,包括 JavaScript 对象表示法(JSON)数据,它已经成为一种流行的数据格式。Ajax 的根在 XML 中,但这是一种冗长的格式。当您运行一个必须传输大量 XML 文档的 web 应用时,冗长会转化为带宽和系统容量方面的实际成本。

JSON 通常被称为 XML 的无脂肪替代品。JSON 易于阅读和编写,比 XML 更紧凑,并且获得了难以置信的广泛支持。JSON 已经超越了它在 JavaScript 中的根基,大量的包和系统理解并使用这种格式。

下面是一个简单的 JavaScript 对象在使用 JSON 表示时的样子:

{"bananas":"2","apples":"5","cherries":"20"}

这个对象有三个属性:bananasapplescherries。这些属性的值分别是2520

JSON 没有 XML 那么丰富的功能,但是对于许多应用来说,这些功能并没有被用到。JSON 简单、轻量、富于表现力。清单 33-7 展示了将 JSON 数据发送到服务器是多么容易。

清单 33-7。向服务器发送 JSON 数据

`...

...`

在这个脚本中,我创建了一个新的Object,并定义了与表单中input元素的name属性值相对应的属性。我可以使用任何数据,但是input元素很方便,并且与前面的例子一致。

为了告诉服务器我正在发送 JSON 数据,我将请求上的Content-Type头设置为application/json,如下所示:

httpRequest.setRequestHeader("Content-Type", "application/json");

我使用JSON对象在 JSON 格式之间进行转换。(大多数浏览器都直接支持这个对象,但是您可以使用在[github.com/douglascrockford/JSON-js/blob/master/json2.js](https://github.com/douglascrockford/JSON-js/blob/master/json2.js)提供的脚本为旧的浏览器添加相同的功能。)JSON 对象提供了两种方法,如表 33-3 所述。

在清单 33-7 的中,我使用了stringify方法并将结果传递给XMLHttpRequest对象的send方法。只有本例中的数据编码发生了变化。在文档中提交表单的效果保持不变。

发送文件

您可以使用一个FormData对象和一个input元素向服务器发送一个文件,元素的type属性为file。当提交表单时,FormData对象将自动确保用户选择的文件内容与表单的其他值一起上传。清单 33-8 展示了如何以这种方式使用FormData对象。

Image 注意对于还不支持FormData对象的浏览器来说,使用 Ajax 上传文件有些棘手。有很多破解和变通方法——一些使用 Flash,另一些涉及复杂的将表单发布到隐藏的iframe元素的序列。它们都有严重的缺点,应该谨慎使用。

清单 33-8。使用 FormData 对象向服务器发送文件

`

             Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}                                         
                
                    
Bananas:
                    
` `                
                
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
File:
                    
****
                
                
                    
Total:
                    
0 items
                

            

            Submit Form                  

             var httpRequest;

             function handleButtonPress(e) {                  e.preventDefault();

                 var form = document.getElementById("fruitform");

                 var formData = new FormData(form);                  httpRequest = new XMLHttpRequest();                  httpRequest.onreadystatechange = handleResponse;                  httpRequest.open("POST", form.action);                  httpRequest.send(formData);              }

             function handleResponse() {                  if (httpRequest.readyState == 4 && httpRequest.status == 200) {                      document.getElementById("results").innerHTML                          = httpRequest.responseText;                  }              }               

`

在这个例子中,重要的变化发生在form元素中。元素的添加导致FormData对象上传用户选择的任何文件。你可以在图 33-4 中看到添加的效果。

Image

图 33-4。通过 FormData 对象添加 input 元素上传文件

Image 提示在第三十七章中,我向你展示了如何使用拖放 API 让用户从操作系统中拖动要上传的文件,而不是使用文件选择器。

跟踪上传进度

当数据发送到服务器时,您可以跟踪数据上传的进度。你可以通过XMLHttpRequest对象的upload属性来实现,这在表 33-4 中有描述。

Image

upload属性返回的XMLHttpRequestUpload对象只定义了为前一章描述的事件注册处理程序所需的属性:onprogressonload等等。清单 33-9 展示了如何使用这些事件向用户显示上传进度。

清单 33-9。监控和显示上传进度

`

    ` `Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}                                         
                **
**                     **
Bananas:
**                     **
**                 **
**                 
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
File:
                    
****
                
                
                    
Progress:
                    
                
                
                    
Total:
                    
0 items
                

            

            Submit Form         

        

             var httpRequest;

             function handleButtonPress(e) {                  e.preventDefault();

                 var form = document.getElementById("fruitform");                  var progress = document.getElementById("prog");

                 var formData = new FormData(form);                  httpRequest = new XMLHttpRequest();

                 var upload = httpRequest.upload;                  upload.onprogress = function(e) {                     progress.max = e.total;                     progress.value = e.loaded;

                 }                  upload.onload = function(e) {                     progress.value = 1;                     progress.max = 1;                  }

                 httpRequest.onreadystatechange = handleResponse;                  httpRequest.open("POST", form.action);                  httpRequest.send(formData);              }

             function handleResponse() {                  if (httpRequest.readyState == 4 && httpRequest.status == 200) {                      document.getElementById("results").innerHTML                          = httpRequest.responseText;                  }              }               

`

在这个例子中,我添加了一个progress元素(在第十五章中描述)并使用它向用户提供数据上传进度信息。我通过读取XMLHttpRequest.upload属性获得一个XMLHttpRequestUpload对象,并注册函数来响应progressload事件。

浏览器不会给出小数据传输的进度信息,所以测试这个例子的最好方法是选择一个大文件。图 33-5 显示了一个电影文件被发送到服务器的进度。

Image

图 33-5。数据上传到服务器时显示进度

请求和处理不同的内容类型

到目前为止,所有的 Ajax 示例都返回一个完整的 HTML 文档,包括headtitlebody元素。这些元素都是开销,考虑到服务器实际传输的数据很少,有用信息与无用信息的比例并不理想。

幸运的是,您不需要返回完整的 HTML 文档。其实根本不需要返回 HTML。在接下来的小节中,我将向您展示如何处理不同种类的数据,并通过这样做来减少 Ajax 请求带来的开销。

接收 HTML 片段

最简单的改变是让服务器返回一个 HTML 片段,而不是整个文档。为此,我首先需要对Node.js服务器脚本的writeResponse进行修改,如清单 33-10 所示。

清单 33-10。修改服务器以发送回一个 HTML 片段

... function writeResponse(res, data) {     var total = 0;     for (fruit in data) {         total += Number(data[fruit]);     }     res.writeHead(200, "OK", {         "Content-Type": "text/html",         "Access-Control-Allow-Origin": "http://titan"});     **res.write('You ordered <b>' + total + '</b> items');**     res.end(); } ...

服务器现在只发送 HTML 的一个片段,而不是完整的文档。清单 33-11 显示了客户端的 HTML 文档。

清单 33-11。使用 HTML 片段

`

             Example                      .table {display:table;}             .row {display:table-row;}             .cell {display: table-cell; padding: 5px;}             .label {text-align: right;}                                         
` `                
                    
Bananas:
                    
                
                
                    
Apples:
                    
                
                
                    
Cherries:
                    
                
                
                    
Total:
                    
0 items
                
            
            Submit Form                  

             function handleButtonPress(e) {                 e.preventDefault();

                var form = document.getElementById("fruitform");

                var formData = new Object();                 var inputElements = document.getElementsByTagName("input");                 for (var i = 0; i < inputElements.length; i++) {                     formData[inputElements[i].name] =  inputElements[i].value;                 }

                httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("POST", form.action);                 httpRequest.setRequestHeader("Content-Type", "application/json");                 httpRequest.send(JSON.stringify(formData));              }

             function handleResponse() {                  if (httpRequest.readyState == 4 && httpRequest.status == 200) {                      document.getElementById("results").innerHTML                          = httpRequest.responseText;                  }              }               

`

我已经删除了一些最近添加的上传文件和监控进度的功能。我将数据作为 JSON 发送到服务器,并接收一个 HTML 片段作为返回(尽管我用来向服务器发送数据的数据格式和我从服务器返回的数据格式之间没有关系)。

因为我控制了服务器,所以我确保将Content-Type头设置为text/html,这告诉浏览器它正在处理 HTML,即使它获得的数据不是以DOCTYPEhtml元素开始。如果您想覆盖Content-Type头并自己指定数据类型,您可以使用overrideMimeType方法,如清单 33-12 所示。

清单 33-12。覆盖数据类型

`

     var httpRequest;

     function handleButtonPress(e) {         e.preventDefault();

        var form = document.getElementById("fruitform");

        var formData = new Object();         var inputElements = document.getElementsByTagName("input");         for (var i = 0; i < inputElements.length; i++) {             formData[inputElements[i].name] =  inputElements[i].value;         }

        httpRequest = new XMLHttpRequest();         httpRequest.onreadystatechange = handleResponse;         httpRequest.open("POST", form.action);         httpRequest.setRequestHeader("Content-Type", "application/json");         httpRequest.send(JSON.stringify(formData));      }

     function handleResponse() {          if (httpRequest.readyState == 4 && httpRequest.status == 200) {             httpRequest.overrideMimeType("text/html");             document.getElementById("results").innerHTML                 = httpRequest.responseText;          }      } `

如果服务器没有按照您想要的方式对数据进行分类,指定数据类型会很有用。当您从文件中交付内容片段,并且服务器已经预先配置好应该如何设置Content-Type头时,这种情况最常发生。

接收 XML 数据

XML 在 web 应用中不像以前那么流行了,大部分已经被 JSON 取代了。也就是说,处理 XML 数据仍然很有用,尤其是在处理遗留数据源时。清单 33-13 显示了向浏览器发送 XML 所需的服务器脚本的变化。

清单 33-13。从服务器发送 XML 数据

`function writeResponse(res, data) {     var total = 0;     for (fruit in data) {         total += Number(data[fruit]);     }     res.writeHead(200, "OK", {         "Content-Type": "application/xml",         "Access-Control-Allow-Origin": "http://titan"});

    res.write("");     res.write("");     for (fruit in data) {         res.write("")         total += Number(data[fruit]);     }     res.write("");     res.end(); }`

这个修改后的函数生成一个简短的 XML 文档,如下所示:

<?xml version='1.0'?> <fruitorder total='27'>     <item name='bananas' quantity='2'/>     <item name='apples' quantity='5'/>     <item name='cherries' quantity='20'/> </fruitorder>

这是我需要在客户端显示的信息的超集,但是它不再是我可以使用 DOM innerHTML属性显示的格式。幸运的是,XMLHttpRequest对象使得使用 XML 变得容易,这并不奇怪,因为 XML 是 Ajax 中的 x 。清单 33-14 显示了如何在浏览器中使用 XML。

清单 33-14。使用 XML Ajax 响应

`

     var httpRequest;

     function handleButtonPress(e) {         e.preventDefault();

        var form = document.getElementById("fruitform");         var formData = new Object();         var inputElements = document.getElementsByTagName("input");         for (var i = 0; i < inputElements.length; i++) {             formData[inputElements[i].name] =  inputElements[i].value;         }

        httpRequest = new XMLHttpRequest();         httpRequest.onreadystatechange = handleResponse;         httpRequest.open("POST", form.action);         httpRequest.setRequestHeader("Content-Type", "application/json");         httpRequest.send(JSON.stringify(formData));      }

     function handleResponse() {          if (httpRequest.readyState == 4 && httpRequest.status == 200) {             httpRequest.overrideMimeType("application/xml");             var xmlDoc = httpRequest.responseXML;             var val = xmlDoc.getElementsByTagName("fruitorder")[0].getAttribute("total");             document.getElementById("results").innerHTML = "You ordered "                 + val + " items";          }      } `

对处理 XML 数据的脚本的所有修改都发生在handleResponse函数中。当请求成功完成时,我做的第一件事是覆盖响应的 MIME 类型:

httpRequest.overrideMimeType("application/xml");

在这个例子中并不需要这样做,因为服务器正在发送一个完整的 XML 文档。但是在处理 XML 片段时,明确告诉浏览器你在处理 XML 是很重要的;否则,XMLHttpRequest对象将无法正确支持responseXML属性,我在下面的语句中使用了该属性:

var xmlDoc = httpRequest.responseXML;

responseXML属性是responseText的替代属性。它解析收到的 XML,并将其作为一个Document对象返回。然后,您可以使用这种技术,使用 HTML 的 DOM 特性在 XML 中导航(在第二十六章的中描述),如下所示:

var val = xmlDoc.getElementsByTagName("fruitorder")[0].getAttribute("total");

该语句获取第一个fruitorder元素中的total属性的值,然后我使用该值和innerHTML属性向用户显示结果:

document.getElementById("results").innerHTML = "You ordered "+ val + " items";

HTML 与 XML 在 DOM 中的对比

是入场的时候了。在本书的第四部分中,我故意淡化了 HTML 和 XML 之间的关系。还有大教堂。我描述的在 HTML 文档中导航和处理元素的所有特性同样适用于处理 XML。

事实上,表示 HTML 元素的对象是从 XML 支持中产生的一些核心对象派生出来的。对于大多数读者来说,HTML 支持是最重要的。如果您正在使用 XML,您可能希望花一些时间阅读核心 XML 支持,您可以在[www.w3.org/standards/techs/dom](http://www.w3.org/standards/techs/dom)找到它的定义。

话虽如此,如果您正在使用 XML 做大量的工作,您可能需要考虑一种替代的编码策略。XML 很冗长,在浏览器中执行复杂的处理并不总是理想的。一种更加定制和简洁的格式,比如 JSON,可能更适合您。

接收 JSON 数据

JSON 数据通常比 XML 更容易处理,因为您最终会得到一个 JavaScript 对象,您可以使用核心语言特性来查询和操作它。清单 33-15 显示了生成 JSON 响应所需的对服务器脚本的修改。

清单 33-15。在服务器上生成 JSON 响应

`function writeResponse(res, data) {     var total = 0;     for (fruit in data) {         total += Number(data[fruit]);     }     data.total = total;     var jsonData = JSON.stringify(data);

    res.writeHead(200, "OK", {         "Content-Type": "application/json",         "Access-Control-Allow-Origin": "http://titan"});     res.write(jsonData);     res.end(); }`

要生成 JSON 响应,我需要做的就是定义对象的total属性,该属性作为data参数传递给函数,并使用JSON.stringify将对象表示为字符串。服务器向浏览器发送响应,如下所示:

{"bananas":"2","apples":"5","cherries":"20","total":27}

清单 33-16 显示了浏览器处理这个响应所需的脚本修改。

清单 33-16。从服务器接收 JSON 响应

`

     var httpRequest;

     function handleButtonPress(e) {         e.preventDefault();

        var form = document.getElementById("fruitform");

        var formData = new Object();         var inputElements = document.getElementsByTagName("input");         for (var i = 0; i < inputElements.length; i++) {             formData[inputElements[i].name] =  inputElements[i].value;         }

        httpRequest = new XMLHttpRequest();         httpRequest.onreadystatechange = handleResponse;         httpRequest.open("POST", form.action);         httpRequest.setRequestHeader("Content-Type", "application/json");         httpRequest.send(JSON.stringify(formData));      }

     function handleResponse() {          if (httpRequest.readyState == 4 && httpRequest.status == 200) {             var data = JSON.parse(httpRequest.responseText);             document.getElementById("results").innerHTML = "You ordered "                 + data.total + " items";          }      } `

JSON 非常容易使用,如这两个清单所示。这种易用性,加上表示的紧凑性,是 JSON 如此受欢迎的原因。

总结

在这一章中,我解释完了 Ajax 的复杂性。我向您展示了如何手动和使用FormData对象向服务器发送数据。您学习了如何发送文件,以及如何在数据上传到服务器时监控进度。我还介绍了如何处理服务器发送的不同数据格式:HTML、HTML 片段、XML 和 JSON。

三十四、使用多媒体

HTML5 支持在浏览器中播放音频和视频文件,而无需使用 Adobe Flash 等插件。浏览器插件是浏览器崩溃的主要原因,尤其是 Flash,是众所周知的问题原因。

作为一个相关的题外话,我已经开始厌恶媒体播放的 Flash 了。我喜欢在写作的时候听播客,Chrome 默认使用 Flash 播放这些。我喜欢集成的便利性,但是时不时会出现问题,我有一台锁定的机器。把我逼疯了,每次都让我诅咒土坯。Flash 的无处不在很有用;这个软件的质量还有许多不足之处。

正如你将在本章看到的,HTML 对本地音频和视频的支持有很大的潜力,但仍有一些问题需要解决。这在很大程度上与每个浏览器支持的格式以及浏览器对其播放文件格式能力的不同理解有关。表 34-1 对本章进行了总结。

Image 提示如果你想重现本章中的例子,你可能需要给你的 web 服务器添加一些 MIME 类型。你可以在清单 34-7 中看到哪些是必需的。

Image

Image

使用视频元素

使用video元素将视频内容嵌入到网页中。表 34-2 描述了video元素。

Image

清单 34-1 显示了这个元素的基本用法。

清单 34-1。使用视频元素

`

             Example                   ****             **Video cannot be displayed**         ****      `

如果你以前看过网页中的视频,使用video元素的结果会很熟悉,如图图 34-1 所示。

Image

图 34-1。使用视频元素

如果浏览器不支持video元素或无法播放视频,将显示回退内容(开始和结束标签之间的内容)。在这个例子中,我提供了一条简单的文本消息,但是一种常见的技术是使用非 HTML5 技术(比如 Flash)来支持旧浏览器,从而提供视频回放。

video元素有许多属性,我在表 34-3 中描述了这些属性。

预加载视频

preload属性告诉浏览器,当包含video元素的页面第一次加载时,它是否应该乐观地下载视频。预加载视频可以减少用户开始回放时的初始延迟,但如果用户不观看视频,则可能会浪费网络带宽。该属性的允许值在表 34-4 中描述。

Image

关于抢先加载视频的决定应该由用户想要观看视频的可能性来驱动,与自动加载视频内容所需的带宽相平衡。自动加载视频会带来更流畅的用户体验,但它会显著增加容量成本,当用户在没有观看视频的情况下离开页面时,这种成本就会被浪费掉。

这个属性的metadata值可以用来在noneauto值之间取得适度的平衡。none值的问题是视频内容显示为屏幕的空白区域。metadata值使浏览器获得足够的信息向用户显示视频的第一帧,而不必下载所有的内容。清单 34-2 显示了在同一文档中使用的nonemetadata值。

清单 34-2。使用预加载属性的无和元数据值

`

             Example                                Video cannot be displayed                               Video cannot be displayed               `

您可以在图 34-2 的中看到这些值是如何影响显示给用户的。

Image

图 34-2。使用预加载属性的无和元数据值

Image 注意metadata值给用户一个很好的预览,但是需要一些注意。在研究这个属性和使用网络分析器时,我发现浏览器倾向于抢先下载整个视频,即使只请求了元数据。平心而论,preload属性表达了浏览器可以随意忽略的偏好。但是,如果您需要限制带宽消耗,poster属性可能会提供一个更好的选择。详情见下一节。

显示占位符图像

您可以使用poster属性向用户显示一个占位符图像。该图像将代替视频显示,直到用户开始播放。清单 34-3 显示了正在使用的poster属性。

清单 34-3。使用海报属性指定占位符图像

`

             Example                                Video cannot be displayed                        `

我截图了视频文件的第一帧,在上面叠加了Poster这个词。该图片包括视频控件,以向用户指示海报代表视频剪辑。在这个例子中,我还包含了一个img元素来演示海报图像是由video元素显示的,没有经过修改。图 34-3 显示了两种形式的海报。

Image

图 34-3。使用海报制作视频剪辑

设置视频尺寸

如果省略了widthheight属性,浏览器将显示一个小占位符元素,当元数据可用时(即,当用户开始回放或preload属性设置为metadata时),该元素的大小将调整为视频的固有尺寸。这可能会在调整页面布局以适应视频时产生不和谐的效果。

如果您指定了widthheight属性,浏览器会保留视频的长宽比——您不必担心视频会在任何方向上被拉伸。清单 34-4 展示了widthheight属性的应用。

清单 34-4。应用宽度和高度属性

`

             Example                      video {                 background-color: lightgrey;                 border: medium double black;             }                                         Video cannot be displayed               `

在这个例子中,我设置了width属性,使其与height属性不成比例。我还对video元素应用了一个样式,以强调浏览器只使用分配给该元素的部分空间来保持视频的纵横比。图 34-4 显示了结果。

Image

图 34-4。保存视频宽高比的浏览器

指定视频源(和格式)

指定视频的最简单方法是使用src属性,给出所需视频文件的 URL。这是我在前面的例子中采用的方法,在清单 34-5 中再次显示。

清单 34-5。使用 src 属性指定视频源

`

             Example                                Video cannot be displayed               `

在这个清单中,我使用 source 元素来指定文件timessquare.webm。这是一个以WebM格式编码的文件。有了这个,你就进入了视频格式的艰难世界。在本书的前面,我提到了浏览器大战——几家公司试图通过对 HTML 和相关技术的非标准添加来控制浏览器市场。令人高兴的是,那些日子已经过去了,遵从标准被视为浏览器的一个卖点,还有速度、易用性和吸引人的标志。

遗憾的是,在视频格式方面,这一点还没有达到。如果一些公司能够建立自己的格式作为 HTML5 的主导格式,他们有可能赚很多钱。许可费可以收取,版税可以征收,专利组合可以增值。因此,没有普遍支持的视频格式,如果您希望使用视频来面向广泛的 HTML5 用户,您可以使用多种格式来编码您的视频。表 34-5 显示了目前有很强支持的格式(尽管这几乎肯定会随着时间的推移而改变)。

Image

可悲的事实是,没有一种单一的格式可以用于所有的主流浏览器——在有之前,需要以多种格式编码同一视频。

Image 注意在视频编码中有一个完整的细节层次,我将直接跳过。它涉及容器、编解码器和其他令人兴奋的概念。结果是,每种格式中都有一些选项和选择,它们为了兼容性而牺牲了质量或紧凑性——鉴于浏览器对视频支持的不断变化,这些组合会频繁变化。我建议您参考主流浏览器的发行说明来确定支持级别,或者像我一样,对每种可能的排列进行编码,看看什么能提供最广泛的支持。

您使用source元素来指定多种格式。该元素在表 34-6 中描述。

清单 34-6 展示了如何使用source元素为浏览器提供视频格式的选择。

清单 34-6。使用源元素

`

             Example                                ****             ****             ****             Video cannot be displayed               `

浏览器按顺序在列表中向下移动,寻找可以播放的视频文件。这可能意味着需要多次请求服务器来获取每个文件的附加信息。浏览器判断是否可以播放视频的方法之一是通过服务器返回的 MIME 类型。通过将type属性应用于source元素,指定文件的 MIME 类型,可以向用户提供提示,如清单 34-7 所示。

清单 34-7。在源元素上应用类型属性

`

             Example                                                                       Video cannot be displayed               `

Image 提示media属性为浏览器提供视频最适合的设备类型的指导。关于如何定义该属性的值,详见第七章。

追踪元素

HTML5 规范包括track元素,它为与视频相关的附加内容提供了一种机制。这包括副标题、题注和章节标题。表 34-7 描述了这个元素,但是目前主流浏览器都没有实现这个元素。

使用音频元素

元素允许您将音频内容嵌入到 HTML 文档中。该元素在表 34-8 中描述。

您可以看到,audio元素与video元素有很多共同之处。清单 34-8 展示了使用中的audio元素。

清单 34-8。使用音频元素

`

             Example                   ****             **Audio content cannot be played**         ****      `

使用src属性指定音频源。尽管音频格式的世界没有视频那么有争议,但是仍然不是一种所有浏览器都可以自然播放的格式,尽管我更希望音频格式会比视频格式有所改变。

Image 提示通过应用autoplay属性,省略controls属性,可以创建一个音频自动播放,用户没有办法阻止的情况。代表你所有的用户,我恳求你不要这样做——尤其是如果你打算播放沉闷的、合成的、匿名的、本质上无法识别的音乐。将这样的音乐强加给你的用户,会让每一笔交易都让人想起漫长的电梯之旅,如果你的音轨中没有可辨别的乐器,这种情况尤其如此。请不要让你的用户听乏味、没有灵魂、毫无意义的音乐,当然也不要让它自动启动,让用户无法关闭它。

清单 34-9 展示了如何使用source元素来提供多种格式。

清单 34-9。使用源元素提供多种音频格式

`

             Example                                ****             ****             ****             Audio content cannot be played               `

在这两个例子中,我使用了controls属性,以便浏览器向用户显示默认控件。不同的浏览器之间会有一些差异,但是图 34-5 会给你一个预期的概念。

Image

图 34-5。谷歌浏览器中音频元素的默认控件

通过 DOM 处理嵌入媒体

audiovideo元素有足够的共同点,以至于HTMLMediaElement对象在 DOM 中为它们定义了核心功能。在 DOM 中,audio元素由HTMLAudioElement对象表示,但是这并没有定义除了HTMLMediaElement之外的额外功能。video元素由HTMLVideoElement对象表示。这确实定义了一些额外的属性,我将在本章后面描述。

Image 提示audiovideo元素有如此多的共同点,唯一的区别是它们占据的屏幕空间大小。audio元素并不是用一大块屏幕来显示视频图像。您实际上可以使用audio元素来播放视频文件(尽管您显然只能获得原声音乐),并且可以使用video元素来播放音频文件(尽管视频显示保持空白)。奇怪却真实。

获取有关媒体的信息

HTMLMediaElement对象定义了许多成员,您可以使用它们来获取和修改关于元素和与之相关的媒体的信息。这些在表 34-9 中描述。

Image

HTMLVideoElement对象定义了表 34-10 中显示的附加属性。

Image

清单 34-10 展示了一些用于获取媒体元素基本信息的HTMLMediaElement属性。

清单 34-10。获取关于媒体元素的基本信息

`

             Example                      table {border: thin solid black; border-collapse: collapse;}             th, td {padding: 3px 4px;}             body > * {float: left; margin: 2px;}                                                                                Video cannot be displayed                                        
PropertyValue
                                          "preload", "src", "volume"];             for (var i = 0; i < propertyNames.length; i++) {                 tableElem.innerHTML +=                     "" + propertyNames[i] + "" +                     mediaElem[propertyNames[i]] + "";             }              

`

本例中的脚本在一个表中显示了许多属性的值,旁边是video元素。你可以在图 34-6 中看到结果。

Image

图 34-6。显示视频元素的基本信息

我在图中展示了 Opera,因为它是唯一正确实现了currentSrc属性的浏览器。该属性显示src属性的值,或者来自媒体元素本身,或者来自正在使用的source元素(当有可用的格式选择时)。

评估回放能力

canPlayType方法可以用来判断浏览器是否可以播放特定的媒体格式。该方法返回表 34-11 中显示的值之一。

这些值显然是模糊的——这又回到了一些媒体格式的复杂性和创建它们时可以使用的编码选项。清单 34-11 显示了正在使用的canPlayType方法。

清单 34-11。使用 canPlayType 方法

`

             Example                      table {border: thin solid black; border-collapse: collapse;}             th, td {padding: 3px 4px;}             body > * {float: left; margin: 2px;}                                         Video cannot be displayed                                        
PropertyValue
                     for (var i = 0; i < mediaTypes.length; i++) {                 var playable = mediaElem.canPlayType(mediaTypes[i]);                 if (!playable) {                     playable = "no";                 }

                tableElem.innerHTML +=                     "" + mediaTypes[i] + "" + playable + "";                 if (playable == "probably") {                     mediaElem.src = mediaFiles[i];                 }             }              

`

在本例的脚本中,我使用了canPlayType方法来评估一组媒体类型。如果我收到一个probably响应,我为video元素设置src属性值。在这个过程中,我将每种媒体类型的响应记录在一个表格中。

当试图以这种方式选择媒体时需要小心,因为浏览器评估其播放格式能力的方式不同。例如,图 34-7 显示了 Firefox 的响应。

Image

图 34-7。评估 Firefox 中的媒体格式支持

火狐非常看好WebM,并且确定OggMP4文件不能播放——然而,火狐似乎很好地处理了Ogg视频文件。图 34-8 显示了 Chrome 的响应。

Image

图 34-8。评估 Chrome 对媒体格式的支持

Chrome 的观点要保守得多,但它能愉快地播放我所有的三个媒体文件。事实上,Chrome 非常保守,我没有从canPlayType方法得到probably响应,所以没有进行媒体选择。

很难批评浏览器的反应不一致。变量太多,无法给出明确的答案,但是评估支持的不同方式意味着应该非常谨慎地使用canPlayType方法。

控制媒体播放

HTMLMediaElement对象定义了许多成员,这些成员允许您控制回放并获得关于回放的信息。这些属性和方法在表 34-12 中描述。

Image

清单 34-12 展示了如何使用表格中的属性来获取关于回放的信息。

清单 34-12。使用 HTMLMediaElement 属性获取媒体播放的详细信息

`

             Example                      table {border: thin solid black; border-collapse: collapse;}             th, td {padding: 3px 4px;}             body > * {float: left; margin: 2px;}             div {clear: both;}                                                                                Video cannot be displayed                                        
PropertyValue
        
            Press Me         
        

            displayValues();

            function displayValues() {                 var propertyNames = ["currentTime", "duration", "paused", "ended"];                 tableElem.innerHTML = "";                 for (var i = 0; i < propertyNames.length; i++) {                     tableElem.innerHTML +=                         "" + propertyNames[i] + "" +                         mediaElem[propertyNames[i]] + "";                 }             }              

`

这个例子包括一个button元素,当按下它时,会导致currentTimedurationpausedended属性的当前值显示在一个表格中。在图 34-9 中可以看到效果。

Image

图 34-9。拍摄回放属性值的快照以响应按钮按下

您可以使用回放方法来替换默认的媒体控制。清单 34-13 提供了一个演示。

清单 34-13。替换默认媒体控件

`

             Example                                                                       Video cannot be displayed                  
            Play             Pause         
        

            function handleButtonPress(e) {                 switch (e.target.innerHTML) {                     case 'Play':                         mediaElem.play();                         break;                     case 'Pause':                         mediaElem.pause();                         break;                 }             }              

`

在这个例子中,我从video元素中省略了controls属性,并使用由button按键触发的playpause方法来开始和停止媒体播放。你可以在图 34-10 中看到效果。

Image

图 34-10。替换默认媒体控件

Image 提示HTML 规范定义了一系列与加载和播放媒体相关的事件,通过HTMLMediaElement对象的controller属性公开。当我写这篇文章时,没有一个主流浏览器支持这个属性或它应该返回的MediaController对象。

总结

在本章中,我向您展示了 HTML5 如何通过videoaudio元素支持本地媒体回放,以及如何使用 DOM 控制这些元素。鉴于 Flash 等插件的困难,原生媒体支持有很大的潜力,但这种方法仍处于采用的早期阶段。在格式支持问题得到解决,并且有足够多的浏览器支持这种方法之前,您将坚持使用混合匹配的方法。

三十五、使用画布元素——第一部分

在前一章中,我提到了(并简单地讲述了)大多数 web 应用开发人员和设计人员对 Adobe Flash 的爱恨情仇。这种憎恨来自于缺乏稳定性和安全性,因为 Adobe 最近被指责软件质量差。对 Flash 的喜爱来自于它无处不在的安装方式,以及它可以用来制作丰富内容的方式。

作为 Flash 的原生替代,HTML5 定义了canvas元素。如果你读过任何关于 HTML5 中新功能的描述,那么canvas很可能是最先提到的特性之一,并且可能被描述为 Flash 杀手。

通常情况下,炒作和现实并不匹配。canvas 元素是我们使用 JavaScript 配置和驱动的绘图表面。它很灵活,相对容易使用,并且它提供了足够的功能,可以取代 Flash 来获得一些丰富的内容。但是称canvas元素为闪存杀手(甚至是闪存替代品)还为时过早,因为在canvas取而代之之前还有一段时间。

这是关于 canvas 元素的两章中的第一章。在这一章中,我将向您展示如何设置 canvas 元素,并介绍我们在 JavaScript 中用来与 canvas 交互的对象。我还向您展示了对基本形状的支持,如何使用纯色和渐变,以及如何在画布上绘制图像。下一章将向你展示如何绘制更复杂的形状,以及如何应用效果和变换。表 35-1 对本章进行了总结。

Image

Image

画布元素入门

canvas元素非常简单,因为它的所有功能都通过一个 JavaScript 对象公开,所以元素本身只有两个属性,如表 35-2 所示。

Image

如果浏览器不支持元素本身,canvas元素的内容被用作后备。清单 35-1 显示了canvas元素和一些简单的回退内容。

清单 35-1。使用具有基本回退内容的画布元素

`

             Example                      canvas {border: medium double black; margin: 4px}                            ****             **Your browser doesn't support the canvas element**         ****      `

正如您可能想象的那样,widthheight属性指定了屏幕上元素的大小。你可以在图 35-1 中看到浏览器是如何显示这个例子的(当然,尽管在这一点上没什么可看的)。

Image 提示我在这个例子中对 canvas 元素应用了一个样式来设置边框。否则将无法在浏览器窗口中看到canvas。我将在这一章的所有例子中显示一个边界,所以我描述的操作与画布坐标的关系总是很清楚的。

Image

图 35-1。将画布元素添加到 HTML 文档

获取画布上下文

为了在一个canvas元素上绘图,我们需要得到一个上下文对象,这是一个为特定风格的图形公开绘图功能的对象。在我们的例子中,我们将使用用于执行二维操作的2d上下文。一些浏览器提供了对实验性 3D 环境的支持,但这仍处于早期阶段。

我们通过表示 DOM 中的canvas元素的对象获得一个上下文。这个物体HTMLCanvasElement在表 35-3 中有描述。

Image

关键方法是getContext——为了获得二维上下文对象,我们请求将2d参数传递给该方法。一旦我们有了背景,我们就可以开始画了。清单 35-2 提供了一个演示。

清单 35-2。获取画布的二维上下文对象

`

             Example                      canvas {border: medium double black; margin: 4px}                                         Your browser doesn't support the canvas element                        `

我已经强调了清单中的关键陈述。我使用document对象在 DOM 中查找代表canvas元素的对象,并使用参数2d调用getContext方法。你会在这一章的所有例子中看到这种说法,或者一种近似的说法。

一旦我有了上下文对象,我就可以开始画了。在这个例子中,我调用了fillRect方法,它在画布上绘制了一个填充的矩形。你可以在图 35-2 中看到(简单的)效果。

Image

图 35-2。获取一个上下文对象并执行简单的绘图操作

绘制矩形

让我们从对矩形的canvas支持开始。表 35-4 描述了相关的方法,所有这些方法都应用于上下文对象(而不是画布本身)。

提示我们可以画出更复杂的形状,但我不会告诉你怎么做,直到第三十六章。我们可以使用矩形来探索画布的一些特性,而不会陷入其他形状如何工作的困境。

Image

这三种方法都有四个参数。前两个(xy如表中所示)是 canvas 元素左上角的偏移量。wh参数指定要绘制的矩形的宽度和高度。清单 35-3 展示了fillRectstrokeRect方法的使用。

清单 35-3。使用 fillRect 和 strokeRect 方法

`

             Example         ` `            canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                  

            for (var i = 0; i < count; i++) { **                ctx.fillRect(i * (offset + size) + offset, offset, size, size);**                 ctx.strokeRect(i * (offset + size) + offset, (2 * offset) + size,                     size, size);             }              

`

本例中的脚本使用fillRectstrokeRect方法创建一系列填充和未填充的矩形。你可以在图 35-3 中看到结果。

Image

图 35-3。绘制填充和未填充的矩形

我这样写脚本是为了强调canvas元素的编程性质。我使用了一个 JavaScript for循环来绘制这些矩形。我可以使用 10 个单独的语句,每个语句都有特定的坐标参数,但是canvas的一个好处是我们不需要这样做。如果你没有编程背景,你可能很难理解这方面的内容。

方法删除指定矩形中的任何内容。清单 35-4 提供了一个演示。

清单 35-4。使用 clearRect 方法

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                  

            for (var i = 0; i < count; i++) {                 ctx.fillRect(i * (offset + size) + offset, offset, size, size);                 ctx.strokeRect(i * (offset + size) + offset, (2 * offset) + size,                                size, size);                 ctx.clearRect(i * (offset + size) + offset, offset + 5, size, size -10);             }              

`

在这个例子中,我使用了clearRect方法来清除画布上已经被fillRect方法绘制过的区域。你可以在图 35-4 中看到效果。

Image

图 35-4。使用 clearRect 方法

设置画布的绘制状态

绘图操作由绘图状态配置。这是一组指定从线宽到填充颜色的所有内容的属性。当我们绘制一个形状时,将使用绘制状态下的当前设置。清单 35-5 提供了一个使用lineWIdth属性的演示,该属性是绘图状态的一部分,设置用于形状的线条宽度,例如由strokeRect方法产生的形状。

清单 35-5。执行操作前设置绘图状态

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                               ctx.strokeRect(10, 10, 50, 50);             ctx.lineWidth = 4;             ctx.strokeRect(70, 10, 50, 50);             ctx.lineWidth = 6;             ctx.strokeRect(130, 10, 50, 50);             ctx.strokeRect(190, 10, 50, 50);              

`

当我使用strokeRect方法时,lineWidth属性的当前值用于绘制矩形。在这个例子中,我将属性值设置为 2、4,最后是 6 个像素,这样可以使矩形的线条变得更粗。请注意,我没有更改对strokeRect的最后两次调用之间的值。我这样做是为了证明绘制状态属性的值在绘制操作之间不会改变,如图 35-5 中的所示。

Image

图 35-5。在绘图操作之间改变绘图状态值

表 35-5 显示了基本的绘图状态属性。在我们研究更高级的特性时,还会遇到其他一些属性。

Image

设置线条连接样式

属性决定了如何绘制相互连接的线条。有三个值:roundbevelmiter。默认值为miter。清单 35-6 显示了使用中的三种风格。

清单 35-6。设置 lineJoin 属性

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                               ctx.strokeRect(20, 20, 100, 100);

            ctx.lineJoin = "bevel";             ctx.strokeRect(160, 20, 100, 100);

            ctx.lineJoin = "miter";             ctx.strokeRect(300, 20, 100, 100);              

`

在这个例子中,我使用了lineWidth属性,这样strokeRect方法就可以用非常粗的线条绘制矩形,然后依次使用每个lineJoin样式值。你可以在图 35-6 中看到结果。

Image

图 35-6。lineJoin 属性

设置填充&笔画样式

当我们使用fillStylestrokeStyle属性设置样式时,我们可以使用我在第三十五章 - 第四章中描述的 CSS 颜色值,使用名称或颜色模型来指定颜色。清单 35-7 提供了一个例子。

清单 35-7。使用 fillStyle 和 strokeStyle 属性设置颜色

`

             Example                      canvas {border: thin solid black; margin: 4px}         ` `                               Your browser doesn't support the canvas element                   **            var strokeColors = ["rgb(0,0,0)", "rgb(100, 100, 100)",** **                                "rgb(200, 200, 200)", "rgb(255, 0, 0)",** **                                "rgb(0, 0, 255)"];**

            for (var i = 0; i < count; i++) { **                ctx.fillStyle = fillColors[i];** **                ctx.strokeStyle = strokeColors[i];**

                ctx.fillRect(i * (offset + size) + offset, offset, size, size);                 ctx.strokeRect(i * (offset + size) + offset, (2 * offset) + size,                                size, size);                 }              

`

在这个例子中,我使用 CSS 颜色名称和rgb模型定义了两个颜色数组。然后我将这些颜色分配给调用fillRectstrokeRect方法的for循环中的fillStylestrokeStyle属性。在图 35-7 中可以看到效果。

Image

图 35-7。使用 CSS 颜色设置填充和笔画样式

Image如果是这样,你可以从apress.com免费获得这本书的所有代码示例。

使用渐变

我们也可以使用渐变而不是纯色来设置填充和笔画样式。渐变是两种或多种颜色之间的渐变。canvas元素支持两种渐变:线性和径向,使用表 35-6 中描述的方法。

Image

这两种方法都返回一个CanvasGradient对象,该对象定义了表 35-7 中所示的方法。这些参数描述渐变所使用的直线或圆,这将在下面的示例中进行解释。

Image

使用线性渐变

线性渐变是我们沿着一条线指定我们想要的颜色。清单 35-8 展示了我们如何创建一个简单的线性渐变。

清单 35-8。创建线性渐变

`

             Example                      canvas {border: thin solid black; margin: 4px}                   ` `                     Your browser doesn't support the canvas element                  

            ctx.fillStyle = grad;             ctx.fillRect(0, 0, 500, 140);              

`

当我们使用createLinearGradient方法时,我们提供四个值作为画布上一行的开始和结束坐标。在这个例子中,我用坐标描述了一条从点00开始到500140结束的直线。这些点对应于画布的左上角和右下角,如图图 35-8 所示。

Image

图 35-8。线性渐变线

这条线代表渐变。我们现在可以在由createLinearGradient方法返回的CanvasGradient上使用addColorStop方法,沿着渐变线添加颜色,就像这样:

grad.addColorStop(0, "red"); grad.addColorStop(0.5, "white"); grad.addColorStop(1, "black");

addColorStop方法的第一个参数是我们想要应用颜色的行的位置,我们使用第二个参数来指定。线的起点(本例中的坐标0, 0由值0表示,线的终点由值1表示。在这个例子中,我已经告诉canvas我想要线条开始的颜色red,线条中间的颜色white,线条结尾的颜色black。然后画布会计算出如何在这些点的颜色之间逐渐过渡。我们可以指定尽可能多的色标(但是如果我们忘乎所以,我们最终会得到看起来像彩虹的东西)。

一旦我们定义了渐变并添加了色标,我们就可以分配CanvasGradient对象来设置fillStylestrokeStyle属性,如下所示:

ctx.fillStyle = grad;

最后,我们可以画一个形状。在本例中,我画了一个实心矩形,如下所示:

ctx.fillRect(0, 0, 500, 140);

这个矩形填充了画布,显示了整个渐变,正如你在图 35-9 中看到的。

Image

图 35-9。在填充的矩形中使用线性渐变

您可以看到颜色沿着渐变线变化。左上角有纯红,线中间有纯白,右下角有纯黑,颜色在这两点之间逐渐偏移。

使用较小形状的线性渐变

当我们定义渐变线时,我们是相对于画布来做的,而不是我们所画的形状。这一开始会引起一些混乱。清单 35-9 包含了我的意思的演示。

清单 35-9。使用不填充画布形状的渐变

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                              grad.addColorStop(1, "black");

            ctx.fillStyle = grad;             ctx.fillRect(10, 10, 50, 50);              

`

本例中的变化只是使矩形变小。你可以在图 35-10 中看到结果。

Image

图 35-10。渐变中缺少渐变

这就是我所说的与画布相关的渐变线。我在一个纯红的区域画了一个矩形。(事实上,如果我们能够放大得足够近,我们可能能够检测到向白色的微小渐变,但总体外观是纯色的。)思考这个问题的最佳方式是,当我们绘制一个形状时,我们允许部分底层渐变显示出来,这意味着我们必须考虑渐变线如何与我们要曝光的区域相关联。清单 35-10 展示了我们如何将渐变线作为一个形状的目标。

清单 35-10。使渐变线匹配期望的形状

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                                

`

在这个例子中,我设置了渐变线,使它开始和停止在我想用我的小矩形显示的区域内。然而,我绘制了矩形来显示渐变的所有的*,这样你就可以看到变化的效果,如图图 11 所示。*

Image

图 35-11。移动和缩短渐变线的效果

你可以看到渐变是如何转移到我要用小矩形曝光的区域的。最后一步是匹配矩形和渐变,如清单 35-11 所示。

清单 35-11。将形状与渐变匹配

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                                

`

Image 提示注意,我在createLinearGradient方法中用作参数的数值不同于我在fillRect方法中使用的参数。createLinearGradient值表示画布中的一对坐标,而fillRect值表示相对于单个坐标的矩形的宽度和高度。如果发现渐变和形状不对齐,这很可能是问题的原因。

现在形状和渐变完美对齐,如图图 35-12 所示。当然,我们并不总是希望它们完全对齐。为了获得不同的效果,我们可能想要曝光一个特定的更大梯度的区域。无论目标是什么,理解渐变和我们使用的形状之间的关系是很重要的。

Image

图 35-12。对齐形状和渐变

使用径向渐变

我们用两个圆来定义径向梯度。渐变的开始由第一个圆定义,渐变的结束由第二个圆定义,我们在它们之间添加颜色停止。清单 35-12 提供了一个例子。

清单 35-12。使用径向渐变

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                  

            ctx.fillStyle = grad;             ctx.fillRect(0, 0, 500, 140);              

`

createRadialGradient方法的六个参数代表:

  • 起点圆中心的坐标(第一个和第二个参数)
  • 起始圆的半径(第三个参数)
  • 终点圆中心的坐标(第四个和第五个参数)
  • 终点圆的半径(第六个参数)

示例中的值给出了开始和结束圆,如图 35-13 中的所示。注意,我们可以指定画布之外的渐变(对于线性渐变也是如此)。

Image

图 35-13。起点和终点呈放射状渐变

在这个例子中,开始圆是较小的一个,被结束圆包围。当我们在这个渐变上添加色标时,它们被放置在开始圆的边缘(色标值0.0)和结束圆的边缘(色标值1.0)之间的线上。

Image 提示指定圆时要小心,不要让一个圆包含另一个圆。浏览器之间在如何导出渐变方面存在一些不一致,结果也很混乱。

因为我们能够指定两个圆的位置,所以圆边缘之间的距离可以变化,颜色之间的渐变率也将变化。你可以在图 35-14 中看到效果。

Image

图 35-14。使用径向梯度

图中显示了整个渐变,但同样的规则也适用于渐变与绘图形状的关系。清单 35-13 创建了一对显示渐变分段的小图形。

清单 35-13。使用带有径向渐变的较小形状

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                               ctx.fillRect(150, 20, 75, 50);

            ctx.lineWidth = 8;             ctx.strokeStyle = grad;             ctx.strokeRect(250, 20, 75, 50);              

`

注意,我可以对fillStylestrokeStyle属性使用渐变,使我们能够对线条和实心形状使用渐变,如图图 35-15 所示。

Image

图 35-15。为填充和描边使用径向渐变

使用模式

除了纯色和渐变,我们还可以创建图案。我们使用由 canvas 上下文对象定义的createPattern方法来实现这一点。2D 绘图上下文定义了对三种模式的支持——图像、视频和画布——但是只有图像模式被实现(而且只有 Firefox 和 Opera 实现)。在我写这篇文章时,其他浏览器忽略了这种模式类型。).

为了使用图像模式,我们将一个HTMLImageElement对象作为第一个参数传递给createPattern方法。第二个参数是重复样式,它必须是表 35-8 中显示的值之一。

清单 35-14 显示了我们如何创建和使用一个图像模式。

清单 35-14。使用图像模式

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                           

            ctx.fillStyle = pattern;             ctx.fillRect(0, 0, 500, 140);              

`

本例中的文档包含一个img元素,用户看不到它,因为我已经应用了hidden属性(在第四章中描述)。在脚本中,我使用 DOM 来定位表示作为createPattern方法第一个参数的img元素的HTMLImageElement对象。对于第二个参数,我使用了repeat值,这导致图像在两个方向上重复。最后,我将模式设置为fillStyle属性的值,并使用fillRect方法绘制一个与画布大小相同的填充矩形。你可以在图 35-16 中看到结果。

Image

图 35-16。创建图像模式

模式是从img元素的当前状态复制的,这意味着如果我们使用 JavaScript 和 DOM 来改变img元素的src属性值,模式不会改变。

与渐变一样,图案应用于整个画布,我们决定图案的哪些部分由我们绘制的形状显示。清单 35-15 展示了如何将该模式用于较小的填充和笔画形状。

清单 35-15。使用带有图像图案的较小形状

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                           

**            ctx.fillStyle = pattern;** **            ctx.fillRect(150, 20, 75, 50);**

**            ctx.lineWidth = 8;** **            ctx.strokeStyle = pattern;** **            ctx.strokeRect(250, 20, 75, 50);**              

`

你可以在图 35-17 中看到结果。

Image

图 35-17。使用带有图像图案的较小形状

保存和恢复绘图状态

我们可以保存绘图状态,并在以后使用表 35-9 中描述的方法返回。

Image

保存的绘图状态存储在后进先出(LIFO)堆栈中,因此我们使用save方法保存的最后一个状态是由restore方法恢复的第一个状态。清单 35-16 展示了这些正在使用的方法。

清单 35-16。保存和恢复状态

`

             Example                      canvas {border: thin solid black; margin: 4px}                   ` `                     Your browser doesn't support the canvas element                  
            Save             Restore         
        

            var colors = ["black", grad, "red", "green", "yellow", "black", "grey"];

            var cIndex = 0;

            ctx.fillStyle = colors[cIndex];             draw();

            var buttons = document.getElementsByTagName("button");             for (var i = 0; i < buttons.length; i++) {                 buttons[i].onclick = handleButtonPress;             }

            function handleButtonPress(e) {                 switch (e.target.innerHTML) {                     case 'Save': **                        ctx.save();**                         cIndex = (cIndex + 1) % colors.length;                         ctx.fillStyle = colors[cIndex];                         draw();                         break;                     case 'Restore':                         cIndex = Math.max(0, cIndex -1);                         ctx.restore();                         draw();                         break;                 }             }             function draw() {                 ctx.fillRect(0, 0, 500, 140);             }              

`

在这个例子中,我定义了一个包含 CSS 颜色名称和线性渐变的数组。当按下Save按钮时,使用save方法保存当前绘图状态。按下Restore按钮,恢复之前的绘图状态。在任一按钮按下后,调用draw函数,该函数使用fillRect方法绘制填充矩形。因为fillStyle属性是绘图状态的一部分,所以当按钮被按下时,该属性在数组中被提前和延迟,并被保存和恢复。在图 35-18 中可以看到效果。

Image

图 35-18。保存和恢复绘图状态

画布的内容不会被保存或恢复;仅保存或恢复绘图状态的特性值。这包括我们在本章已经看到的属性,比如lineWidthfillStylestrokeStyle,以及一些我在第三十六章中描述的附加属性。

绘制图像

我们可以使用drawImage方法在画布上绘制图像。这个方法需要三个、五个或九个参数。第一个参数总是图像的来源,它可以是表示一个imgvideo,或另一个canvas元素的 DOM 对象。清单 35-17 给出了一个例子,使用一个img元素作为源。

清单 35-17。使用 drawImage 方法

`

             Example                      canvas {border: thin solid black; margin: 4px}                                         Your browser doesn't support the canvas element                  ****                      ctx.drawImage(imageElement, 120, 10, 100, 120);             ctx.drawImage(imageElement, 20, 20, 100, 50, 250, 10, 100, 120);              

`

当使用三个参数时,第二个和第三个参数给出了画布上应该绘制图像的坐标。图像以其固有的宽度和高度绘制。当使用五个参数时,附加参数指定应该绘制图像的宽度和高度,覆盖固有大小。

使用九个参数时:

  • 第二个和第三个参数是源图像的偏移量。
  • 第四个和第五个参数是将要使用的源图像区域的宽度和高度。
  • 第六个和第七个参数指定画布坐标,在该坐标处将绘制所选区域的左上角。
  • 第八个和第九个参数指定所选区域的宽度和高度。

你可以在图 35-19 中看到这些参数的效果。

Image

图 35-19。绘制图像

使用视频图像

我们可以使用一个video元素作为drawImage方法的图像源。当我们这样做的时候,我们拍一张视频的快照。清单 35-18 提供了一个演示。

清单 35-18。使用视频作为 drawImage 元素的来源

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                         Video cannot be displayed                  
            Snapshot         
                              Your browser doesn't support the canvas element                       

`

在这个例子中,我有一个video元素、一个button,元素和一个canvas元素。当按钮被按下时,当前视频帧被用于使用drawImage方法绘制画布。你可以在图 35-20 中看到结果。

Image

图 35-20。使用视频作为画布绘制图像方法的来源

如果你发现自己在看 HTML5 演示,你会经常看到用来绘制视频的画布。这是使用我刚刚向你展示的技术,结合一个定时器(如第二十七章中描述的那样)来完成的。清单 35-19 展示了如何把这些放在一起。这不是我特别喜欢的技巧。如果您想知道原因,只需观察显示这种类型文档的机器上的 CPU 负载。

清单 35-19。使用画布显示和绘制视频

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                                  Your browser doesn't support the canvas element                              setInterval(function() {                 ctx.drawImage(imageElement, 0, 0, 360, 240);                 ctx.strokeRect(180 - (width/2),120 - (height/2), width, height);             }, 25);

            setInterval(function() {                 width = (width + 1) % 200;                 height = (height + 3) % 200;             }, 100);

             

`

在这个例子中,有一个我已经应用了hidden属性的video元素,所以它对用户是不可见的。我使用了两个定时器——第一个每 25 毫秒触发一次,绘制当前视频帧,然后绘制一个描边矩形。第二个计时器每隔 100 毫秒触发一次,并更改用于矩形的值。效果是矩形改变尺寸并叠加在视频图像上。你可以在图 35-21 中感受到这种效果,尽管为了充分理解所发生的事情,你应该将示例文档加载到浏览器中。

Image

图 35-21。使用计时器在画布上重新创建覆盖的视频

像这样使用视频元素时,我们不能使用内置控件。我使用了autoplay属性来保持例子的简单,但是一个更有用的解决方案是实现自定义控件,如第三十四章所示。

使用画布图像

我们可以使用一个画布的内容作为另一个画布上的drawImage方法的源,如清单 35-20 所示。

清单 35-20。使用画布作为 drawImage 方法的来源

`

             Example                      canvas {border: thin solid black}             body > * {float:left;}                                     ****             **Your browser doesn't support the canvas element**         ****         
            Press Me         
        ****             **Your browser doesn't support the canvas element**         <**/canvas>**         

            var width = 100;             var height = 10;             ctx.lineWidth = 5;             ctx.strokeStyle = "red";             ctx2.lineWidth = 30;             ctx2.strokeStyle = "black;"

            setInterval(function() {                 ctx.drawImage(imageElement, 0, 0, 360, 240);                 ctx.strokeRect(180 - (width/2),120 - (height/2), width, height);             }, 25);

            setInterval(function() {                 width = (width + 1) % 200;                 height = (height + 3) % 200;             }, 100);

            function takeSnapshot() {                 ctx2.drawImage(srcCanvasElement, 0, 0, 360, 240);                 ctx2.strokeRect(0, 0, 360, 240);             }              

`

在这个例子中,我添加了第二个canvas元素和一个button。当按钮被按下时,我使用代表原始canvasHTMLCanvasElement对象作为第一个参数,调用第二个canvas的上下文对象上的drawImage方法。实质上,按下按钮会获取左侧画布的快照,并将其显示在右侧画布上。我们复制画布上的一切,包括红色覆盖的矩形。我们可以执行进一步的绘制操作,这就是为什么我在第二个画布上绘制了一个粗黑边框作为快照的一部分。在图 22 中可以看到效果。

Image

图 35-22。使用一个画布作为另一个画布上 drawImage 方法的源

总结

在这一章中,我已经介绍了canvas元素,展示了如何绘制基本形状,如何配置、保存和恢复绘制状态,以及如何在绘制操作中使用纯色和渐变。我还展示了如何使用imgvideo,或其他canvas元素的内容作为图像源来绘制图像。在第三十六章,我将展示如何绘制更复杂的形状,以及如何应用效果和变换。