JavaScript-Web-应用高级教程-全-
JavaScript Web 应用高级教程(全)
一、做好准备
客户端 web 应用开发一直是服务器端编码的穷亲戚。这是因为浏览器和运行它们的设备没有企业级服务器强大。为了提供任何一种严肃的 web 应用功能,服务器必须为浏览器做所有繁重的工作,相比之下,这是相当愚蠢和简单的。
在过去的几年里,浏览器变得更加智能,功能更加强大,并且在实现 web 技术和标准方面更加一致。过去创造独特功能的斗争已经变成了创造最快和最兼容浏览器的斗争。智能手机和平板电脑的激增为高质量的 web 应用创造了一个巨大的市场,HTML5 的逐渐采用为 web 应用开发人员提供了构建丰富和流畅的客户端体验的坚实基础。
可悲的是,虽然客户端技术已经赶上了服务器端,但客户端程序员使用的技术仍然落后。客户端 web 应用的复杂性已经达到了一个临界点,规模、优雅和可维护性是必不可少的,快速解决方案的时代已经过去了。在本书中,我将公平竞争,向您展示如何加快您的客户端开发,以接受来自服务器端世界的最佳技术,并将它们与最新的 HTML5 特性相结合。
关于这本书
这是我关于技术的第 15 本书,为了纪念这一点,Apress 要求我做一些不同的事情:分享我用来创建复杂的客户端 web 应用的工具、技巧和技术。其结果是比我的常规工作更加个人化、非正式和不拘一格。我将向您展示如何从服务器端开发中获取工业级的开发概念,并将它们应用到浏览器中。通过使用这些技术,您可以构建更容易编写、更容易维护的 web 应用,并为您的用户提供更好、更丰富的功能。
你是谁?
你是一个经验丰富的 web 开发人员,你的项目已经开始失控。JavaScript 代码中的错误越来越多,找到并修复每个错误需要更长的时间。您的目标是越来越广泛的设备,包括台式机、平板电脑和智能手机,保持所有这些设备正常工作变得越来越困难。你的工作日变长了,但是你花在新特性上的时间却变少了,因为维护你已经拥有的代码消耗了你大量的时间。
来自工作的兴奋已经消退,你已经忘记了编码的一天是什么感觉。你知道出了问题,你知道你正在失去控制,你知道你需要找到一种不同的方法。如果这听起来很熟悉,那么你是我的目标读者。
在你阅读这本书之前,你需要知道什么?
这是一本高级的书,你需要是一个有经验的 web 程序员才能理解内容。您需要 HTML 的工作知识,您需要知道如何编写 JavaScript,并且您已经使用这两者创建了客户端 web 应用。您需要理解浏览器是如何工作的,HTTP 是如何适应这种情况的,什么是 Ajax 请求,以及为什么您应该关心它们。
如果你没有这样的经历呢?
你可能仍然会从这本书里得到一些好处,但是你必须自己弄清楚一些基本的东西。我已经写了几本其他的书,作为这本书的入门,你可能会觉得有用。如果你是 HTML 新手,那么请阅读 HTML5 的权威指南。这解释了创建常规 web 内容和基本 web 应用所需的一切。我解释了如何使用 HTML 标记和 CSS3(包括新的 HTML5 元素),以及如何使用 DOM API 和 HTML5 APIs(如果您不熟悉这门语言,还包括一个 JavaScript 初级读本)。我在本书中大量使用了 jQuery。我提供了每个主题所需的所有信息,但是如果你想更好地了解 jQuery 如何工作以及它与 DOM API 的关系,那么请阅读 Pro jQuery 。这两本书都是出版社出版的。
除了书籍,通过阅读 W3C 在[www.w3.org](http://www.w3.org)发布的规范,你可以学到很多关于 HTML 和浏览器 API 的知识。这些规范是权威的,但可能很难做到,也不总是那么清晰。一个更容易获得的资源是位于[developer.mozilla.org](http://developer.mozilla.org)的 Mozilla 开发者网络。从 HTML 到 JavaScript,这是一个极好的信息来源。人们普遍偏向于 Firefox,但这通常不是问题,因为主流浏览器在实现 web 标准的方式上通常是兼容且一致的。
这是一本关于 HTML5 的书吗?
不,尽管我确实谈到了一些新的 HTML5 JavaScript APIs。这本书的大部分内容都是关于技术的,其中大部分都适用于 HTML4,就像它适用于 HTML5 一样。有些章节纯粹是基于 HTML5 APIs 构建的(比如第五章和第六章,它们向你展示了如何创建离线工作的 web 应用,以及如何在浏览器中存储数据),但其他章节并没有绑定到任何特定的 HTML 版本。我没有详细介绍 HTML5 中描述的新元素。这是一本关于编程的书,新元素对 JavaScript 编程没有太大影响。
这本书的结构是怎样的?
在第二章的中,我为一个虚构的奶酪零售商 CheeseLux 构建了一个简单的 web 应用,它基于我在本章后面介绍的基本示例。我遵循一些非常标准的方法来创建这个 web 应用,并在本书的剩余部分向您展示如何应用工业级技术来改进不同的方面。我试图将每一章合理地分开,但这是一本相当非正式的书,我确实在几章中逐步介绍了一些概念。每章都建立在前面章节介绍的技术之上。如果可以的话,你应该按章节顺序读这本书。以下部分总结了本书的章节。
第一章:做好准备
除了描述这本书,我还介绍了 CheeseLux 示例的静态 HTML 版本,我在这本书里一直使用它。我还列出了您需要的软件,如果您想自己重新创建示例或尝试本书附带的源代码下载中的清单(可从 Apress.com 免费获得)。
第二章:入门
在这一章中,我使用一些基本的技术来创建一个更动态的 CheeseLux 示例版本,从一个网站转移到一个 web 应用。我以此为契机,介绍本书剩余部分需要的一些工具和概念,并提供一个背景,以便我可以在后面的章节中展示更好的技术。
第三章:添加视图模型
我描述的第一个高级技术是在 web 应用中引入客户端视图模型。视图模型是设计模式中的关键组件,如模型视图控制器(MVC)和模型-视图-视图模型。如果你只采用这本书里的一种技术,那就选这一种;它将对您的开发实践产生最大的影响。
第四章:使用 URL 路由
URL 路由允许您扩展 web 应用中的导航机制。您可能没有意识到您有一个导航问题,但是当您看到 URL 路由如何在客户端工作时,您将会看到它是一种多么强大和灵活的技术。
第五章:创建离线网络应用
在这一章中,我将向您展示如何使用一些新的 HTML5 JavaScript APIs 来创建即使在用户离线时也能工作的 web 应用。这是一项强大的技术,随着智能手机和平板电脑进入市场,这项技术变得越来越重要。永远在线的网络连接的想法正在改变,能够适应离线工作对于许多 web 应用来说是必不可少的。
第六章:存储数据
除非你还能访问存储的数据,否则离线运行 web 应用没有多大用处。在本章中,我将向您展示可用于存储不同类型数据的不同 HTML5 APIs,从简单的名称/值对到持久化 JavaScript 对象的可搜索层次结构。
第七章:创建响应式网络应用
在传统的桌面和移动设备分类之外,还有许多网络设备类别。应对不同设备类型激增的一种方法是创建 web 应用,这些应用能够动态适应它们所使用的设备的功能,根据需要定制它们的外观、功能和交互模型。在这一章中,我将向你展示如何发现你关心的能力并对它们做出反应。
第八章:创建移动网络应用
创建响应式 web 应用的另一种方法是创建一个针对特定设备的单独版本。在本章中,我将向您展示如何使用 jQuery Mobile 来创建这样一个 web 应用,以及如何将 URL 路由等高级功能整合到移动 web 应用中。
第九章:写更好的 JavaScript
本书的最后一章是关于改进代码的——不是指更好地使用 JavaScript,而是指创建易于维护的代码模块,这些模块更易于在自己的项目中使用,也更易于与他人共享。我将向您展示一些基于约定的方法,并介绍异步模块定义,当外部库依赖于其他功能时,异步模块定义可以解决一些复杂的问题。我还将向您展示如何轻松地对客户端代码应用单元测试,包括如何对复杂的 HTML 转换进行单元测试。
你描述过设计模式吗?
我没有。这不是那种书。这是一本关于获得结果的书,我不会花太多时间讨论支撑我描述的每种技术的设计模式。如果你正在读这本书,那么你希望看到那些结果,并得到它们提供的好处现在。我的建议是解决你眼前的问题,然后开始研究理论。有很多关于设计模式和相关理论的有用信息。维基百科是一个很好的起点。一些读者可能会对维基百科作为编程信息来源的想法感到惊讶,但它提供了大量平衡且编写良好的内容。
我喜欢设计模式。我认为它们是重要的、有用的,并且是交流复杂问题的一般解决方案的有价值的机制。可悲的是,它们经常被用作一种宗教,在这种宗教中,模式的每个方面都必须完全按照指定的方式应用,并且关于竞争模式的优点和适用性的长期而令人讨厌的冲突就会爆发。
我的建议是将设计模式视为开发技术的基础。混合搭配不同的设计模式以适应您的项目,并挑选出能解决您所面临问题的部分。不要让任何人决定你使用模式的方式,并且始终专注于为真实的用户解决真实项目中的真实问题。你开始争论理论问题的解决方案的那一天,就是你走向黑暗面的那一天。要坚强。保持专注。抵制模式狂热者。
你会谈论平面设计和布局吗?
不。这也不是那种书。示例 web 应用的布局非常简单。这有几个原因。首先,这是一本关于编程的书,虽然我花了很多时间向您展示动态管理标记的技术,但实际的视觉效果是一个很大的副作用。
第二个原因是我有柠檬的艺术才能。我不画画,不作画,也没有副业在当地画廊卖我的布面油画作品。事实上,当我还是个孩子的时候,我被免除了艺术课,因为我完全没有天赋。我是一个相当好的程序员,但是我的设计技巧很烂。在这本书里,我坚持我所知道的,也就是重载编程。
如果你不喜欢我描述的技术或工具怎么办?
然后你调整这些技术,直到你确实喜欢它们,并找到以你喜欢的方式工作的替代工具。本书中的关键信息是,你可以应用重型服务器端技术来创建更好的 web 应用。精细的实现细节并不重要。我喜欢的工具和技术对我来说很好,如果你像我一样思考代码,它们也会对你很好。但是,如果你的思维以不同的方式工作,改变我的方法中不适合的部分,丢弃那些不起作用的部分,并使用剩下的部分作为你自己方法的基础。只要你最终开发出可伸缩性更好的 web 应用,让你的编码更有趣,并减轻维护负担,我们都会领先。
这本书的代码多吗?
是的。事实上,代码太多了,我都装不下。书籍有一个页面预算,在项目开始时就设定好了。页面预算影响图书的进度、生产成本和图书的最终售价。坚持页面预算是一件大事,每当我的编辑认为我要花很长时间(嗨,本!).我必须做一些编辑,以适应所有我想包含的代码。所以,当我引入一个新主题或者一口气做了很多改动的时候,我会给你看一个完整的 HTML 文档或者 JavaScript 代码文件,就像清单 1-1 中显示的那样。
清单 1-1。一个完整的 HTML 文档
`
Would you like to use our mobile web app?
这个列表基于第八章中的一个。完整的清单给你一个更广阔的背景,让你了解手头的技术如何适应 web 应用世界。当我展示一个小的变化或者强调一个特定的代码区域时,我会展示一个类似于清单 1-2 中的代码片段。
清单 1-2。一段代码片段
`...
<title>CheeseLux</title> <script src="jquery-1.7.1.js" type="text/javascript"></script> <script src="jquery.mobile-1.0.1.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/> <link rel="stylesheet" type="text/css" href="styles.mobile.css"/> **<meta name="viewport" content="width=device-width, initial-scale=1">** <script> ...`这些片段被累积应用到最后一个完整的列表中,这样,列表 1-2 中的片段显示一个meta元素被添加到列表 1-1 的head部分。如果您想对示例进行实验,您不必自己应用这些更改。相反,你可以从 Apress.com 下载本书中所有代码的完整列表。这个免费下载还包括我在本章后面提到的服务器端代码,并在本书中用来创建 web 应用的不同方面。
这本书需要什么软件?
如果你想重现书中的例子,你需要一些软件。每种类型都有很多选择,我用的都是免费的。我在接下来的章节中描述了每一种工具,以及我在每一类中的首选工具。
获取源代码
你需要下载这本书的源代码,Apress.com 免费提供。源代码下载包含按章节组织的所有清单和所有支持资源,比如图像和样式表。如果您想要完全重新创建任何示例,您将需要该下载的内容。
获得一个 HTML 编辑器
几乎任何编辑器都可以用来处理 HTML。我不依赖这本书的任何特性,所以使用任何适合你的编辑器。我在活动状态下使用 Komodo Edit。它免费且简单,对 HTML、JavaScript、jQuery 和 Node.js 都有很好的支持。可以从[activestate.com](http://activestate.com)获得 Komodo Edit,有 Windows、Mac、Linux 的版本。
获得桌面网络浏览器
任何现代主流桌面浏览器都会运行本书中的例子。我喜欢谷歌 Chrome 我发现它很快,我喜欢简单的用户界面,开发者工具也很不错。这本书里的大部分截图都是谷歌 Chrome 的,虽然有时候我会用 Firefox,因为 Chrome 没有完全实现 HTML5 的功能。(在我写这篇文章时,对 HTML5 APIs 的支持有点复杂,但每个浏览器版本都会改善这种情况。)
获得移动浏览器模拟器
在第七章和第八章中,我谈到了针对不同类型的设备。在开发的早期阶段,处理真实设备可能是一项缓慢而令人沮丧的工作,所以我使用一个移动浏览器模拟器来开始并将主要功能放在一起。直到我有了一些实用的、可靠的东西,我才开始在真正的移动设备上测试。
我喜欢 Opera 手机模拟器,从[www.opera.com/developer/tools/mobile](http://www.opera.com/developer/tools/mobile)开始可以免费获得;有适用于 Windows、Mac 和 Linux 的版本。该模拟器使用与真实的、广泛使用的 Opera Mobile 相同的代码库,虽然有一些奇怪的地方,但体验相当忠实于原作。我喜欢这个包,因为它让我可以为从小屏幕智能手机到高清平板电脑的不同屏幕尺寸创建模拟器。支持模拟触摸事件和改变设备的方向。你可以在任何浏览器中运行第七章和第八章中的例子,但是这些章节的部分重点是优雅地检测移动设备,并且你将通过使用仿真器获得最佳结果,即使它不是用于 Opera 的。
获取 JavaScript 库
我不相信重新创建一个写得很好、公开可用的 JavaScript 库中的功能。为此,我在每一章中都使用了一些库。有些是众所周知的,如 jQuery、jQuery UI 和 jQuery Mobile,但也有一些提供了一些特殊功能或弥补了没有实现某些 HTML5 APIs 的浏览器的空白。在我介绍时,我会告诉您如何获得每个库,它们都可以在 Apress.com 的源代码下载中找到。为了使用我讨论的技术,您不需要使用我喜欢的库,但是您将需要它们来重新创建示例。
获取 Web 服务器
本书中的例子集中在客户端 web 应用上,但是有些技术需要服务器的某些行为。大多数例子都适用于任何 web 服务器提供的内容,但是如果您想要重新创建本书中的每个例子,您将需要使用 Node.js。
我选择 Node.js 的原因是它是用 JavaScript 编写的,并且在许多平台上都得到支持。这意味着本书的任何读者都能够设置服务器,阅读并理解驱动服务器的代码。
服务器端代码包含在从 Apress.com 下载的源代码中,在一个名为server.js的文件中。我不打算详细介绍这段代码,也不打算列出它。它没有做任何特别的事情;它只是提供内容,并有几个特殊的 URL,允许我从示例 web 应用中发布数据,并获得定制的响应。还有一些其他的 URL 会产生特殊的效果,比如给一些请求增加延迟。如果你想知道里面有什么,可以看一看server.js,但是你不需要理解(甚至不需要看)服务器端代码就可以从这本书里得到最好的东西。
但是,您需要安装和设置 Node.js,以便它可以在您的网络上运行。在接下来的部分中,我提供了启动和运行的说明。
获取和准备 Node.js
可以从[nodejs.org](http://nodejs.org)下载 Node.js。安装包可用于 Windows、Mac 和 Linux,如果您想为不同的平台编译,源代码也是可用的。设置 Node 的说明经常变化,最好的入门方法是阅读 Felix Geisendö rfer 的 Node 初学者指南,你可以在[nodeguide.com/beginner.html](http://nodeguide.com/beginner.html)找到。
我依赖于一些第三方模块,所以在安装 Node.js 包之后运行以下命令:
npm install node-static jqtpl
这个命令下载并安装我在示例中用来交付静态和模板化内容的node-static和jqtpl包。该命令将生成类似如下的输出(但是您可能会看到一些额外的警告,这些警告可以忽略):
npm http GET https://registry.npmjs.org/node-static npm http GET https://registry.npmjs.org/jqtpl npm http 200 https://registry.npmjs.org/jqtpl npm http 200 https://registry.npmjs.org/node-static node-static@0.5.9 ./node_modules/node-static jqtpl@1.0.9 ./node_modules/jqtpl
源代码下载是按章节组织的。您需要在 Node.js 目录中创建一个名为content的目录,并将章节内容复制到其中。content目录没有多少结构;为了简单起见,几乎所有的资源和清单都在同一个目录中。
注意章节之间的资源文件有变化,所以在章节内容之间移动时,一定要清除浏览器的历史记录。
您还需要将源代码下载中的server.js文件复制到 Node.js 目录中。这个节点脚本只为书中的例子服务;不要依赖它做任何其他用途,当然也不要用它来托管真正的项目。一旦一切就绪,只需运行以下命令:
node server.js
您将看到下面的输出(或与之非常接近的内容):
The "sys" module is now called "util". It should have a similar interface. Ready on port 80
如果您使用的是 Windows,可能会提示您允许 Node 通过 Windows 防火墙进行通信,您应该这样做。这样,您的服务器就可以正常运行了。该脚本监听端口 80 上的请求。如果您需要更改这一点,请在server.js文件中查找以下行:
http.createServer(handleRequest).listen(80);
注意 Node.js 非常不稳定,经常发布新版本。我在这本书里使用的版本是 0.6.6,但是当你读到这本书的时候,它已经被取代了。我坚持使用更稳定的节点 API,但是您可能需要做一些小的调整来让一切正常工作。
介绍 CheeseLux 示例
本书中的大多数例子都基于一个虚构的奶酪零售商 CheeseLux 的 web 应用。我想把重点放在本书中的个别技术上,所以我尽可能保持 web 应用的简单性。首先,我创建了一个静态网站,向用户提供有限的产品。站点的入口点是example.html文件。我用example.html来表示这本书里几乎所有的列表。清单 1-3 显示了example.html的初始静态版本。
清单 1-3。静态 example.html
`

Gourmet European Cheese
我从一些基本的东西开始。web 应用的静态版本有四个页面,尽管在后面的章节中我倾向于只关注前两个页面的功能。这些是产品列表和显示用户选择的购物篮(在静态版本中由basket.html处理)。在图 1-1 中可以看到example.html和basket.html是如何在浏览器中显示的。

图 1-1。浏览器中显示的 example.html 和 basket.html 文件
您不需要对静态文件做任何事情,但是如果您查看basket.html的内容,例如,您会看到我使用模板来生成基于通过 HTML 表单提交的数据的内容,如清单 1-4 所示。
清单 1-4。使用模板生成内容
`
<head> <title>CheeseLux</title> <link rel="stylesheet" type="text/css" href="styles.css"/> </head> <body> <div id="logobar"> <img src="cheeselux.png"> <span id="tagline">Gourmet European Cheese</span> </div> <form action="/shipping" method="post"> <div class="cheesegroup"> <div class="grouptitle">Your Basket</div> <table class="basketTable" border=0> <thead> <tr><th>Cheese</th><th>Quantity</th><th>Subtotal</th></tr>` ` <tr><td class="sumline" colspan=3></td></tr> </thead> <tbody> **{{each properties}}** **{{if $value.propVal > 0}}** **<tr>** **<td>${$data.getProp($value.propName, "name")}</td>** **<td>${$value.propVal}</td>** **<td>** **$${$data.getSubtotal($value.propName, $value.propVal)}** **</td>** **</tr>** **{{/if}}** **{{/each}}** </tbody> <tfoot> <tr><td class="sumline" colspan=3></td></tr> <tr><th colspan=2>Total:</th><td>$${$data.total}</td> </tfoot> </table> <div class="cornerplaceholder"></div> </div> <div id="buttonDiv"> <input type="submit" /> </div> **{{each properties}}** **<input type="hidden" name="${$value.propName}" value="${$value.propVal}"/>** **{{/each}}** </form> </body> </html>`这些模板由您为 Node.js 下载的jqtpl模块处理。这个模块是一个简单模板库的节点兼容版本,广泛用于 jQuery 库。我在客户端示例中不使用这种风格的模板,但是我想解释一下这些标记的含义,以防您想偷看静态内容。
在下一章中,我将使用一些基本的 JavaScript 技术来创建这个简单应用的更动态的版本,然后在本书的剩余部分向您展示更高级的技术,您可以使用这些技术来为您自己的项目创建更好、更可伸缩、响应更快的 web 应用。
字体归属
我在这本书里使用了一些自定义的网络字体。字体文件包含在从 Apress.com 下载的源代码中。我使用的字体来自可移动类型联盟([www.theleagueofmoveabletype.com](http://www.theleagueofmoveabletype.com))和谷歌网络字体服务([www.google.com/webfonts](http://www.google.com/webfonts))。
总结
在这一章中,我概述了这本书的内容和结构,并列出如果你想用书中的例子进行实验所需的软件。我还介绍了 CheeseLux 示例,该示例贯穿了本书。在下一章,我将使用一些基本的技术来增强静态网页,并介绍一些我在本书中使用的核心工具。从那时起,我将向您展示一系列更好的工业级技术,这是本书的核心。
二、入门指南
在这一章中,我将增强我在第一章中介绍的示例 web 应用。这些都是入门级的技术,本书的大部分内容致力于向您展示改善结果的不同方法。这并不是说本章中的例子没有用;对于简单的 web 应用来说,它们绝对没问题。但是它们对于大型复杂的 web 应用来说是不够的,这就是为什么接下来的章节解释了如何从服务器端开发的世界中获取关键概念并应用到您的 web 应用中。
这一章也让我为一些我将在本书中使用的 web 应用开发原则奠定了基础。首先,我将尽可能依赖 JavaScript 库,以避免创建别人已经生成和维护的代码。我将最常用的库是 jQuery,以使使用 DOM API 变得更简单和容易(我在本章的例子中解释了一些 jQuery 基础知识)。第二,我将专注于单个 HTML 文档。
升级提交按钮
首先,我将使用 JavaScript 替换第一章中基线示例中的提交按钮。浏览器从一个类型为submit的input元素创建了这个按钮,我将把它换成与文档其余部分视觉上一致的东西。更具体地说,我将使用 jQuery 来替换input元素。
准备使用 jQuery
DOM API 很全面,但使用起来很笨拙——笨拙到有许多 JavaScript 便利库包装了 DOM API,使它更容易使用。根据我的经验,这些库中最好的是 jQuery,它易于使用,并且得到了积极的开发和支持。jQuery 也是许多其他 JavaScript 库的基础,其中一些我稍后会用到。jQuery 只是 DOM API 的包装器,如果需要的话,它允许使用底层的 DOM 对象和方法。
您可以从 jQuery.com 下载 jQuery 库。和大多数 JavaScript 库一样,jQuery 有两个版本。未压缩版本包含完整的源代码,对于开发和调试非常有用。压缩版本(也称为最小化的版本)要小得多,但是不可读。较小的尺寸使最小化版本非常适合在 web 应用部署到生产环境中时节省带宽。对于流行的网络应用来说,带宽可能很贵,任何节省都是值得的。
下载你想要的版本,放在你的content目录下,和example.html放在一起。我将在本书中使用未压缩版本,所以我下载了一个名为jquery-1.7.1.js的文件。
提示我使用的是未压缩版本,因为它们使调试更容易,当你探索本书中的例子时,你会发现这很有用。对于真正的 web 应用,您应该在部署之前切换到最小化版本。
文件名包括 jQuery 版本,在我写这篇文章时是 1.7.1。使用一个script元素将 jQuery 库导入到示例文档中,如清单 2-1 所示。我已经在文档的head部分添加了script元素。
清单 2-1。将 jQuery 导入示例文档
`...
<head> <title>CheeseLux</title> <link rel="stylesheet" type="text/css" href="styles.css"/> ** <script src="jquery-1.7.1.js" type="text/javascript"></script>** </head> ...`使用 CDN 进行 JQUERY
在您自己的 web 服务器上托管 jQuery 库的另一种方法是使用托管 jQuery 的公共内容分发网络 (CDN)。CDN 是一个由服务器组成的分布式网络,使用离用户最近的服务器向用户交付文件。使用 CDN 有几个好处。首先是用户体验更快,因为 jQuery 库文件是从离他们最近的服务器上下载的,而不是从您的服务器上。通常根本不需要这个文件。jQuery 如此受欢迎,以至于用户的浏览器可能已经缓存了来自另一个也使用 jQuery 的应用的库。第二个好处是,您不会将宝贵而昂贵的带宽花费在向用户交付 jQuery 上。
使用 CDN 的时候,一定要对 CDN 运营商有信心。您希望确保用户收到他们应该收到的文件,并且服务将始终可用。谷歌和微软都免费为 jQuery(以及其他流行的 JavaScript 库)提供 CDN 服务。两家公司都有运行高可用性服务的丰富经验,不太可能故意篡改 jQuery 库。你可以在[www.asp.net/ajaxlibrary/cdn.ashx](http://www.asp.net/ajaxlibrary/cdn.ashx)了解微软服务,在[code.google.com/apis/libraries/devguide.html](http://code.google.com/apis/libraries/devguide.html)了解谷歌服务。
CDN 方法不适合在内部网中交付给用户的应用,因为它会导致所有浏览器都通过互联网来获取 jQuery 库,而不是访问本地服务器,后者通常更近、更快且带宽成本更低。
所以,让我们直接使用 jQuery 来隐藏现有的input元素,并在它的位置上添加一些东西。清单 2-2 展示了这是如何做到的。
清单 2-2。隐藏输入元素并添加另一个元素
`
Gourmet European Cheese
我在文档中添加了另一个script元素。该元素包含内联代码,而不是加载外部 JavaScript 文件。我这样做是因为这样更容易向你展示我所做的改变。jQuery 并不要求使用内联代码,如果愿意,可以将 jQuery 代码放在外部文件中。在script元素的四个 JavaScript 语句中发生了很多事情,所以我将在接下来的章节中一步一步地进行分解。
了解就绪事件
jQuery 的核心是$函数,这是开始使用 jQuery 特性的一种便捷方式。使用 jQuery 最常见的方式是将$视为一个 JavaScript 函数,并传递一个 CSS 选择器或一个或多个 DOM 对象作为参数。在 jQuery 中使用$函数是很常见的。例如,我在四行代码中使用了三次。
$函数返回一个 jQuery 对象,您可以在其上调用 jQuery 方法。jQuery对象是您选择的元素的包装器,如果您传递一个 CSS 选择器作为参数,jQuery对象将包含文档中与您指定的选择器匹配的所有元素。
提示这是 jQuery 优于内置 DOM API 的主要优势之一:可以更容易地选择和修改多个元素。DOM API 的最新版本(包括 HTML5 的一部分)提供了使用选择器查找元素的支持,但是 jQuery 做得更简洁和优雅。
第一次使用清单中的$函数时,我将document对象作为参数传入。document对象是 DOM 中元素层次的根节点,我用$函数选择了它,这样我就可以调用ready方法,如清单 2-3 中突出显示的。
清单 2-3。选择文档并调用就绪方法
`...
...`
浏览器一找到文档中的script元素就执行 JavaScript 代码。当您想要操作 DOM 中的元素时,这就给我们带来了一个问题,因为您的代码是在浏览器解析完 HTML 文档的其余部分、发现了您想要处理的元素并将对象添加到 DOM 以表示它们之前执行的。最好的情况是您的 JavaScript 代码不工作,最坏的情况是当这种情况发生时您会导致一个错误。有许多方法可以解决这个问题。最简单的解决方案是将script元素放在文档的末尾,这样浏览器就不会发现并执行您的 JavaScript 代码,直到 HTML 的其余部分被处理完。一种更优雅的方法是使用 jQuery ready方法,它在刚刚显示的清单中突出显示。
您将一个 JavaScript 函数作为参数传递给ready方法,一旦浏览器处理完文档中的所有元素,jQuery 就会执行这个函数。使用ready方法允许您将script元素放在文档中的任何地方,因为知道您的代码直到正确的时刻才会被执行。
注意一个常见的错误是忘记将要执行的 JavaScript 语句封装在一个函数中,这会导致奇怪的效果。如果您向ready方法传递一个语句,那么它将在浏览器处理script元素时立即执行。如果您传递多个语句,那么浏览器通常会报告一个 JavaScript 错误。
ready方法为ready事件创建一个处理程序。在本章的后面,我将向您展示 jQuery 支持事件的更多方式。ready事件仅对document对象可用,这就是为什么您会在几乎所有使用 jQuery 的 web 应用的清单中看到突出显示的语句。
选择和隐藏输入元素
既然我已经将 JavaScript 代码的执行推迟到 DOM 准备好之后,那么我可以转到任务的下一步,即隐藏提交表单的input元素。清单 2-4 突出显示了例子中这样做的语句。
清单 2-4。选择并隐藏输入元素
`...
...`
这是一个经典的由两部分组成的 jQuery 语句:首先,我选择我想要处理的元素,然后我应用一个 jQuery 方法来修改所选择的元素。您可能不认识我使用的选择器,因为:submit部分是除 CSS 规范中定义的选择器之外 jQuery 定义的选择器之一。表 2-1 包含了最有用的 jQuery 自定义选择器。
注意jQuery 定制选择器非常有用,但是它们会影响性能。只要有可能,jQuery 就使用本地浏览器支持来查找文档中的元素,这通常非常快。然而,jQuery 必须以不同的方式处理自定义选择器,因为浏览器对它们一无所知,这比本机方法花费的时间更长。这种性能差异对于大多数 web 应用来说无关紧要,但是如果性能很重要,您可能希望坚持使用标准的 CSS 选择器。

在清单 2-4 中,我的选择器匹配任何类型为submit的input元素,并且它是id属性为buttonDiv的元素的后代。我不需要对选择器如此精确,因为它是文档中唯一的submit元素,但是我想演示 jQuery 对选择器的支持。$函数返回一个包含所选元素的jQuery对象,尽管在本例中只有一个元素与选择器匹配。
选择元素后,我调用hide方法,通过将 CSS display属性设置为none来改变所选元素的可见性。input元素是这样的方法调用之前的:
<input type="submit">
并且在方法调用之后像这样转换:
<input type="submit" **style="display: none; "**>
浏览器不会显示display属性为none的元素,因此input元素变得不可见。
提示hide方法的对应方法是show,它删除了display设置,并将元素返回到可见状态。我将在本章后面演示show方法。
插入新元素
接下来,我想在文档中插入一个新元素。清单 2-5 突出显示了例子中这样做的语句。
清单 2-5。向文档添加新元素
`...
...`
在这个语句中,我将一个 HTML 片段字符串传递给了 jQuery $函数。这导致 jQuery 解析片段并创建一组对象来表示它包含的元素。然后这些元素对象在一个jQuery对象中返回给我,就好像我从文档本身中选择了元素一样,只是浏览器还不知道这些元素,它们还不是 DOM 的一部分。
在这个清单的 HTML 片段中只有一个元素,所以 jQuery 对象包含一个a元素。为了将这个元素添加到 DOM 中,我调用 jQuery 对象上的appendTo方法,传入一个 CSS 选择器,它告诉 jQuery 我希望将元素插入到文档中的什么位置。
appendTo方法将我的新元素作为选择器匹配的元素的最后一个子元素插入。在本例中,我指定了buttonDiv元素,这意味着我的 HTML 片段中的元素被插入到隐藏的input元素旁边,就像这样:
`...
...`
提示如果我传递给appendTo方法的选择器匹配了多个元素,那么 jQuery 将复制 HTML 片段中的元素,并插入一个副本作为每个匹配元素的的最后一个子元素。
jQuery 定义了许多方法,可以用来将子元素插入到文档中,其中最有用的方法在表 2-2 中有描述。当您追加元素时,它们成为其父元素的最后一个子元素。当你前置元素时,它们成为其父元素的第一个子元素。(我将在本章后面解释为什么有两个 append 和两个 prepend 方法。)

应用 CSS 类
在前面的例子中,我插入了一个a元素,但是我没有将它分配给一个 CSS 类。清单 2-6 展示了我如何通过调用addClass方法来纠正这个遗漏。
清单 2-6。链接 jQuery 方法调用
`...
...`
请注意,我只是将对addClass方法的调用添加到了语句的末尾。这被称为方法链接,一个支持方法链接的库据说有一个流畅的 API 。
大多数 jQuery 方法返回的 jQuery 对象与调用该方法时返回的对象相同。在这个例子中,我通过向$函数传递一个 HTML 片段来创建jQuery对象。这产生了一个包含一个a元素的jQuery对象。appendTo方法将元素插入到文档中,并返回一个 jQuery 对象,该对象包含与结果相同的a元素。这允许我进行进一步的方法调用,比如对addClass的调用。流畅的 API 可能需要一段时间来适应,但它们可以使代码简洁而富有表现力,并减少重复。
addClass方法将参数指定的类添加到选定的元素中,如下所示:
`...
...`在styles.css中定义了a.button类,它使a元素的外观与文档的其余部分保持一致。
了解方法对和方法链
如果你看一下表 2-2 中描述的方法,你会发现你可以用两种方式附加或前置元素。插入的元素可以包含在调用方法的 jQuery 对象中,也可以包含在方法参数中。jQuery 提供了不同的方法,因此您可以选择哪些元素包含在用于方法链接的jQuery对象中。在我的例子中,我使用了appendTo方法,这意味着我可以安排事情,使jQuery对象包含从 HTML 片段解析的元素,允许我链接对addClass方法的调用,并将类应用于a元素。
append方法颠倒了父元素和子元素之间的关系,如下所示:
$('#buttonDiv').append('<a href=#>Submit Order</a>').addClass("button");
在这个语句中,我选择父元素并提供 HTML 片段作为方法参数。append 方法返回一个包含buttonDiv元素的jQuery对象,因此addClass对父div元素生效,而不是新的a元素。
概括一下,我隐藏了原来的input元素,添加了一个a元素,最后,将a元素赋给了button类。你可以在图 2-1 中看到结果。

图 2-1。替换标准表单提交按钮
用四行代码(其中只有两行操作 DOM),我将标准的提交按钮升级为与 web 应用的其余部分一致的东西。正如我在本章开始时所说的,一点点代码可以带来显著的增强。
应对事件
我还没有完全完成新的a元素。浏览器知道一个类型属性为submit的input元素应该向服务器提交 HTML 表单,当按钮被点击时,它会自动执行这个动作。
我添加到 DOM 中的a元素看起来像一个按钮,但是浏览器不知道这个元素的用途,所以没有应用相同的自动操作。我必须添加一些 JavaScript 代码来完成这个效果,并使a元素的行为像一个按钮,而不只是看起来像。
您可以通过响应事件来做到这一点。事件是当元素的状态改变时,例如,当用户单击元素或将鼠标移到元素上时,浏览器发送的消息。您告诉浏览器您对哪些事件感兴趣,并提供事件发生时执行的 JavaScript 回调函数。当一个事件被浏览器发送时,据说已经被触发,回调函数负责处理该事件。在接下来的部分中,我将向您展示如何处理事件来完成替换按钮的功能。
处理点击事件
本例中最重要的是click,当用户按下并释放鼠标按钮时(换句话说,当用户单击一个元素时)触发该事件。对于这个例子,我想通过向服务器提交 HTML 表单来处理click事件。DOM API 提供了处理事件的支持,但是 jQuery 提供了一个更好的选择,你可以在清单 2-7 中看到。
清单 2-7。处理点击事件
`
Gourmet European Cheese
jQuery 提供了一些有用的方法,使得处理常见事件变得简单。这些事件以事件命名;因此,click方法将作为方法参数传递的回调函数注册为click事件的处理程序。我已经将对click事件的调用链接到创建和格式化a元素的其他方法。为了提交表单,我通过类型选择了form元素并调用了submit方法。这就是全部了。我现在已经有了按钮的基本功能。它不仅与 web 应用的其他部分具有相同的视觉样式,而且单击按钮会将表单提交给服务器,就像最初的按钮一样。
处理鼠标悬停事件
我还想处理另外两个事件来完成按钮功能;他们是mouseenter和mouseleave。当鼠标指针在元素上移动时触发mouseenter事件,当鼠标离开元素时触发mouseleave事件。
我想处理这些事件,给用户一个可以点击按钮的视觉提示,当鼠标在元素上时,我通过改变按钮的样式来做到这一点。处理这些事件最简单的方法是使用 jQuery hover方法,如清单 2-8 所示。
清单 2-8。使用 jQuery 悬停方法
`...
...`
hover方法将两个函数作为参数。第一个函数在mouseenter事件被触发时执行,第二个函数响应mouseleave事件被触发。在这个例子中,我使用这些函数从a元素中添加和删除了buttonHover类。该类更改 CSS background-color属性的值,以便当鼠标位于元素上方时高亮显示按钮。你可以在图 2-2 中看到效果。

图 2-2。使用事件将类应用于元素
使用事件对象
在前一个例子中,我作为参数传递给hover方法的两个函数基本相同。我可以将这两个函数合并成一个可以处理这两个事件的处理器,如清单 2-9 所示。
清单 2-9。在单个处理函数中处理多个事件
`...
...`
本例中的回调函数接受一个参数e。这个参数是浏览器提供的一个Event对象,为您提供关于您正在处理的事件的信息。我已经使用了Event.type属性来区分我的函数所期望的事件类型。type属性返回一个包含事件名称的字符串。如果事件名是mouseenter,那么我调用addClass方法。如果没有,我调用removeClass方法,该方法的作用是从 jQuery 对象的元素的class属性中删除指定的类,与addClass方法的作用相反。
处理默认动作
为了让程序员的生活更轻松,当特定元素类型的特定事件被触发时,浏览器会自动执行一些操作。这些被称为默认动作,它们意味着你不必为 HTML 文档中的每个事件和元素创建事件处理程序。例如,浏览器将导航到由a元素的href属性指定的 URL,以响应click事件。这是网页导航的基础。
我把href属性设置成#有点作弊。这是定义其动作将由 JavaScript 管理的元素时的常用技术,因为当执行默认动作时,浏览器不会离开当前文档。换句话说,我不必担心默认动作,因为它并没有真正做任何用户会注意到的事情。
当你需要改变元素的行为,而你又不能像使用#作为 URL 那样做一些小技巧的时候,默认动作可能会更重要。清单 2-10 提供了一个演示,我已经将a元素的href属性更改为一个真实的网页。我已经使用了attr方法将a元素的href属性设置为[apress.com](http://apress.com)。通过这种修改,单击元素不再提交表单;它会导航到 Apress 网站。
清单 2-10。管理默认操作
`...
...`
要解决这个问题,需要调用传递给事件处理函数的Event对象上的preventDefault方法。这将禁用事件的默认操作,意味着将只使用事件处理函数中的代码。你可以在清单 2-11 中看到这个方法的使用。
清单 2-11。防止违约行为
`...
...`
对于a元素上的mouseenter和mouseleave事件没有默认动作,所以在这个清单中,我只需要在处理click事件时调用preventDefault方法。当我现在点击元素时,表单被提交,href属性值没有任何影响。
添加动态购物篮数据
您已经看到了如何通过添加和修改元素以及处理事件来改进 web 应用。在本节中,我将进一步演示如何使用这些简单的技术,通过将购物篮阶段显示的信息与产品选择结合起来,创建一个响应更快的奶酪店。我称之为动态购物篮,因为当用户改变单个奶酪产品的数量时,我将更新向他们显示的信息,而不是静态购物篮,当用户使用该 web 应用的未增强版本提交他们的选择时,就会显示该信息。
添加购物篮元素
第一步是向文档中添加我需要的附加元素。我可以使用 HTML 片段和appendTo方法添加元素,但是为了多样化,我将使用另一种技术,称为潜在内容。潜在内容指的是文档中的 HTML 元素,它们使用 CSS 隐藏,使用 JavaScript 显示和管理。那些没有启用 JavaScript 的用户将看不到这些元素,并将获得基本的功能,但是一旦我揭示了这些元素并设置了我的事件处理,那些使用 JavaScript 的用户将获得更丰富、更完美的体验。清单 2-12 展示了向 HTML 文档添加潜在内容。
清单 2-12。在 HTML 文档中添加隐藏元素
`
Gourmet European Cheese
我已经突出显示了清单中的附加元素。它们都被分配给了latent类,该类在styles.css文件中有如下定义:
... .latent { display: none; } ...
在这一章的前面,我向你展示了 jQuery hide方法将 CSS display属性设置为none来对用户隐藏元素,我在设置这个类的时候也遵循了同样的方法。这些元素在文档中,但对用户不可见。
显示潜在的内容
既然潜在元素已经就位,我就可以使用 jQuery 来处理它们了。第一步是向用户展示它们。因为我使用 JavaScript 操作这些元素,所以它们将只对启用了 JavaScript 的用户显示。清单 2-13 展示了对script元素的添加。
清单 2-13。揭示潜在内容
`...
...`
突出显示的语句选择所有属于latent类的元素,然后调用show方法。show方法为每个选中的元素添加了一个style属性,该属性将display属性设置为inline,具有显示元素的效果。这些元素仍然是latent类的成员,但是在style 属性中定义的值覆盖了在style 元素中定义的值,因此这些元素变得可见。
响应用户输入
为了创建一个动态购物篮,我希望能够显示每一项的小计,以及每当用户更改产品数量时的总计。我将处理两个事件来获得我想要的效果。第一个事件是change,当用户输入一个新值,然后将焦点移动到另一个元素时触发。第二个事件是keyup,当用户释放之前按下的一个键时触发。这两件事的结合意味着我可以自信地对新的价值观做出平稳的反应。jQuery 定义了change和keyup方法,我可以像之前使用click方法一样使用它们,但是因为我想以同样的方式处理这两个事件,所以我将使用bind方法,如清单 2-14 所示。
清单 2-14。绑定到 change 和 keyup 事件
`...
...`
bind方法的优点是它允许我使用同一个匿名 JavaScript 函数处理多个事件。为此,我选择了文档中的input元素来获取一个 jQuery 对象,并对其调用了bind方法。bind方法的第一个参数是一个包含要处理的事件名称的字符串,其中事件名称由空格字符分隔。第二个参数是当事件被触发时处理事件的函数。事件处理函数中只有两个语句,但是它们值得一解,因为它们包含了 jQuery、DOM API 和纯 JavaScript 的有趣组合。
提示像这样处理两个事件意味着我的回调函数可能会在不需要的时候被调用。例如,如果用户按下 Tab 键,焦点将转移到下一个元素,并且change和keyup事件都将被触发,即使input元素中的值没有改变。我倾向于接受这种重复,作为确保流畅用户体验的代价。我宁愿我的功能执行得比实际需要的更频繁,不要错过任何用户交互。
计算小计
函数中的第一条语句负责计算input值已更改的奶酪产品的小计。以下是声明:
var subtotal = $(this).val() * priceData[this.name];
用 jQuery 处理事件时,可以使用名为this的变量来引用触发事件的元素。this变量是一个HTMLElement对象,DOM API 用它来表示文档中的元素。有一组由HTMLElement定义的核心属性,其中最重要的在表 2-3 中描述。
补充了核心属性,以适应不同元素类型的独特特征。一个这样的例子是name属性,它返回那些支持它的元素的name属性的值,包括input元素。我已经在this变量上使用了这个属性来获取input元素的名称,这样我就可以用它来从我添加到脚本中的priceData对象中获取一个值:
var subtotal = $(this).val() * **priceData[this.name];**
priceData对象是一个简单的 JavaScript 对象,它有一个对应于每种奶酪的属性,每个属性的值就是奶酪的价格。
this变量也可以用来创建jQuery对象,如下所示:
var subtotal = **$(this)**.val() * priceData[this.name];
通过将一个HTMLElement对象作为参数传递给 jQuery $函数,我创建了一个jQuery对象,它的行为就像我使用 CSS 选择器选择了元素一样。这允许我轻松地将 jQuery 方法应用于来自 DOM API 的对象。在这个语句中,我调用了val方法,该方法返回jQuery对象中第一个元素的value属性的值。
提示我的jQuery对象中只有一个元素,但是 jQuery 方法被设计成可以处理多个元素。当您使用类似于val的方法从元素中读取一些值时,您从选择的第一个元素中获取值,但是当您使用相同的方法设置值(通过将值作为参数传递)时,所有选择的元素都将被修改。
使用this变量,我已经能够获得触发事件的input元素的值以及与之相关的产品价格。然后,我将价格和数量相乘以确定小计,并将其赋给一个名为subtotal的局部变量。
显示小计
处理函数中的第二条语句负责向用户显示小计。这一声明也分为两部分。第一部分选择将用于显示值的元素:
**$(this).siblings("span").children("span").**text(subtotal)
我再次使用this变量创建了一个 jQuery 对象。我调用了siblings方法,该方法返回一个jQuery对象,该对象包含原始jQuery对象中匹配指定 CSS 选择器的元素的任何兄弟元素。该方法返回一个jQuery对象,该对象包含触发事件的input元素旁边的潜在span元素。
我链接了对children方法的调用,该方法返回一个jQuery对象,该对象包含前面的jQuery对象中匹配指定选择器的元素的所有子元素。我以一个包含嵌套的span元素的jQuery对象结束。在这个例子中,我本来可以简化选择器,但是我想演示 jQuery 如何支持在文档中导航元素,以及在一系列方法调用中jQuery对象的内容如何改变。这些变化在表 2-4 中描述。

通过像这样组合方法调用,我能够在元素层次结构中导航以创建一个jQuery对象,该对象精确地包含我想要处理的一个或多个元素,在本例中,是触发事件的元素的兄弟元素的子元素。
语句的第二部分是对text方法的调用,该方法设置jQuery对象中元素的文本内容。在本例中,文本是subtotal变量的值:
$(this).siblings("span").children("span")**.text(subtotal)**
最终结果是,一旦用户更改了所需的数量,奶酪的小计就会更新。
计算整体总数
为了完成这个篮子,我需要在每次小计发生变化时生成一个总计。我在script元素中定义了一个新函数,并在input元素的事件处理函数中添加了对它的调用。清单 2-15 显示了增加的内容。
清单 2-15。计算总体总数
`...
...`
calculateTotal函数中的第一条语句定义了一个局部变量,并初始化为零。我用这个变量来计算各个小计的总和。下一个语句是这个函数中最有趣的一个。语句的第一部分选择一组元素:
... **$('span.subtotal span').not('#total')**.each(function(index, elem) { ...
我首先选择所有的span元素,它们是属于subtotal类的span元素的后代。这是选择小计元素的另一种方式。然后我使用not方法从选择中移除元素。在这种情况下,我删除了id为total的元素。我这样做是因为我使用相同的类和样式定义了 subtotal 和 total 元素,并且我不希望在计算新的总计时包含当前的总计。
选择完项目后,我使用each方法。这个方法为一个jQuery对象中的每个元素调用一次函数。该函数的参数是选择中当前元素的索引和代表 DOM 中元素的HTMLElement对象。
我使用text方法获取每个 subtotal 元素的内容。我通过将HTMLElement对象作为参数传递给$函数来创建一个jQuery对象,就像我在本章前面对this变量所做的一样。
text方法返回一个字符串,所以我使用 JavaScript Number函数创建一个数值,我可以将它添加到运行总数中:
total += **Number**($(elem).text());
最后,我选择了total元素,并使用text方法来显示总计:
$('#total').text("$" + total);
添加此函数的效果是奶酪数量的变化会立即反映在总数和单个小计中。
改变表单目标
通过添加动态购物篮,我将购物篮 web 页面的功能引入了应用的主页面。当启用 JavaScript 的用户提交表单时,将他们发送到购物篮 web 页面是没有意义的,因为它只是复制了他们已经看到的信息。我将更改form元素的目标,以便提交表单直接进入发货页面,完全跳过购物篮页面。清单 2-16 显示了改变目标的语句。
清单 2-16。更改表单元素的目标
`...
...`
至此,新语句的工作原理应该显而易见了。我按类型选择了form元素(因为文档中只有一个这样的元素),并调用attr方法为action属性设置一个新值。提交表单时,用户会被带到 shipping details 页面,完全跳过购物篮页面。你可以在图 2-3 中看到效果。

图 2-3。改变申请流程
正如这个例子所演示的,您可以改变 web 应用的流程以及各个页面的外观和交互性。当然,后端服务需要了解不同类型的用户可以通过 web 应用遵循的各种路径,但这很容易通过一点预先考虑和规划来实现。
了解渐进式改进
我在这一章中展示的技术是基本的,但是非常有效。通过使用 JavaScript 管理 DOM 中的元素并响应事件,我已经能够使示例 web 应用对用户的响应更快,提供关于用户产品选择成本的有用和及时的信息,并简化应用本身的流程。
但是——这很重要——因为这些变化是通过 JavaScript 完成的,所以对于非 JavaScript 用户来说,web 应用的基本性质和结构保持不变。图 2-4 显示了启用和禁用 JavaScript 时的 web 应用主页面。

图 2-4。禁用和启用 JavaScript 时的 web 应用
非 JavaScript 用户体验的版本仍然功能齐全,但使用起来比较笨拙,需要更多步骤才能下订单。
创建一个基本的功能级别,然后有选择地丰富它,这是渐进增强的一个例子。渐进式改进不仅仅是关于 JavaScript 的可用性;它包括基于任何因素的选择性丰富,例如带宽量、浏览器类型,甚至用户的经验水平。然而,在创建 web 应用时,最常见的渐进式改进形式是由用户是否启用了 JavaScript 来驱动的。
提示与渐进增强类似的术语是优雅退化。就我在本书中的目的而言,渐进增强和适度降级是相同的,即 web 应用的核心内容和功能对所有用户都是可用的,而与用户浏览器的功能无关。
如果您不想支持非 JavaScript 浏览器,那么您应该让非 JavaScript 访问者清楚地看到存在问题。最简单的方法是使用noscript和meta元素将浏览器重定向到解释情况的页面,如清单 2-17 所示。
清单 2-17。处理非 JavaScript 用户
`...
<head> <title>CheeseLux</title> <link rel="stylesheet" type="text/css" href="styles.css"/> <script src="jquery-1.7.1.js" type="text/javascript"></script> <script>` ` ... JavaScript code goes here... </script> ** <noscript>** ** <meta http-equiv="refresh" content="0; noscript.html"/>** ** </noscript>** </head> ...`这些元素的组合将用户重定向到一个名为noscript.html的页面,这是一个 HTML 文档,告诉用户我需要 JavaScript(显然,并不依赖于 JavaScript 本身)。你可以在本书附带的源代码下载中找到这一页,并在图 2-5 中看到结果。

图 2-5。在 web 应用中强制执行仅 JavaScript 策略
要求 JavaScript 很诱人,但我建议谨慎;您可能会惊讶于有多少用户不启用 JavaScript 或者根本不能启用。对于大公司的用户来说尤其如此,在大公司中,计算机通常被锁定,普通人群中常见的功能以安全的名义被禁用,遗憾的是,包括浏览器中的 JavaScript。有些 web 应用没有 JavaScript 就没有意义,但是在决定开发它们之前,要仔细考虑你要排除的潜在用户/客户。
注意这是一本关于用 JavaScript 构建 web 应用的书,所以我不打算在接下来的章节中保持渐进式增强。不要认为这是对纯 JavaScript 政策的认可。在我自己的项目中,我尽可能地支持非 JavaScript 用户,即使这需要很多额外的工作。
重温按钮:使用 UI 工具包
在本章的最后,我想向你展示一种不同的方法来获得本章中的一个结果:创建一个视觉上一致的按钮。我之前使用的技术演示了如何操作 DOM 和响应事件来定制元素的外观和行为,这是本章的主要前提。
也就是说,对于专业开发来说,最好不要写可以从好的 JavaScript 库中获得的东西,当我想创建视觉上丰富的元素时,我会使用 UI 工具包。在这一节中,我将向您展示使用 jQuery UI 创建自定义按钮是多么容易,jQuery UI 是由 jQuery 团队开发的,是使用最广泛的 JavaScript UI 工具包之一。
设置 jQuery UI
设置 jQuery UI 是一个多阶段的过程。第一步是创建一个主题,它定义了 jQuery UI 小部件(UI 工具包创建的样式元素的名称)使用的 CSS 样式。要创建主题,请转到[jqueryui.com](http://jqueryui.com),点击主题按钮,展开屏幕左侧的每个部分,并指定您想要的样式。当您进行更改时,屏幕右侧的示例小部件将会更新以反映新的设置。我花了大约五分钟(以及一点点尝试和错误)来创建一个与示例 web 应用外观相匹配的主题。如果您不想创建自己的主题,我已经在本书的源代码下载中包含了我创建的主题。
提示如果您不想创建自定义主题,您可以从图库中选择预定义的样式。如果你不想匹配现有的应用设计,这可能是有用的,尽管一些画廊风格中使用的颜色非常惊人。
完成后,点按“下载主题”按钮。您将看到一个屏幕,允许您选择下载中包含哪些 jQuery UI 组件。如果您深入了解 jQuery UI 的细节,您可以创建一个较小的下载,但是对于本书,请确保选择了所有的组件并单击 download 按钮。您的浏览器将下载一个.zip文件,其中包含 jQuery UI 库、您创建的 CSS 主题和一些支持图片。
设置的第二部分是将以下文件从.zip文件复制到 Node.js 服务器的content目录中:
development-bundle\ui\jquery-ui-1.8.16.custom.jsfile- File
development-bundle\themes\custom-theme\jquery-ui-1.8.16.custom.cssfiledevelopment-bundle\themes\custom-theme\imagesfolder
这些文件的名称包括 jQuery UI 版本号。当我写这篇文章的时候,当前的版本是 1.8.16,但是在这本书出版的时候,你可能会有一个更高的版本。
提示我再次使用 JavaScript 文件的未压缩版本,以使调试更容易。你会在.zip文件的js文件夹中找到最小化版本。
创建 jQuery UI 按钮
既然 jQuery UI 已经设置好了,我可以在 HTML 文档中使用它来创建一个按钮小部件并简化我的代码。清单 2-18 显示了将 jQuery UI 导入文档并创建一个按钮所需的附加内容。
导入 jQuery UI 只是添加一个用于导入 JavaScript 文件的script元素和一个用于导入 CSS 文件的link元素。你不需要明确地引用images目录。
提示注意,导入 jQuery UI JavaScript 文件的script元素出现在导入 jQuery 的元素之后的元素。这种排序很重要,因为 jQuery UI 依赖于 jQuery。
清单 2-18。使用 jQuery UI 创建一个按钮
`
...`
在使用 jQuery UI 时,我不必隐藏input元素并插入一个替代。相反,我使用 jQuery 选择我想要修改的元素并调用button方法,如下所示:
$('#buttonDiv input:submit').button()
通过一次方法调用,jQuery UI 改变标签的外观,并在鼠标悬停在按钮上时处理高亮显示。在这种情况下,我不需要担心处理click事件,因为submit input元素的默认动作是提交表单,这正是我想要发生的。
我使用css方法进行了一次额外的方法调用。该方法使用style属性将 CSS 属性直接应用于所选元素,我已经用它在input元素上设置了font-family属性。jQuery UI 主题系统不太支持处理字体,并且使用单一的字体系列生成小部件。我已经从 Google Fonts ( [www.google.com/webfonts](http://www.google.com/webfonts)和 the excellent League of mobile Type([www.theleagueofmoveabletype.com](http://www.theleagueofmoveabletype.com))中设置了 web 字体,所以我必须覆盖 jQuery UI CSS 样式,以便将我喜欢的字体应用到 button 元素。在图 2-6 中可以看到使用 jQuery UI 创建按钮的结果。如您所见,结果与 web 应用的其余部分一致,但用 JavaScript 创建要简单得多。

图 2-6。用 jQuery UI 创建按钮
像 jQuery UI 这样的工具包只是我前面描述的相同 DOM、CSS 和事件技术的方便包装。理解幕后发生的事情很重要,但是我推荐使用 jQuery UI 或其他好的 UI 库。这些库经过了全面的测试,它们让您不必编写和调试定制代码,让您可以将更多时间花在使您的 web 应用从竞争中脱颖而出的功能上。
总结
正如我在本章开始时提到的,我在这些例子中使用的技术简单、可靠,并且完全适合小型 web 应用。如果应用很小,维护起来不会有任何问题,那么使用这些方法本质上没有任何问题,因为它的行为的每个方面对程序员来说都是显而易见的。
然而,如果你正在读这本书,你想更进一步,创建大型的、复杂的、有许多活动部件的 web 应用。当应用于这样的网络应用时,这些技术会产生一些根本性的问题。潜在的问题是 web 应用的不同方面都混在一起了。应用数据(产品和购物篮)、数据的表示(HTML 元素)以及它们之间的交互(JavaScript 事件和处理函数)分布在整个文档中。这使得很难在不引入错误的情况下添加额外的数据、扩展功能或修复错误。
在接下来的章节中,我将向您展示如何将服务器端开发领域的重型技术应用到 web 应用中。多年来,客户端开发一直是服务器端工作的穷亲戚,但是随着浏览器变得更加强大(以及 web 应用程序员变得更加雄心勃勃),我们再也不能假装客户端不是一个完全成熟的平台。是时候认真对待 web 应用开发了,在接下来的章节中,我将向您展示如何为您的 web 应用创建一个坚实、健壮、可伸缩的基础。
三、添加视图模型
如果你做过任何严肃的桌面或服务器端开发,你会遇到模型-视图-控制器 (MVC)设计模式或者它的衍生模型-视图-视图-模型 (MVVM)。我不打算详细描述这两种模式,只想说这两种模式的核心概念都是将应用的数据、操作和表示分离成独立的组件。
将相同的基本原则应用于 web 应用有很多好处。我不会陷入设计模式和术语中。相反,我将重点演示构建 web 应用的过程,并解释这样做的好处。
重置示例
理解如何应用视图模型以及这样做所带来的好处的最好方法就是简单地去做。要做的第一件事是把应用中除了基础的东西都删掉,这样我就有了一个全新的开始。正如你在清单 3-1 中看到的,除了文档的基本结构,我已经删除了所有内容。
清单 3-1。擦石板
`
Gourmet European Cheese
创建视图模型
下一步是定义一些数据,这将是视图模型的基础。首先,我添加了一个描述奶酪店产品的对象,如清单 3-2 所示。
清单 3-2。向文档添加数据
``
我创建了一个包含奶酪产品详细信息的对象,并将其分配给一个名为cheeseModel的变量。该对象描述了我在第二章中使用的相同产品,并且是我的视图模型的基础,我将在这一章中构建它;现在它是一个简单的数据对象,但是我很快会用它做更多的事情。
提示如果你发现自己盯着闪烁的光标,不知道如何定义你的应用数据,那么我的建议很简单:开始输入。采用视图模型的最大好处之一是它使更改变得更容易,这包括对底层数据结构的更改。如果你做得不对也不要担心,因为你以后总是可以改正的。
采用视图模型库
遵循不编写好的 JavaScript 库中可用内容的原则,我将使用视图模型库将视图模型引入 web 应用。我要用的这个叫做击倒。我喜欢应用结构的 KO 方法,KO 的主要程序员是 Steve Sanderson,他是我的合著者,也是来自 Apress 的Pro ASP.NET MVC一书的作者,是一个全面的好人。要获得 KO,请转到[knockoutjs.com](http://knockoutjs.com)并点击下载链接。从文件列表中选择最新的版本(在我撰写本文时是 2.0.0 ),并将其复制到 Node.js content目录。
提示如果你和 KO 处不好也不用担心。其他结构库是可用的。主要竞争来自于骨干([documentcloud.github.com/backbone](http://documentcloud.github.com/backbone))和 AngularJS ( [angularjs.org](http://angularjs.org))。这些备选库中的实现细节可能有所不同,但基本原理是相同的。
在接下来的小节中,我将把我的视图模型和视图模型库放在一起,以分离示例应用的各个部分。
从视图模型生成内容
首先,我将使用数据在文档中生成元素,以便向用户显示产品。这是对视图模型的简单使用,但它再现了第二章中实现的基本功能,并为本章的其余部分打下了良好的基础。清单 3-3 显示了将 KO 库添加到文档中,并从数据中生成元素。
清单 3-3。从视图模型生成元素
`
Gourmet European Cheese
这个清单中有三组附加内容。第一个是用一个script元素将 KO JavaScript 库导入到文档中。第二个附加项告诉 KO 使用我的视图模型对象:
ko.applyBindings(cheeseModel);
ko对象是 KO 库功能的网关,applyBindings方法将视图模型对象作为参数,顾名思义,使用它来完成文档中定义的绑定;这是第三组新增内容。你可以在图 3-1 中看到这些绑定的结果,我将在接下来的章节中解释它们是如何工作的。

图 3-1。从视图模型创建内容
了解值绑定
绑定的值是视图模型中的属性和 HTML 元素之间的关系。这是现有的最简单的绑定方式。下面是一个具有值绑定的 HTML 元素的示例:
<div class="grouptitle" **data-bind="text: category"**></div>
所有 KO 绑定都是使用data-bind属性定义的。这是一个text绑定的例子,它将 HTML 元素的文本内容设置为指定的视图模型属性的值,在本例中是category属性。
当调用applyBindings方法时,KO 搜索绑定并将适当的数据值插入到文档中,像这样转换元素:
<div class="grouptitle" data-bind="text: category">**French Cheese**</div>
提示我喜欢在将要应用 KO 数据绑定的元素中定义它们,但是有些人不喜欢这种方法。有一个简单的库支持不显眼的 KO 数据绑定,这意味着绑定是在script元素中使用 jQuery 建立的。您可以在[gist.github.com/1006808](https://gist.github.com/1006808)获取代码并查看示例。
我在这个例子中使用的另一个绑定是attr,它将元素属性的值设置为模型中的一个属性。下面是清单中的一个attr绑定示例:
<input **data-bind="attr: {name: id}"** value="0"/>
该绑定指定 KO 应该为name属性插入id属性的值,这在应用绑定时会产生以下结果:
<input data-bind="attr: {name: id}" value="0" **name="camembert"**>
KO 值绑定不支持任何格式或值的组合。事实上,值绑定只是将单个值插入到文档中,这意味着通常需要额外的元素作为值绑定的目标。您可以在清单中的label元素中看到这一点,这里我添加了几个span元素:
<label data-bind="attr: {for: id}" class="cheesename"> ** <span data-bind="text: name"></span>** $(**<span data-bind="text:price"></span>**) </label>
我想插入两个数据值作为label元素的内容,并用一些环绕的字符来表示货币。获得想要的效果的方法很简单,尽管它给 HTML 结构增加了一些复杂性。另一种方法是创建自定义绑定,我会在第四章的中解释。
提示text和attr绑定是最有用的,但是 KO 也支持其他类型的值绑定:visible、html、css和style。我在本章后面使用了visible绑定,在第四章的中使用了css绑定,但是你应该在knockoutjs.com查阅 KO 文档以了解其他的细节。
了解流控制绑定
流控制绑定提供了使用视图模型来控制文档中包含哪些元素的方法。在清单中,我使用了foreach绑定来枚举items视图模型属性。foreach绑定用于视图模型属性,这些属性是数组,并为数组中的每一项复制子元素集:
`<div data-bind="foreach: items">
...
子元素上的值绑定可以引用单个数组项的属性,这就是我能够为input元素上的attr绑定指定id属性的原因:KO 知道正在处理哪个数组项,并从该项插入适当的值。
提示除了foreach绑定之外,KO 还支持if、ifnot和with绑定,这些绑定允许有选择地在文档中包含或排除内容。我将在本章的后面描述if和ifnot绑定,但是你应该在knockoutjs.com查阅 KO 文档以获得完整的细节。
利用视图模型
现在我已经有了应用的基本结构,我可以使用视图模型和 KO 做更多的事情。我将从一些基本特性开始,然后逐步向您展示一些更高级的技术。
向视图模型添加更多产品
视图模型带来的第一个好处是能够更快地进行更改,并且错误更少。这方面最简单的演示就是向奶酪店目录中添加更多的产品。清单 3-4 显示了添加来自其他国家的奶酪所需的更改。
清单 3-4。添加到视图模型
`
Gourmet European Cheese
最大的变化是视图模型本身。我改变了数据对象的结构,使得每个产品类别都是分配给products属性的数组中的一个元素(当然,我添加了两个新类别)。就 HTML 内容而言,我只需添加一个foreach流控制绑定,这样每个类别中包含的元素都是重复的。
提示这些添加的结果是一个又长又细的 HTML 文档。这不是显示数据的理想方式,但正如我在《??》第一章中所说,这是一本关于高级编程的书,而不是一本关于设计的书。有很多方法可以更有效地呈现这些数据,我建议从查看 UI 工具包(如 jQuery UI 或 jQuery Tools)提供的选项卡小部件开始。
创建可观察的数据项
在前面的例子中,我像使用简单的模板引擎一样使用 KO;我从视图模型中获取值,并使用它们来生成一组元素。我喜欢使用模板引擎,因为它们简化了标记,减少了错误。但是当你创建可观察的数据项时,视图模型带来了更大的好处。简而言之,可观察的数据项是视图模型中的一个属性,当它被更新时,会导致所有绑定到该属性的值的 HTML 元素也被更新。清单 3-5 展示了如何创建和使用一个可观察的数据项。
清单 3-5。创建可观察的数据项
`
Gourmet European Cheese
mapProducts函数是一个简单的工具,它允许我对每个单独的奶酪产品应用一个函数。这个函数使用 jQuery each方法,该方法为数组中的每一项执行一个函数。通过使用两次each函数,我可以到达每个类别中奶酪产品的内部数组。
在这个例子中,我已经将每个奶酪产品的price属性转换成一个可观察的数据项,如下所示:
mapProducts(function(item) { item.price = **ko.observable(item.price);** });
ko.observable方法将数据项的初始值作为其参数,并设置将更新传播到文档中的绑定所需的管道。我不必对绑定本身做任何更改;KO 为我处理所有的细节。
剩下的就是创造一个环境,让改变发生。我在文档中添加了一个新按钮,并为click事件定义了一个处理程序,如下所示:
$('#discount').click(function() { mapProducts(function(item) { ** item.price(item.price() - 2);** }); });
当单击按钮时,我使用mapProducts函数来更改视图模型中每个 cheese 对象的 price 属性值。由于这是一个可观察的数据项,新的值将被推送到值绑定,并导致文档被更新。
注意我在修改值时使用的稍微奇怪的语法。最初的 price 属性是一个 JavaScript Number,这意味着我可以像这样更改值:
item.price -= 2;
但是ko.observable方法将属性转换成 JavaScript 函数,以便与一些旧版本的 Internet Explorer 一起工作。这意味着通过调用函数(换句话说,通过调用item.price())读取可观察数据项的值,并通过向函数传递一个参数(换句话说,通过调用item.price(newValue))更新该值。这可能需要一点时间来适应,我仍然会忘记这样做。
图 3-2 显示了可观测数据项的效果。当点击应用折扣按钮时,显示给用户的所有价格都被更新,如图图 3-2 所示。

图 3-2。使用可观测的数据项
可观察数据项的能力和灵活性非常重要;它创建了一个应用,在该应用中,视图模式的更改(无论它们是如何发生的)都会导致文档中的数据绑定立即更新。正如你将在本章的其余部分看到的,当我向示例 web 应用添加更复杂的特性时,我使用了大量可观察的数据项。
创建双向绑定
一个双向绑定是一个form元素和一个可观察数据项之间的双向关系。当视图模型更新时,元素中显示的值也会更新,就像常规的可观察对象一样。此外,改变元素值会导致向其他方向的更新:视图模型中的属性被更新。因此,例如,如果我对一个input元素使用双向绑定,KO 确保当用户输入一个新值时模型被更新。通过使用多个元素和同一个模型属性之间的双向关系,您可以轻松地使复杂的 web 应用保持同步和一致。
为了演示双向绑定,我将向 cheese shop 添加一个特价商品部分。这让我可以从完整的部分中挑选一些产品,应用折扣,理想情况下,将客户的注意力吸引到他们可能不会考虑的产品上。
清单 3-6 包含了对 web 应用的更改,以支持特殊优惠。为了建立双向绑定,我将做另外两件有趣的事情:扩展视图模型和使用 KO 模板生成元素。我将在清单后面的小节中解释这三个变化。
清单 3-6。使用动态绑定创建特别优惠
`
** **
Gourmet European Cheese
**
**`
扩展视图模型
JavaScript 的松散类型和动态特性使其非常适合创建灵活且适应性强的视图模型。我喜欢能够获取初始数据并重塑它,以创建更符合 web 应用需求的东西,在这种情况下,添加对特殊优惠的支持。首先,我向视图模型添加了一个名为specials的property,将其定义为一个对象,该对象与模型的其余部分一样具有category和items属性,但添加了一些有用的内容:
cheeseModel.specials = { category: "Special Offers", **discount: 3,** **ids: ["stilton", "tomme"],** items: [] };
属性discount指定了我希望应用于特价商品的美元折扣,属性ids包含了将成为特价商品的产品 id 数组。
当我第一次定义数组时,它是空的。为了填充数组,我枚举了products数组来查找那些在specials.ids数组中的产品,如下所示:
mapProducts(function(item) { ** if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {** ** item.price -= cheeseModel.specials.discount;** ** cheeseModel.specials.items.push(item);** ** }** item.quantity = ko.observable(0); });
我使用inArray方法来确定迭代中的当前项目是否是将作为特价商品包含的项目之一。inArray方法是另一个 jQuery 工具,如果某项包含在数组中,它将返回该项的索引,如果不包含在数组中,则返回-1。对于我来说,这是一种快速简单的方法,可以查看当前商品是否是我感兴趣的特价商品。
如果某个商品在特价商品列表上是,那么我将price属性的值减少discount的数量,并使用push方法将该商品插入到specials.items数组中。
item.price -= cheeseModel.specials.discount; cheeseModel.specials.items.push(item);
在我遍历了视图模型中的商品之后,specials.item数组包含了一组完整的要打折的商品,在此过程中,我降低了它们的价格。
在这个例子中,我将quantity属性变成了一个可观察的数据项:
item.quantity = ko.observable(0);
这很重要,因为我将为特价商品显示多个input元素:一个元素在原始奶酪类别中,另一个在新的Special Offers类别中,我将在下一节中解释。通过在input元素上使用一个可观察的数据项和双向绑定,我可以很容易地确保输入的奶酪数量得到一致的显示,而不管使用的是哪个input元素。
生成内容
现在剩下的工作就是从视图模型中生成内容。我想为特价商品和普通商品生成相同的元素集,所以我使用了 KO 模板特性,它允许我在文档中的多个点生成相同的元素集。下面是清单中的模板:
<script id="categoryTmpl" type="text/html"> <div class="cheesegroup"> <div class="grouptitle" data-bind="text: category"></div> <div data-bind="foreach: items"> <div class="groupcontent"> <label data-bind="attr: {for: id}" class="cheesename"> <span data-bind="text: name"> </span> $(<span data-bind="text:price"></span>)</label> <input data-bind="attr: {name: id}, value: quantity"/> </div> </div> </div> </script>
模板包含在一个script元素中。type属性被设置为text/html,这阻止浏览器将内容作为 JavaScript 执行。模板中的大多数绑定与我在前面的例子中使用的text和attr绑定相同。对input元素的重要添加如下:
<input data-bind=**"attr: {name: id}, value: quantity"**/>
这个元素的data-bind属性定义了两个绑定,用逗号分隔。第一个是常规的attr绑定,但是第二个是value绑定,这是 KO 定义的双向绑定之一。我不必采取任何行动来使value绑定双向;KO 会自动处理。在这个清单中,我创建了一个到quantity可观察数据项的双向绑定。
我使用template绑定从模板生成内容。当使用模板时,KO 复制它所包含的元素,并将它们作为具有template绑定的元素的子元素插入。文档中有两点我使用了模板,它们略有不同:
`
`当使用template绑定时,name属性指定模板元素的id属性值。如果您只想生成一组元素,那么您可以使用data属性来指定将使用哪个视图模型属性。我使用data来指定清单中的specials属性,这为我的特价产品创建了一个内容部分。
提示您必须记住用引号将模板元素的id括起来。如果不这样做,KO 将会悄悄地失败,而不会从模板生成元素。
如果想为数组中的每一项生成一组元素,可以使用foreach属性。我已经通过指定products数组为常规产品类别完成了这项工作。这样,我可以将模板应用于数组中的每个元素,以一致地生成内容。
提示注意,特价元素被插入到了form元素的外部。特价产品的input元素将具有与常规产品类别中相应的input元素相同的name属性值。通过在form之外插入特价元素,我可以防止在提交表单时向服务器发送重复的条目。
查看结果
既然我已经解释了我为设置双向绑定所做的每一个改变,是时候看看结果了,你可以在图 3-3 中看到。

图 3-3。扩展视图模型、创建动态绑定和使用模板的结果
这很好地展示了使用视图模型可以节省时间和减少错误。我对特价产品应用了 3 美元的折扣,这是通过修改视图模型中的属性price的值来实现的。尽管price属性是不可见的,视图模型和模板的结合确保了在最初生成元素时整个文档中显示正确的价格。(您可以看到两个Stilton列表的价格都是 6 美元,而不是视图模型最初指定的 9 美元。)
双向绑定是这个例子中最有趣和最有用的特性。所有的input元素都与它们对应的quantity属性有双向绑定,并且由于在文档中有两个input元素用于每种特价奶酪,在其中一个元素中输入一个值将会使立即在另一个元素中显示该值;您可以在图中看到Stilton产品发生了这种情况(但这种效果最好通过在浏览器中加载示例来体验)。
因此,只需很少的努力,我就可以增强视图模型,并使用这些增强来保持表单的一致性和响应性,同时为应用添加新的特性。在下一节中,我将在这些增强的基础上创建一个动态篮子,向您展示视图模型带来的其他一些好处。
提示如果您将此表单提交给服务器,订单摘要将显示原始的未打折价格。当然,这是因为我只在浏览器中应用了折扣。在一个实际的应用中,服务器也需要知道特别的优惠,但是我将跳过这一点,因为这本书关注的是客户端开发。
添加动态购物篮
既然我已经解释并演示了如何使用值和双向绑定来检测和传播更改,我可以完成这个示例,这样用户就可以使用第二章中的所有功能。这意味着我需要实现一个动态购物篮,我将在接下来的小节中实现它。
添加小计
使用视图模型,可以快速添加新功能。虽然我需要使用一些额外的 KO 特性,但是添加每一项小计的更改非常简单。首先,我需要增强视图模型。清单 3-7 突出显示了对mapProduct函数的调用中script元素的变化。
清单 3-7。扩展视图模型以支持小计
`...
mapProducts(function(item) {
if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
}
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
});
...`
我已经为subtotal属性创建了所谓的计算可观测数据项。这就像一个常规的可观察项,只是值是由一个函数产生的,该函数作为第一个参数传递给ko.computed方法。第二种方法在函数执行时用作this变量的值;我已经将它设置为item循环变量。
这个特性的好处是 KO 管理所有的依赖项,这样当我的计算出的可观察函数依赖于一个常规的可观察数据项时,对常规项的改变会自动触发计算值的更新。在本章的后面,我将使用这个行为来管理总的总数。
接下来,我需要添加一些绑定到模板的元素,如清单 3-8 所示。
清单 3-8。向模板添加元素以支持小计
``
内部的span元素使用一个text数据绑定来显示我刚才创建的subtotal属性的值。更有趣的是,外层的span元素使用了另一个 KO 绑定;这一个是visible。对于这个绑定,当指定的属性为 false-like ( zero、null、undefined或false)时,子元素被隐藏。对于真值值(1、true或非null对象或数组),显示子元素。我已经为visible绑定指定了subtotal值,这个小技巧意味着只有当用户在input元素中输入非零值时,我才会显示小计。你可以在图 3-4 中看到结果。

图 3-4。选择性显示小计
您可以看到,一旦将基本结构添加到应用中,创建新功能是多么简单快捷。一些新的标记和一点点脚本大有帮助。此外,小计功能可以与特别优惠无缝协作;因为两者都在视图模型上操作,所以应用于特价的折扣无缝地(并且毫不费力地)合并到小计中。
添加购物篮行项目和合计
我不想使用我在第二章中采用的内嵌购物篮方法,因为有些产品会显示两次,而且文档太长,用户无法向下滚动以查看其选择的总成本。相反,我将创建一个单独的购物篮元素集,它将与产品一起显示。你可以在图 3-5 中看到我所做的。

图 3-5。添加单独的购物篮
清单 3-9 显示了支持购物篮所需的变更。
清单 3-9。添加购物篮元素和行项目
`
** **
` ` Gourmet European Cheese
**
**
**
** **
** **
** **
** **
** **
** **
** **
** **
** **
** **
** **
** **
**
| Cheese | Subtotal | |
|---|---|---|
| Total: | $ | |
**
** **
**
****
`
我将逐一介绍我所做的每一类改变,并解释其效果。当我这样做的时候,请思考一下添加这个特性只需要做很少的改变。同样,视图模型和一些基本的应用结构创建了一个基础,可以快速方便地添加新特性。
扩展视图模型
清单中对视图模型的更改是添加了total属性,这是一个计算出来的可观察值,它将各个subtotal值相加:
cheeseModel.total = ko.computed(function() { var total = 0; mapProducts(function(elem) { total += elem.subtotal(); }); return total; });
正如我之前提到的,KO 自动跟踪可观察数据项之间的依赖关系。对subtotal值的任何更改都将导致 total 被重新计算,新值将显示在与其绑定的元素中。
添加购物篮结构和模板
我添加到文档中的 HTML 元素的外部结构只是一个奶酪类别的副本,以保持视觉一致性。篮子的核心是table元素,它包含几个数据绑定:
`
** **
** **
** **
** **
| Cheese | Subtotal | |
|---|---|---|
| Total: | $<span data-bind="text: total"> | |
这里最重要的补充是格式奇怪的 HTML 注释。这就是所谓的无容器绑定,它允许我应用template绑定,而不需要为将要复制的内容提供容器元素。从嵌套数组向表中添加行是这种技术的理想情况,因为添加一个元素以便应用绑定会导致布局问题。无容器绑定包含在常规的foreach绑定中,但是您可以像嵌套常规元素一样嵌套绑定注释。
另一个绑定是一个简单的text值绑定,它使用我刚才创建的计算出的total可观察值显示篮子的总数。我不必采取任何措施来确保总数是最新的;KO 管理视图模型中的total、subtotal和quantity属性之间的依赖链。
我添加来生成table行的模板有四个数据绑定:
<script id="basketRowTmpl" type="text/html"> <tr **data-bind="visible: quantity, attr: {'data-prodId': id}"**> <td **data-bind="text: name"**></td> <td>$<span **data-bind="text: subtotal"**></span></td> <td><a href="#"></a></td> </tr> </script>
您以前见过这些类型的绑定。在tr元素上的visible绑定确保表行只对那些quantity不为零的奶酪可见;这可以防止购物篮中塞满用户不感兴趣的产品。
注意tr元素上的attr绑定。我已经使用 HTML5 data属性特性定义了一个定制属性,该特性将行所代表的产品的id值嵌入到tr元素中。我将很快解释我为什么这样做。
我还移动了submit按钮,使其位于购物篮下方,方便用户提交订单。我分配给篮子元素的样式使用 CSS position属性的fixed值,这意味着篮子总是可见的,即使用户向下滚动页面。为了适应这个篮子,我使用 jQuery 将 CSS width属性的一个新值直接应用到 cheese category 元素(而不是篮子本身):
$('div.cheesegroup').not("#basket").css("width", "50%");
从购物篮中移除项目
最后一组更改基于添加到basketRowTmpl模板中每个表格行的a元素:
$('#basketTable a') .button({icons: {primary: "ui-icon-closethick"}, text: false}) .click(function() { var targetId = $(this).closest('tr').attr("data-prodId"); mapProducts(function(item) { if (item.id == targetId) { item.quantity(0); } }); })
我使用 jQuery 选择所有的a元素,并使用 jQuery UI 从它们创建按钮。jQuery UI 主题包括一组图标,我传递给 jQuery UI button方法的对象创建了一个按钮,该按钮使用这些图像中的一个,并且不显示任何文本。这给了我一个漂亮的小十字按钮。
在click函数中,我使用 jQuery 从触发click事件的a元素导航到使用closest方法的第一个祖先tr元素。这将选择包含自定义data属性的tr元素,该属性是我之前插入到模板中并使用attr方法读取的:
var targetId = $(this).closest('tr').attr("data-prodId");
这个语句让我确定用户想要从购物篮中移除的产品的id。然后我使用mapProducts函数找到匹配的奶酪对象,并将quantity设置为零。由于quantity是一个可观察的数据项,KO 传播新的值,这使得subtotal值被重新计算,并且相应的tr元素上的visible绑定被重新评估。由于quantity为零,表格行将自动隐藏。并且,因为subtotal是可观察的,所以total也将被重新计算,并且新的值被显示给用户。如您所见,拥有一个无缝管理数据值之间依赖关系的视图模型是非常有用的。最终结果是一个动态的篮子,它总是与视图模型中的值一致,因此总是向用户提供正确的信息。
完成示例
在我结束这个话题之前,我想调整一些事情。首先,当用户没有选择任何项目时,购物篮看起来很差,如图图 3-6 所示。为了解决这个问题,我将在购物篮为空时显示一些占位符文本。

图 3-6。空篮子
第二,用户没有办法通过一个单一的动作清空购物篮,所以我将添加一个按钮,将所有产品的数量重置为零。最后,通过将submit按钮移到form元素之外,我已经失去了依赖默认动作的能力。我必须添加一个事件处理程序,以便用户可以提交表单。清单 3-10 显示了我为支持这些特性而添加的 HTML 元素。
清单 3-10。添加元素以完成示例
`...
<body> <div id="logobar"> <img src="cheeselux.png"> <span id="tagline">Gourmet European Cheese</span> </div>**
** No products selected**
**
<table id="basketTable" data-bind="visible: total">
CheeseSubtotal
Total:$
**
**...`
我在包含占位符文本的div元素上使用了ifnot绑定。KO 定义了一对绑定,if和ifnot,它们类似于visible绑定,但是向 DOM 添加和移除元素,而不是简单地隐藏它们。当指定的视图模型属性为 true-like 时,if绑定显示其元素,如果为 false-like,则隐藏它们。ifnot绑定反转;当属性为 true-like 时,它显示其元素。
通过指定与total属性绑定的ifnot,我确保我的占位符元素仅在total为零时显示,这发生在所有subtotal值为零时,这发生在所有quantity值为零时。我再次依赖 KO 的能力来管理可观察数据项之间的依赖关系,以获得我需要的效果。
我希望占位符显示时table元素不可见,所以我使用了visible绑定。
我本可以使用if绑定,但是这样做会导致一个问题。绑定到 total property意味着最初不会显示table,通过绑定if,元素将从 DOM 中移除。这意味着当我试图选择a元素来设置移除按钮时,它们也不会出现。visible绑定将元素留在文档中供 jQuery 查找,但对用户隐藏它们。
您可能想知道为什么我不移动 jQuery 选择,让它在调用ko.applyBindings之前执行。原因是我想用 jQuery 选择的a元素包含在 KO 模板中,该模板在调用applyBindings方法之前不会用来创建元素。没有好的方法可以解决这个问题,因此需要visible绑定。
对 HTML 元素的另一个改变是添加了一个类型为reset的input元素。这个元素在form元素之外,所以我必须处理click事件来从篮子中移除项目。清单 3-11 显示了script元素的相应变化。
清单 3-11。增强脚本以完成示例
`...
`我在清单中只展示了部分脚本,因为改动很小。请注意我是如何使用 jQuery 和普通 JavaScript 来操作视图模型的。我不需要为购物篮占位符添加任何代码,因为它将由 KO 管理。事实上,我需要做的就是扩大 jQuery 的选择范围,这样我就可以为submit和reset input元素创建 jQuery UI 按钮小部件,并添加一个click处理函数。在函数中,我提交表单或将quantity值改为 0,这取决于用户点击的按钮。您可以在图 3-7 中看到篮子的占位符。

图 3-7。当购物篮为空时使用占位符
如果你想知道按钮是如何工作的,你必须在浏览器中加载这些例子。最简单的方法是使用本书附带的源代码下载,在 Apress.com 可以免费获得。
总结
在这一章中,我向你展示了如何拥抱你以前在桌面或服务器端开发中使用过的那种设计哲学,或者至少是对你的项目有意义的那种哲学。
通过向我的 web 应用添加一个视图模型,我能够创建一个更加动态的示例应用版本;它更具可伸缩性,更易于测试和维护,并且使更改和增强变得轻而易举。
您可能已经注意到,结构化 web 应用的形状发生了变化,因此相对于 HTML 标记的数量来说,代码要多得多。这是一件好事,因为它将应用的复杂性放到了您可以更好地理解、测试和修改它的地方。HTML 成为数据的一系列视图或模板,由视图模型通过结构库驱动。我再怎么强调采用这种方法的好处也不为过;它确实为专业级 web 应用奠定了基础,并将使创建、增强和维护您的项目变得更简单、更容易、更愉快。
四、使用 URL 路由
在这一章中,我将向你展示如何在你的 web 应用中添加另一个服务器端的概念:URL 路由。URL 路由背后的想法非常简单:我们将 JavaScript 函数与内部的 ?? URL 联系起来。内部 URL 是相对于当前文档的 URL,包含一个散列片段。事实上,它们通常只表示为散列片段本身,比如#summary。
在正常情况下,当用户点击一个指向内部 URL 的链接时,浏览器会查看文档中是否有一个元素的id属性值与片段相匹配,如果有,就滚动以使该元素可见。
当我们使用 URL 路由时,我们通过执行 JavaScript 函数来响应这些导航变化。这些函数可以显示和隐藏元素,更改视图模型,或者执行应用中可能需要的其他任务。使用这种方法,我们可以为用户提供一种在应用中导航的机制。
当然,我们可以使用事件。问题还是在于规模。对于小型简单的 web 应用来说,处理由元素触发的事件是一种完全可行且可接受的方法。对于更大、更复杂的应用,我们需要更好的东西,URL 路由提供了一种简单、优雅、可伸缩的好方法。当我们使用 URL 作为导航机制时,向 web 应用添加新的功能区域,并为用户提供使用它们的方法,变得非常简单和健壮。
构建一个简单的路由 Web 应用
解释 URL 路由的最好方式是用一个简单的例子。清单 4-1 显示了一个依赖于路由的基本 web 应用。
清单 4-1。一个简单的路由 Web 应用
`
这是一个相对较短的列表,但是有很多内容,所以我将在接下来的部分中分解内容并解释活动的部分。
添加路由库
我将再次使用一个公开可用的库来获得我需要的效果。周围有一些 URL 路由库,但我最喜欢的一个叫做 Crossroads。它简单、可靠、易于使用。它有一个缺点,那就是它依赖于同一作者的另外两个库。我喜欢看到依赖关系被整合到一个单独的库中,但是这并不是一个普遍的偏好,这仅仅意味着我们必须下载一些额外的文件。表 4-1 列出了我们从下载档案中需要的项目和 JavaScript 文件,这些文件应该复制到 Node.js 服务器content目录中。(如果您不想单独下载这些文件,这三个文件都是本书源代码下载的一部分。可在 Apress.com 免费下载。)

我使用script元素将 Crossroads、它的支持库和我的新cheeseutils.js文件添加到 HTML 文档中:
`...
** **
通过对容器元素应用buttonset方法,我能够从子a元素创建一组按钮。我使用了buttonset,而不是button,这样 jQuery UI 将在一个连续的块中设计元素的样式。你可以在图 4-1 中看到这造成的效果。

图 4-1。应用路由的基本应用
由buttonset方法创建的按钮之间没有空间,按钮组的外边缘被很好地圆化了。您还可以在图中看到一个内容元素。这个想法是,点击其中一个按钮将允许用户显示相应的内容项。
应用 URL 路由
我几乎准备好了一切:一组导航控件和一组内容元素。我现在需要将它们连接在一起,这是通过应用 URL 路由来实现的:
``
突出显示的前三条语句设置了 Hasher 库,以便它能与 Crossroads 一起工作。Hasher 通过location.hash browser 对象响应内部 URL 的变化,并在有变化时通知 Crossroads。
Crossroads 检查新的 URL,并将其与给定的每条路线进行比较。使用addRoute方法定义路线。该方法的第一个参数是我们感兴趣的 URL,第二个参数是用户导航到该 URL 时要执行的函数。因此,例如,如果用户导航到#select/Apple,那么将视图模型中的selectedItem可观察值设置为Apple的函数将被执行。
提示我们在使用addRoute方法时不需要指定#字符,因为 Hasher 在通知 Crossroads 发生变化之前会删除它。
在这个例子中,我定义了三条路由,每条路由对应于我在a元素上使用formatAttr绑定创建的一个 URL。
这是 URL 路由的核心。您创建一组驱动 web 应用行为的 URL 路由,然后在文档中创建导航到这些 URL 的元素。图 4-2 显示了示例中这种导航的效果。

图 4-2。浏览示例 web 应用
当用户点击一个按钮时,浏览器导航到由底层a元素的href属性指定的 URL。这种导航变化被路由系统检测到,从而触发对应于该 URL 的功能。该函数更改视图模型中可观察项目的值,并导致用户显示表示所选项目的元素。
需要理解的重要一点是,我们正在使用浏览器的导航机制。当用户单击其中一个导航元素时,浏览器移动到目标 URL 尽管 URL 位于同一文档中,但浏览器的历史记录和 URL 栏会更新,如图所示。
这给 web 应用带来了两个好处。首先是后退按钮的工作方式符合大多数用户的预期。第二,用户可以手动输入 URL 并导航到应用的特定部分。要查看这两种行为的运行情况,请按照以下步骤操作:
Load the list in the browser.
。
Enter
cheeselux.com/#select/Bananain the address bar of the browser.Click the back button of the browser.
当您单击橙色按钮时,橙色项目被选中,并且该按钮被突出显示。当您输入 URL 时,香蕉商品也会发生类似的情况。这是因为应用的导航机制现在由浏览器来协调,这就是我们如何能够使用 URL 路由来分离应用的另一个方面。
在我看来,第一个好处是最有用的。当用户单击后退按钮时,浏览器会导航回上一次访问的 URL。这是一个导航更改,如果以前的 URL 在我们的文档中,新的 URL 将与应用定义的路由集匹配。这是一个将应用状态展开到上一步的机会,在示例应用中,上一步显示橙色按钮。对于用户来说,这是一种更自然的工作方式,特别是与使用常规事件相比,在常规事件中,点击后退按钮往往会导航到用户在应用之前访问的站点。
巩固路线
在前面的例子中,我分别定义了每条路由及其执行的功能。如果这是定义路由的唯一方式,那么复杂的 web 应用将会陷入路由和功能的泥沼,并且与常规事件处理相比没有任何优势。幸运的是,URL 路由非常灵活,我们可以轻松地合并我们的路由。在接下来的部分中,我将描述这方面可用的技术。
使用可变段
清单 4-4 显示了将之前演示的三条路线合并成一条路线是多么容易。
清单 4-4。合并路线
``
URL 的路径部分由段组成。比如 URL 路径select/Apple有两段,分别是select和Apple。当我指定一条路线时,像这样:
/select/Apple
只有当两段完全匹配时,路由才会与 URL 匹配。在清单中,我已经能够通过添加一个变量段来合并我的路线。可变段允许路由匹配具有相应段的任何值的 URL。因此,为了明确起见,简单 web 应用中的所有导航 URL 都将匹配我的新路线:
select/Apple select/Orange select/Banana
第一段仍然是静态的,这意味着只有第一段是select的 URL 才会匹配,但是我为第二段添加了一个通配符。
*这样我就可以适当地响应 URL,变量段的内容作为参数传递给我的函数。我使用这个参数来更改视图模型中可观察的selectedItem的值,这意味着/select/Apple的 URL 会导致如下调用:
viewModel.selectedItem('Apple');
一个 URLselect/Cherry将导致这样一个调用:
viewModel.selectedItem('Cherry');
处理意外的段值
最后一个网址有问题。在我的 web 应用中没有一个名为 Cherry 的项目,将视图模型observable设置为这个值会为用户创建一个奇怪的效果,如图图 4-3 所示。

图 4-3。意外变量段值的结果
URL 路由带来的灵活性也是一个问题。能够导航到应用的特定部分对用户来说是一个有用的工具,但是,对于用户提供输入的所有机会,我们必须防止意外的值。对于我的示例应用,验证变量段值的最简单方法是检查视图模型中数组的内容,如清单 4-5 所示。
清单 4-5。忽略意外的段值
... crossroads.addRoute("select/{item}", function(item) { **if (viewModel.items.indexOf(item) > -1) {** viewModel.selectedItem(item); **}** }); ...
在这个清单中,我选择了阻力最小的方法,即简单地忽略意外值。有许多可供选择的方法。我本可以显示一条错误消息,或者如清单 4-6 所示,接受这个意外的值并将其添加到视图模型中。
清单 4-6。通过将意外值添加到视图模型中来处理它们
``
如果变量 segment 的值不是视图模型中的items数组中的值之一,那么我使用push方法添加新值。我改变了视图模型,所以使用ko.observableArray方法,items数组是一个可观察的项目。一个可观察数组就像一个常规的可观察数据项,除了像foreach这样的绑定会随着数组内容的改变而更新。使用可观察数组意味着添加一个项目会导致 Knockout 在文档中生成内容和导航元素。
这个过程的最后一步是再次调用 jQuery UI buttonset方法。KO 不知道应用于a元素来创建按钮的 jQuery UI 样式,必须重新应用这个方法才能获得正确的效果。在图 4-4 中可以看到导航到#select/Cherry的结果。

图 4-4。将意外的段值合并到应用状态中
使用可选段
可变段的限制是 URL 必须包含一个段值来匹配路由。比如路由select/{item}会匹配任何一个第一段是select的两段式 URL,但是不会匹配select/Apple/Red(因为段太多)或者select(因为段太少)。
我们可以使用可选航段来增加路线的灵活性。清单 4-7 显示了该示例的可选段上的应用。
清单 4-7。使用路线中的可选路段
... crossroads.addRoute(**"select/:item:"**, function(item) { **if (!item) {** **item = "Apple";** } else if (viewModel.items.indexOf(item)== -1) { viewModel.items.push(item); $('div.catSelectors').buttonset(); } viewModel.selectedItem(item); }); ...
为了创建一个可选的段,我简单地用冒号替换括号字符,这样{item}就变成了:item:。通过这一改变,路由将匹配具有一个或两个段并且第一个段是select的 URL。如果没有第二段,那么传递给函数的参数将为 null。在我的清单中,如果是这种情况,我默认使用Apple值。一条路线可以包含任意多的静态、变量和可选航段。在这个例子中,我将保持我的路线简单,但是您可以创建几乎任何您需要的组合。
添加默认路线
随着可选段的引入,我的路由将匹配一段和两段 URL。我想添加的最后一个路由是一个默认路由,它是一个当 URL 中根本没有段时将被调用的路由。这是完成对后退按钮的支持所必需的。要查看我正在解决的问题,请将清单加载到浏览器中,单击其中一个导航元素,然后单击 Back 按钮。你可以在图 4-5 中看到效果——或者说,没有效果。

图 4-5。导航回应用起始点
单击“后退”按钮时,应用不会重置为其原始状态。只有当点击 Back 按钮将浏览器带回到 web 应用的基本 URL(在我的例子中是[cheeselux.com](http://cheeselux.com))时,才会发生这种情况。什么都不会发生,因为基本 URL 与应用定义的路由不匹配。清单 4-8 显示了增加一条新的路线来解决这个问题。
清单 4-8。为基本 URL 添加路由
`...
...`
此路由不包含任何类型的段,只匹配基本 URL。现在,单击 Back 按钮直到到达基本 URL 会使应用返回到其初始状态。(嗯,它回到了它的初始状态;在这一章的后面,我将解释这种方法中的一个小问题,并告诉你如何改进它。)
使事件驱动控件适应导航
并不总是能够限制文档中的元素,使得所有的导航都可以通过a元素来处理。当向路由的应用添加 JavaScript 事件时,我遵循一个简单的模式,它在 URL 路由和常规事件之间架起了一座桥梁,给了我许多路由的好处,也让我可以使用其他类型的元素。清单 4-9 展示了这种应用于其他元素类型的模式。
清单 4-9。URL 路由和 JavaScript 事件之间的桥接
`...
...`
这里的技术是向元素添加一个data-url属性,这些元素的事件将导致导航的改变。我使用 jQuery 来处理具有data-url属性的元素的change和click事件。处理这两个事件让我能够迎合不同种类的input元素。我使用了live方法,这是一个简洁的 jQuery 特性,它依靠事件传播来确保在脚本执行后为添加到文档中的元素处理事件;当文档中的元素集可以根据视图模型的变化而改变时,这是非常重要的。这种方法允许我使用这样的元素:
`...
该标记为视图模型items数组中的每个元素生成一组单选按钮。我用我的自定义formatAttr数据绑定为data-url属性创建值,我在前面已经描述过了。select元素需要一些特殊的处理,因为当select元素触发change事件时,关于哪个值被选中的信息是从子option元素获得的。下面是创建一个使用该模式的select元素的一些标记:
`...
目标 URL 的一部分在select元素的data-url属性中,其余部分取自option元素的value属性。包括select在内的一些元素会同时触发click和change事件,所以在使用location.replace触发导航更改之前,我会检查目标 URL 是否不同于当前 URL。清单 4-10 显示了这种技术如何应用到select元素、按钮、单选按钮和复选框中。
清单 4-10。事件之间的桥接和不同类型元素的路由
`
**
**


浙公网安备 33010602011771号