富互联网应用,开箱即用—为用户而写
“顾客永远是对的。” 这句老生常谈——归功于著名的英国塞尔福里奇百货商店的创始人哈里·塞尔福里奇,或芝加哥以他的名字命名的百货商店马歇尔·菲尔德——已经被无休止地讨论和剖析。毫无疑问,我们每个人都能举出很多顾客不对的情况,并且那样对待他们是没有意义的。然而,真正的事实是,如果您想销售(或开发)对顾客有用的东西,您必须按照他们实际工作的方式来构建它,而不是您希望他们工作的方式。
在 Web 的早期,我们都被无需安装任何东西,只需一个浏览器就能访问任何应用程序的能力所吸引。开发者喜欢用一种通用的语言编写代码的想法。更妙的是,HTML 是声明式的——没有有趣的组件和回调,没有特定平台或特定操作系统版本的怪癖(或多或少)。用户喜欢简单的书本范例。您可以后退和前进(不出所料,这就是按钮的名称),甚至可以点击重新加载。语义很简单;为平台编写代码很容易,而且与管理每台桌面电脑相比,部署感觉像是新的启蒙运动。
当然,缺点是显而易见的,但也是可以接受的代价。如果每个页面都是静态生成的,只包含 HTML,那么每次更改,无论多么小——比如文本更改或添加警告——都需要完全重新加载页面。除了给用户带来麻烦之外,它既不自然又缓慢。20 世纪 90 年代的一些权威人士认为,正是由于这个原因,Web 永远不会成为主流平台。基于 JavaScript 的动态 HTML 允许 DOM 操作,给了我们一些回旋余地,但是任何来自服务器的东西——真实数据——都需要重新加载。
在 2000 年代初期至中期,开发者开始探索如何在不重新加载页面的情况下与服务器通信。微软在 1999 年推出了 XMLHTTP ActiveX 控件,后来被所有其他浏览器采用。2005 年,Adaptive Path 的联合创始人 Jesse Garrett 创造了术语 Asynchronous JavaScript with XML,即 AJAX。尽管 Jesse 并没有发明它,但他无疑普及了它,这再次强调了我们工程师往往会忽视的营销的重要性。有趣的是,最早为人所知的 AJAX 用法之一出现在……1596 年,由约翰·哈灵顿爵士用来描述他的新发明:抽水马桶。
AJAX 非常棒。我们可以从服务器获取我们想要的东西,而无需重新加载整个网页。我们可以在后台处理它。我们可以根据需要获取尽可能少或尽可能多的内容。Web 应用程序,现在称为富互联网应用程序,似乎终于在部署的简易性和性能方面完全可以与桌面应用程序相媲美。它促成了 Google 地图等无处不在的应用程序的出现,如果没有 AJAX,这些应用程序是不可能实现的。
AJAX 应用程序的最大问题是它们破坏了 Web 语义。刷新、后退和前进按钮完全基于浏览器 URL 栏中的地址工作。在静态页面的时代,这在很大程度上表明了您所在的位置:http://example.com/store?product=12345 肯定与 http://example.com/store?product=99999 不同。
然而,在现代 RIA AJAX 世界中,URL 是 http://example.com/store。由于产品是使用 AJAX 渲染的,URL 没有改变,重新加载极不可能将您带回您所在的位置。
最初的反应是在服务器上添加复杂的状态。JavaEE、PHP 框架和其他框架都添加了会话变量,您可以在其中存储大量关于用户上次请求的信息,这样您就可以大致尝试为下一次请求重建它。整个 JavaServer Faces (JSF) 框架都是围绕这种复杂的状态语义构建的。这些方法或多或少地完成了工作,但它们非常复杂,需要付出很多努力才能使用。
接下来的尝试基本上是说,“我们不支持浏览器按钮!” 换句话说,“我们和技术是对的,而用户是错的。” 任何曾经做过生意的人都知道,这种策略注定要失败。如果您的客户没有其他选择,它可能会在短时间内奏效,但被告知他们错了并且“只是不明白”的客户很快就会寻找替代品。硅谷到处都是抱怨“我们的客户只是不明白”的创业公司的尸体。当然,是创业公司(和工程师)没有明白。
那么,我们需要的是一种使用 AJAX 应用程序并修改 URL 栏的方法,使其不会重新加载页面,但仍然相当完整地指示我们所在的位置。这样,后退和前进,更不用说刷新,就可以正常工作了。
魔力在于一个小小的字符,井号 (#)。在 HTML 规范 RFC 1866 中,您可以为锚点命名,如下所示
<a name="myname"/>
如果您这样做,浏览器应该能够通过在 URL 后附加 # 和锚点名称来转到页面上命名的部分。例如,如果您有一个名为 mypage.html 的 HTML 页面
<html> <head> <!-- lots of stuff --> </head> <body> <div>Lots of content</div> <a name="part2"/> <div>Even more content</div> </body> </html>
要转到上面的页面,您应该访问 http://example.com/mypage.html。但是,如果您想转到该页面并直接转到 part2,您应该访问 http://example.com/mypage.html#part2。
最有趣的部分是,如果浏览器已经在 mypage.html 上,而您访问 mypage.html#part2,浏览器应该并且将会直接转到 part2,而无需重新加载网页。更进一步,如果浏览器找不到名为 part2 的锚点,它将静默且优雅地失败。最后但并非最不重要的是,JavaScript 事件可以捕获此更改并进行处理。
有了以上这些,我们就拥有了一个系统的雏形,该系统使用 AJAX 实现富互联网应用程序的动态性,但可以更改 URL 以指示我们所在的位置,从而与用户合作而不是对抗用户。事实上,如果您使用 Gmail 并仔细观察,您会发现它正是这样工作的。
当然,记住管理 URL 可能很困难,并且会改变您的工作方式。难道没有人开发一个框架来管理所有这些吗?
Sammy 是 Aaron Quint 开发的一个出色的 Web 框架。它不仅为管理 URL 提供了框架,还提供了许多额外的功能,而且实际上还极大地改进了您编写客户端应用程序的方式。您从程序驱动转向声明式。您回到了早期 Web 1.0 时代的易用性,当时 URL 明确定义了您所在的位置,但又没有放弃 AJAX 的动态性。URL 再次成为应用程序中位置的声明者,您可以充分利用它的力量。
让我们探索一个基本的 Sammy 应用程序。为了我们的目的,让我们使用一个联系人应用程序。为了简单起见,本文我们不做任何数据更新,尽管 Sammy 的语义完全支持它。让我们坚持简单的 GET 请求。在联系人应用程序中,我们有十个联系人,每个联系人的 ID 为 1 到 10(很复杂!),每个联系人都有名字、姓氏和电子邮件属性。我们的应用程序视图有一个左侧窗格,其中列出了联系人,以及一个右侧窗格,其中显示了联系人详细信息。请记住,我们希望这是一个富互联网应用程序,全部在一个页面中运行。
警告:本文中的代码可能不完整。如果您想下载并运行它,请从 Web 上获取示例应用程序(请参阅资源)。
首先,让我们定义我们的单个 HTML 页面 contacts.html
<html> <head> <script type="text/javascript" charset="utf-8" ↪src="jquery.min.js"></script> <script type="text/javascript" charset="utf-8" ↪src="sammy.min.js"></script> <script type="text/javascript" charset="utf-8" ↪src="contactapp.js"></script> <style type="text/css"> #list {float: left; width: 48%;} #details {float: left; width: 48%;} </style> </head> <body> <h2>Contact Application</h2> <p>Click on a contact to view the details</p> <div id="list"> <table></table> </div> <div id="details"> <table> <tr><td>First Name:</td><td id="firstName"></td></tr> <tr><td>Last Name:</td><td id="lastName"></td></tr> <tr><td>Email:</td><td id="email"></td></tr> </table> </div> </body> </html>
请注意以下几个要素
安装:我们包含了 jQuery,Sammy 的先决条件(以及一个非常棒的库)。
安装:我们在 jQuery 之后包含了 Sammy。
HTML:页面非常简单。有两个空白 div,一个 ID 为 list,另一个 ID 为 details。它们是浮动的。
接下来,我们需要声明应用程序可以存在的所有状态。这些将决定我们想要的路径。在我们的联系人应用程序中,我们实际上只有两种状态:1) 列出联系人,以及 2) 查看一个特定联系人(同时主列表保持打开状态)。
为了与 RESTful 风格保持一致,让我们将我们的 URL 声明如下
1) 列出联系人contacts.html#/contacts.
2) 查看一个特定联系人contacts.html#/contacts/:id(其中 :id 被替换为被查看联系人的 ID)。
此外,我们想要一个默认路径。如果用户只是打开 contacts.html 会发生什么?
3) 默认路径contacts.html,重定向到contacts.html#/contacts.
请注意一些有趣的事情。我们正在定义各种声明式路径。当遇到这些路径中的每一个时,我们都希望采取一定的行动。本质上,这些是路由。大多数基于 Ruby 的框架(Sinatra、Rails、Merb/Rails3 等)都使用这种精确的语言,Sammy 也是如此。
因此,我们有三个路由及其操作
contacts.html→重定向到contacts.html#/contacts.
contacts.html#/contacts→列出联系人。
contacts.html#/contacts/:id→显示详细信息联系人 :id.
在我们包含的 JavaScript 文件 contactapp.js 中,我们声明每个路由
var app = $.sammy(function(){ // for the verb GET with the path #/, go to #/contacts this.get("#/",function(context){ this.redirect("#/contacts"); }); // for the verb GET with the path #/contacts, render the contacts this.get("#/contacts",function(context){ // get our contact list from the server $.get("/contacts",function(res,status) { // render the results - should include // status-checking for safety // jQuery already parsed the response to JSON for us var list = res, tr, td, table = $("#list table"), a; // clear the existing list table.empty(); // use jQuery to go through each result $.each(list,function(i,elm) { tr = $("<tr></tr>").appendTo(table); td = $("<td></td>").appendTo(tr); // the key part: make it a URL a = $("<a></a>").attr("href","#/contacts/"+elm.id).text ↪(elm.lastName + " " + elm.firstName).appendTo(td); }); },"json"); // hide the details $("#details table").hide(); }); // for the verb GET with a specific path #/contacts/:id, // render that one contact this.get("#/contacts/:id",function(context){ // get our contact list from the server - access // param :id as this.params.id $.get("/contacts/"+this.params.id,function(res,status) { // render the results - should include // status-checking for safety // jQuery already parsed the response to JSON for us var contact = res, table = $("#details table"); // find the elements in the table, and fill them with the data $("#firstName",table).text(contact.firstName); $("#lastName",table).text(contact.lastName); $("#email",table).text(contact.email); // make sure the table is shown table.show(); },"json"); }); }); // set up a default route for contacts.html $(function(){ app.run("#/"); });
请注意以下几个关键要素
这里根本没有事件处理程序。尽管我们可能需要一些用于编辑按钮或按键之类的事件处理程序,但应用程序中的导航实际上是使用 URL <a> 链接发生的。这使得管理应用程序并了解每个更改的作用变得非常容易。点击列表中的联系人就是点击 URL。我们只是碰巧使用该 URL 来控制我们的应用程序。
我们也可以使用处理程序。我们可以不使用 <a> 链接,而是使用处理程序$("list td").click(function(e),{...});.
这个应用程序非常简短且易于理解。这就是 Sammy 的魅力。
浏览器 URL 更改,但页面不会重新加载。我们仍然处于富互联网应用程序的世界中,但浏览器语义可以正常工作:后退、前进、重新加载。试试看!
完整的示例,不包含精简的 JS,可以在线获取(请参阅资源)。
Sammy 使我们能够同时提供富互联网应用程序,与用户的思维模式合作而不是对抗,并使用声明式路由来编程我们的应用程序,从而使构建更丰富的互联网应用程序变得更加简单。
Sammy 库在 MIT 许可证下开源,并且可以在线获取(请参阅资源)。
资源
jQuery: jquery.com
Ruby on Rails: rubyonrails.org
Sinatra for Ruby: www.sinatrarb.com
Sammy: code.quirkey.com/sammy
本文示例: jsorm.com/doc/samples/contacts/contacts.html
RFC 1866: www.rfc-editor.org/rfc/rfc1866.txt
Douglas Crockford 的 JavaScript 站点: www.crockford.com
Avi Deitcher 是一位常驻纽约和以色列的运营和技术顾问,自 Z80 和 Apple II 时代以来一直从事技术工作。他拥有哥伦比亚大学电气工程学士学位和杜克大学 MBA 学位。可以通过 avi@atomicinc.com 与他联系。