-- 作者:hgpimac
-- 发布时间:3/23/2007 6:01:00 PM
-- [转载]五个很酷的 Ajax 小部件
Ajax 和 XML: 五个很酷的 Ajax 小部件 使用 Ajax 和 XML 以及新的图形工具增强您的站点 级别: 中级 Jack D Herrington (jherr@pobox.com), 高级软件工程师, Leverage Software Inc. 2007 年 2 月 16 日 随着 Web 2.0 浪潮的到来,用户体验得到了全新的关注。用户体验的一部分就是以新颖的方式与用户交互以及为用户提供信息。这些新的界面常常被称作小部件,它们使用 Asynchronous JavaScript + XML (Ajax) 与服务器通信。本文介绍了五个可用于增强站点交互性的小部件。 Web 2.0 强调以独特、新颖的方式与网站的客户交互。其中很多创新技术都使用图形和小部件,它们与服务器进行通信,获取用于显示的数据。在本文中,我将介绍五个这样的小部件 —— 有些是开源的,有些是需要许可的 —— 它们通过 Ajax 和 XML 与服务器通信。 请访问 Ajax 技术资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。 * carousel: 这个小部件是一个滚动的图像浏览器,客户可以通过滚动查看一系列的项目,每个项目用一个小图形表示。当用户单击一个项目时,进行什么处理可以由您来决定。 carousel 在实际情况中的例子有 Flikr 站点和 Apple 的 iTunes 界面。carousel 是免费提供的,它基于流行的 jQuery JavaScript 框架。 * SWF/Charts:这种基于 Adobe Flash 的控件从服务器上的 XML 中读取图表数据和样式选项,然后根据数据显示一个图表。它的界面非常优雅,由于很容易创建 XML 数据,所以很容易将动态图形添加到页面中。 * SWF/Gauge: 与 SWF/Charts 类似,这个 Flash 小部件使用服务器上的 XML 来构建一个完全可定制的仪表盘显示屏。其外观可以仿制飞机或汽车上的仪表盘,或者更流行的样式。这可完全由您选择。 * 就地编辑: 严格来说它不能算是个小部件,而是从用户那里获得信息的一种直观的、交互式的、轻量级的方式。这种功能是 Scriptaculous 框架附带的,位于 prototype.js 库之上。 * DHTML windows: DHTML window 为在页面内容上放置无模式的悬浮窗提供了一种机制。用户可以移动窗口,调整它的大小,或者使之消失。窗口的内容可以由页面上的 JavaScript 指定,也可以通过 Ajax 从服务器上读取。这种类型的窗口非常适合用作一种报警机制,也很适合用于弹出小的窗体,从而避免重新装载整个页面。 我将首先展示 SWF/Charts 小部件,因为我认为它是最容易部署的小部件之一。相对于所花费的精力,它的回报也是最大的。 SWF/Charts 小部件 俗话说:“一画抵千言”。这句话很难反驳,尤其是在谈论图形的时候。然而一直以来,在 Web 上画图并非易事。虽然有些 Web 框架包括了一些用于构建图像的基本图形,但大多数 Web 框架都缺少即开即用的画图工具。这种功能的缺失使您必须自己来构造图形。 如果有一个小部件能将 XML 编码的数据画出来,岂不是很好?事实上就有这么一个小部件:SWF/Charts。为了开始使用这个小部件,我从网站下载了 SWF 文件,另外还下载了这个小部件所使用的其他一些 SWF 文件。然后,我将这些文件安装在我的站点上,并在 HTML 上添加了一个到 SWF 小部件的链接,如 清单 1 所示。 清单 1. Chart_page.html <html><body> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub.../swflash.cab#version=6,0,0,0" width="400" height="250"> <param name="movie" value="charts.swf?xml_source=chart_data.xml&library_path=charts_library"> <embed src="charts.swf?xml_source=chart_data.xml&library_path=charts_library" width="400" height="250" type="application/x-shockwave-flash" pluginspace="http://www.macromedia.com/go/getflashplayer"> </embed> </object> </body></html> Charts.swf 有两个参数:一个是其库目录的位置,还有一个是 XML 数据的 URL。XML 数据格式相当简单。清单 2 显示了一个简单的例子。 清单 2. Chart_data.xml <chart> <chart_type>bar</chart_type> <chart_data> <row> <null/> <string>2005</string> <string>2006</string> </row> <row> <string>Projected</string> <number>500</number> <number>700</number> </row> <row> <string>Actual</string> <number>600</number> <number>900</number> </row> </chart_data> </chart> 这个文件基本上都是用于图表的数据,还有一些可选的视觉信息。在这个例子中,我将图表的类型指定为条形图。我下载的 SWF 文件所在的那个站点上有关于可以设置的选项以及可用的图形类型的更多信息。 当在 Firefox 浏览器中浏览到这个文件时,可以看到如 图 1 显示的图形。 图 1. 使用中的 Chart 小部件 使用中的 Chart 小部件 可以看到,这个图表的默认颜色模式和外观确实很整洁。这个图恰到好处地对轴线值进行了均匀布置。整体效果非常好,而我为之付出的精力并不多。 显然,可以用一个动态 Web 页面替换 graph_data.xml 文件:只要返回的数据具有正确的格式,图形控件关系不大。本文中的所有例子都是这样的。实际上,可以在一个 Web 浏览器中运行本地文件上的所有例子,而不必使用 Web 服务器(比如 Apache Tomcat 或 IBM® WebSphere® Application Server)或 Web 编程语言(比如 PHP、Microsoft® ASP.NET、Java™ 2 Enterprise Edition [Java EE])。 回页首 SWF/Gauge 小部件 显示数据的另一种极具吸引力的方式就是使用仪表盘小部件。个人而言,我不是很欣赏这种做法,因为即使是显示一点点信息都要占用大量的空间。但仪表盘是高管仪表板的一个关键特性,所以若能快速创建仪表盘,将会十分方便。 但是,如果 Web 连简单的柱形图都处理不好的话,那么当然也就不能处理圆形的仪表盘了。所以,我又将目光投向创建 XML/Graph 的同一家公司。他们也有一个针对仪表盘的解决方案:XML/Gauge。 我将从嵌入了 SWF/Gauge 小部件的 HTML 开始,如 清单 3 所示。 清单 3. Gauge_page.html <html><body> <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/.../swflash.cab#version=6,0,0,0" width="110" height="55"> <param name=movie VALUE="gauge.swf?xml_source=gauge_data.xml"> <embed src="gauge.swf?xml_source=gauge_data.xml" width="110" height="55" type="application/x-shockwave-flash" pluginspace="http://www.macromedia.com/go/getflashplayer"> </embed></object> </body></html> gauge.swf 带有一个参数:数据的位置。在这个例子中,这个位置为 gauge_data.xml,如 清单 4 所示。 清单 4. Gauge_data.xml <gauge> <circle fill_color="888888" start="275" fill_alpha="100" line_color="555555" line_thickness="3" line_alpha="90" radius="50" x="55" end="445" y="55"/> <circle fill_color="99bbff" start="280" fill_alpha="90" line_thickness="4" line_alpha="20" radius="45" x="55" end="440" y="55"/> <circle fill_color="666666" start="317" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="322" y="55"/> <circle fill_color="666666" start="337" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="342" y="55"/> <circle fill_color="666666" start="357" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="362" y="55"/> <circle fill_color="666666" start="377" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="382" y="55"/> <circle fill_color="666666" start="397" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="402" y="55"/> <circle fill_color="666666" start="417" fill_alpha="100" line_color="333333" line_alpha="0" radius="44" x="55" end="422" y="55"/> <circle fill_color="99bbff" start="280" fill_alpha="100" radius="40" x="55" end="440" y="55"/> <circle fill_color="FF4400" start="280" fill_alpha="100" radius="44" x="55" end="310" y="55"/> <circle fill_color="44FF00" start="50" fill_alpha="100" radius="44" x="55" end="80" y="55"/> <circle fill_color="99bbff" start="280" fill_alpha="80" radius="40" x="55" end="440" y="55"/> <circle fill_color="333333" start="270" fill_alpha="100" line_alpha="0" radius="20" x="55" end="450" y="55"/> <rotate start="280" shake_span="2" shadow_alpha="15" step="1" x="55" span="0" y="55" shake_frequency="20"> <rect fill_color="ffff00" fill_alpha="90" line_alpha="0" height="40" x="53" width="4" y="13"/> </rotate> <circle fill_color="111111" start="270" fill_alpha="100" line_thickness="5" line_alpha="50" radius="15" x="55" end="450" y="55"/> </gauge> 可以看到,SWF 对这个小部件采取了一种不同的处理方法。实际上,我是用圆、弧和矩形之类的基本图形构建仪表盘的,而没有通过为仪表盘(或图形)指定数据的方式。 坦白地说,我更喜欢一组预先设置好的仪表盘,这样一来,我所要做的只是用它们提供数据。但是,这里的这种方法仍然有效,它给予了我无限的调整空间 —— 尽管我更喜欢预先设置好的仪表盘以便在此基础上展开工作。 当在浏览器中访问那个页面时,可以看到如 图 2 所示的仪表盘。 图 2. 使用中的仪表盘小部件 使用中的仪表盘小部件 您可能会想,由于局限于一些基本图形,这种小部件并不能带来更好的效果。事实并非如此。基本图形也包括一些简单的动画技术,所以仍然有可发挥的空间,例如可以创建热点链接区,用户可以通过单击这些链接进行导航。此外,还可以想得更远一些,不仅可以将它用于仪表盘,而且还可以使用其简单的图形原语构建任何类型的图像和简单的动画。 回页首 就地编辑 在桌面应用程序中,就地编辑功能并不鲜见,但是到目前为止,这个功能在 Web 上还不多见。在 Web 2.0 中,交互性变得非常重要,所以像就地编辑之类的技术也更加普遍起来。 为了实现就地编辑,可以自己编写代码,也可以使用一个 JavaScript 框架来处理大部分事情。最流行的一个工具包就是 Scriptaculous 框架,它构建在 prototype.js 库之上。Scriptaculous 库使得构建就地编辑控件非常容易。 清单 5 显示了就地编辑的一个简单的 HTML 测试文件。 清单 5. Inplace.html <html><head> <script src="prototype.js"></script> <script src="effects.js"></script> <script src="controls.js"></script> <script src="scriptaculous.js"></script> </head><body> <table width="100%"> <tr><th width="10%">Name</th> <td width="90%"><p id="name">Candy bar</p></td> </tr></table> <script> new Ajax.InPlaceEditor('name', 'submitted.html' ); </script> </body> </html> 首先,Inplace.html 包括所有必需的 JavaScript 源文件。然后,我插入了一个简单的表,其中有一个包含可就地编辑的 数据的段落。在文件的最后,我插入了一小段脚本,用于为那个段落创建 InPlaceEditor 对象。 InPlaceEditor 构造函数带有两个参数:一个参数是段落的 ID,另一参数是用于在我完成编辑之后处理提交的那个页面的 URL。在这个例子中,该页面为 submitted.html;但实际上,它也可能是一个 ASP.NET、Java EE 或 PHP 页面,或者是使用其他一些动态 Web 技术的页面。 清单 6 显示了简单的 submitted.html 文件。 清单 6. Submitted.html <p>Name changed!</p> 现在来做一下测试。首先用浏览器打开这个 HTML 文件。一开始可以看到初始的文本。当把鼠标放在文本上时,它变成黄色,如 图 3 所示。 图 3. 就地编辑的初态 就地编辑的初态 黄色意味着用户可以单击该字段,然后对其进行编辑。于是,我单击该字段,随后得到了一个 Name 字段、一个 ok 按钮和一个 cancel 链接,如 图 4 所示。 图 4. 单击之后编辑文本 单击之后编辑文本 然后,我改变文本,并单击 ok,这样就会将数据发送到服务器(或者 submitted.html 页面,就像这个例子一样)。然后,服务器返回替换了初始文本的 HTML 页面。在这个例子中,我发回了 Name changed! (如 图 5 所示);在实际应用中,它更可能是数据的新值。 图 5. 单击 ok 后的新内容 单击 ok 后的新内容 这些简单的界面升级就可以使应用程序的可用性大大提高。等待页面装载(尤其是从较慢的服务器上装载)会让人觉得界面太土、太陈旧。使用就地编辑器之类的简单工具就可让应用程序光鲜起来,而实现起来并不复杂。 回页首 DHTML 窗口 浏览器使在 Web 页面上构建模式窗口变得更困难,这也许是件好事。但是有时候,小窗口也不错。它们便于显示警告,或者弹出小型表单。它们也是弹出广告的一种很好的方式,这些烦人的广告总是盖住了页面的内容。哦,等等:最后一句收回。 不管怎样,为动态 HTML(DHTML)页面构建窗口并不容易。所以,当我发现这个基于流行的 Protoype.js 库的极其健壮的窗口包时,感到非常高兴。它不仅容易使用,界面还可以换肤,而且在每个浏览器上都运行得很好。清单 7 显示了 window.html 页。 清单 7. Window.html <html> <head> <link href="default.css" rel="stylesheet" type="text/css" /> <script src="prototype.js"></script> <script src="window.js"></script> </head> <body> <script> var win = new Window( 'myPopup', { title: "Terms and Conditions", top:70, left:100, width:300, height:200, resizable: true, url: "terms.html", showEffectOptions: { duration: 3 } } ); win.show(); </script> </body> </html> 我首先将 prototype.js 和 window.js 源文件放进头部。然后,用我喜欢的参数构建弹出对象,包括大小、位置、标题和小部件从中获取显示内容的页面的 URL。通过 Ajax 从一个页面装载内容仅仅是获取内容的一种方法。您也可以通过 JavaScript 代码动态地设置它们,或者用页面上已有的 <div> 标记围住窗口。 在这个例子中,我引用了在 清单 8 中显示的 terms.html 文件。 清单 8. Terms.html <html><body bgcolor="white"> <h1>Terms and Conditions</h1> <p>In order to use this site you must comply with the following conditions...</p> </body></html> 当在浏览器中打开该页面时,会看到如 图 6 所示的窗口。 图 6. 初始窗口 初始窗口 不,那不是两个层叠的 Mac 窗口。其中一个是真正的 Firefox 浏览器窗口,另一个是 Mac 风格的假 DHTML 窗口。只不过彼此看起来很相似。 我可以放大和移动窗口,如 图 7 所示。 图 7. 移动和缩放之后的窗口 移动和缩放之后的窗口 为了写作本文,同时也为了工作之用,我对几个 DHTML 窗口库进行过一些考察,我可以肯定地告诉您,这一个是我感觉最棒的一个。其他窗口包有的在显示上有问题,有的显示起来不完整,有的在我调整其大小时效果很差。而这个窗口则看上去非常像浏览器中的一个真正的窗口。 回页首 carousel 小部件 从事过与用户界面(UI)相关的工作的人都会说,屏幕是非常宝贵的。将尽可能多的数据塞进一个给定的空间里,同时又不显得拥挤,这一点很重要。所以,当我第一次在 Apple iTunes 中看到一个 carousel 控件时,我感到异常惊喜。 carousel 控件 可以在一个固定的区域里显示多个图像。在图像区域的左边和右边有左箭头和右箭头。如果单击箭头,则图像就会向左或向右移动,并被一组新的图像取代。在 iTunes 中,图像都是一些唱片封面,每种唱片风格都有一个 carousel 控件。 这个控件可以节省很多空间:可以将 30 个唱片封面放在三个封面大的空间里,并还能以合理的大小显示每个封面。而且,这个空间也很直观。它看上去像一个简化的滚动条。 它的缺点就是不容易实现,很重要的一个原因就是图像左右移动这个诱人的动画效果不好实现。所以,很高兴能看到一个名为 carousel 的、构建在 jQuery JavaScript 框架之上的 carousel。 在 清单 9 中,我在 Web 页面上实现了一个简单的 carousel 小部件。 清单 9. Carousel.html <html> <head> <script type="text/javascript" src="js/jquery-1.0.3.js"></script> <script type="text/javascript" src="js/jcarousel.js"></script> <style type="text/css"> #mycarousel { display: none; } .jcarousel-scope { position: relative; width: 255px; -moz-border-radius: 10px; background: #D4D0C8; border: 1px solid #808080; padding: 20px 45px; } .jcarousel-list li { width: 81px; height: 81px; margin-right: 7px; } .jcarousel-list li img { border: 1px solid #808080; } .jcarousel-list li a { display:block; outline: none; border: 2px solid #D4D0C8; -moz-outline:none; } .jcarousel-list li a:hover { border: 2px solid #808080; } .jcarousel-next { position: absolute; top: 45px; right: 5px; cursor: pointer; } .jcarousel-next-disabled { cursor: default; } .jcarousel-prev { position: absolute; top: 45px; left: 5px; cursor: pointer; } .jcarousel-prev-disabled { cursor: default; } .loading { position: absolute; top: 0px; right: 0px; display: none; } </style> <script type="text/javascript"> function loadItemHandler( carousel, start, last, available ) { if (available) { carousel.loaded(); return; } var cr = carousel; jQuery.get("data.xml", function(data) { appendItemCallback(cr, start, last, data); }); }; function appendItemCallback( carousel, start, last, data ) { var items = data.match( /(\<img .*?\>)/g ); for (i = start; i <= last; i++) { if ( items[ i - 1 ] == undefined ) break; var item = carousel.add( i, getItemHTML( items[i-1]) ); item.each(function() { jQuery("a.thickbox", this).click(function() { var t = this.title || this.name || null; var g = this.rel || false; TB_show(t,this.href,g); this.blur(); return false; }); }); } carousel.loaded(); }; function getItemHTML( item ) { var found = item.match( /href=\"(.*?)\"/ ); var url = jQuery.trim(found[1]); var title = jQuery.trim(found[1]); var url_m = url.replace(/_s.jpg/g, '_m.jpg'); return '<a href="' + url_m + '" title="' + title + '" class="thickbox"><img src="' + url + '" width="' + 75 + '" height="' + 75 + '" alt="' + title + '" /></a>'; }; var nextOver = function() { jQuery(this).attr("src", "img/horizontal-ie7/next-over.gif"); }; var nextOut = function() { jQuery(this).attr("src", "img/horizontal-ie7/next.gif"); }; var nextDown = function() { jQuery(this).attr("src", "img/horizontal-ie7/next-down.gif"); }; function nextButtonStateHandler(carousel, button, enabling) { if (enabling) { jQuery(button).attr("src", "img/horizontal-ie7/next.gif") .mouseover(nextOver).mouseout(nextOut).mousedown(nextDown); } else { jQuery(button).attr("src", "img/horizontal-ie7/next-disabled.gif") .unmouseover(nextOver).unmouseout(nextOut).unmousedown(nextDown); } } var prevOver = function() { jQuery(this).attr("src", "img/horizontal-ie7/prev-over.gif"); }; var prevOut = function() { jQuery(this).attr("src", "img/horizontal-ie7/prev.gif"); }; var prevDown = function() { jQuery(this).attr("src", "img/horizontal-ie7/prev-down.gif"); }; function prevButtonStateHandler(carousel, button, enabling) { if (enabling) { jQuery(button).attr("src", "img/horizontal-ie7/prev.gif") .mouseover(prevOver).mouseout(prevOut).mousedown(prevDown); } else { jQuery(button).attr("src", "img/horizontal-ie7/prev-disabled.gif") .unmouseover(prevOver).unmouseout(prevOut).unmousedown(prevDown); } } jQuery(document).ready(function() { jQuery().ajaxStart(function() { jQuery(".loading").show(); }); jQuery().ajaxStop(function() { jQuery(".loading").hide(); }); jQuery("#mycarousel").jcarousel({ itemVisible: 3, itemScroll: 2, wrap: true, loadItemHandler: loadItemHandler, nextButtonStateHandler: nextButtonStateHandler, prevButtonStateHandler: prevButtonStateHandler }); }); </script></head><body><div id="mycarousel"> <div class="loading"> <img src="img/loading.gif" width="16" height="16" border="0" />Loading...</div> <img src="img/horizontal-ie7/prev.gif" border="0" class="jcarousel-prev" /> <img src="img/horizontal-ie7/next.gif" border="0" class="jcarousel-next" /> <ul></ul> </div></body></html> 是的,与前面的例子相比,这个例子多了很多代码。但大部分代码用于设置图形和解释从服务器返回的 Ajax 数据。实际上,本文中的大部分代码都是以下载部分提供的一个例子为基础的。所以,我不必学习很多东西,也不必阅读任何文档,就可以使用这个控件。 清单 10 显示了用于 carousel 的数据。 清单 10. Data.xml <images> <img href="pics/image1.jpg" /> <img href="pics/image2.jpg" /> <img href="pics/image3.jpg" /> <img href="pics/image4.jpg" /> </images> 这是一个简单的 XML 文件,它有一个 <images> 标记,其中包含一组 <img> 标记,在这些标记中保存有每个图像的 URL。 您可以使用您喜欢的任何格式,因为这个控件并不是一个纯正的 Ajax 小部件。我还编写了解释 XML 并创建 carousel 中每个幻灯片元素的代码。最终的结果如 图 8 所示。 图 8. 页面上的图像 carousel 页面上的图像 carousel 我可以单击图像,进入到包含该图像的页面(或我指定的任何 URL)。或者,我也可以通过单击左箭头或右箭头滚动 carousel,以查看更多图像。实际效果确实令人印象深刻。 回页首 结束语 我展示了网上提供的一些小部件和工具,它们有的是商业性的,有的是免费的。当我准备撰写本文时,我也看到过很多没有使用 Ajax 的工具。虽然这些工具不适合放到本文讨论,但是它们本身也是很值得注意的。特别是,网上供下载的很多高质量的开源 WYSIWYG 编辑器给我留下了深刻的印象。当用户不得不使用文本框中的 HTML 将内容以粗体、斜体、链接、图像等形式放到站点上时,他们常常会感到十分郁闷。而这些编辑器则能隐藏所有 HTML,使用户感觉起来像是在文字处理应用程序中进行编辑一样。 分享这篇文章...... digg 将本文提交到 Digg del.icio.us 发布到 del.icio.us Slashdot 提交到 Slashdot! 除了 WYSIWYG 编辑器以外,还可以找到进度条、带选项卡的对话框、折叠式控件、时钟、日期选择器、RSS 和 Outline Processor Markup Language (OPML) 阅读器,甚至交互终端窗口的解决方案。所以,在构建自己的 DHTML 或 Flash 控件之前,显然应该看看网上有没有可用的控件(通常都是免费的)。通过使用像前面提到的那样的小部件,可以很容易地为站点增加交互性。 参考资料 学习 * 您可以参阅本文在 developerWorks 全球网站上的 英文原文。 * 您可以参阅 本系列的其他文章。 * Ajax 技术资源中心:developerWorks 上所有有关 Ajax 的问题都可以在这里找到解答。 * developerWorks 中国网站 XML 专区:在 developerWorks 中国网站 XML 专区了解更多关于 XML 的信息。 * jQuery 和 prototype.js:查看这些流行的 JavaScript 框架。 * IBM XML certification:了解如何才能成为一名 IBM 认证的 XML 及相关技术的开发人员。 * XML 技术库:查阅 developerWorks XML 专区,这里有很多技术文章和技巧、教程、标准及 IBM 红皮书。 * 随时关注 developerWorks 技术活动 和 网络广播。 * Ajaxian:通过这个很棒的资源,关注 Ajax 和使用它的前端小部件的发展。 获得产品和技术 * JCarousel:试试这个构建在 jQuery 框架上的 carousel 小部件。 * SWF/Gauge 和 SWF/Charts:获取 maani.us 开发的 SWF/Gauge 和 SWF/Charts。还可以找到一个 Flash XML Slideshow 小部件。 * DHTML window:试试我所发现的最好的 DHTML 窗口包。 * Scriptaculous:下载 Scriptaculous,从 script.aculo.us 站点了解更多关于它的信息。另外还需要 Prototype 库。 讨论 * 参与论坛讨论。 * XML 专区讨论论坛:参与以 XML 为中心的论坛。 关于作者 Jack D. Herrington 是一位有着超过 20 年经验的高级软件工程师。他是 Code Generation in Action、Podcasting Hacks 和 PHP Hacks 这三本书的作者。他还撰写了 30 多篇文章。可以通过 jherr@pobox.com 与 Jack 联系。
|