重构HTML:改善Web应用的设计(修订版)

978-7-115-29407-4
作者: 【美】Elliotte Rusty Harold
译者: 陈贤安等
编辑: 杨海玲

图书目录:

详情

本书采用理论与实践相结合的方式,展示了如何重构 HTML,以获得更佳的可靠性、性能、可用性、安全性、可访问性、兼容性,甚至实现良好的搜索引擎优化。

图书摘要

版权信息

书名:重构HTML:改善Web应用的设计(修订版)

ISBN:978-7-115-29407-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]Elliotte Rusty Harold

  译    陈贤安 等

  责任编辑  杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


本书采用理论与实践相结合的方式,展示了如何重构HTML,以获得更佳的可靠性、性能、可用性、安全性、可访问性、兼容性,甚至实现良好的搜索引擎优化。书中详细介绍了如何辨别应该重构的Web代码中的“坏味道”,如何把旧的HTML转换为良构和有效的XHTML,如何使用CSS改善现有的布局,如何通过用GET替换POST、替换旧的联系表单和重构JavaScript来更新Web应用程序,如何系统地重构内容和链接,如何在不改变用户所依赖的URL的前提下重建网站。

本书适合Web设计人员、开发人员、项目¾理和需要维护或更新既有网站的人使用。


Authorized translation from the English language edition, entitled: Refactoring HTML: Improving the Design of Existing Web Applications, 978-0-321-50363-3 by Elliotte Rusty Harold, published by Pearson Education, Inc., publishing as Addison-Wesley Professional, Copyright © 2008 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD. and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2012.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。


“真是一本介绍新知识新方法的绝妙小册子!令人回味无穷啊!Elliotte的这本书出人意料。书中除了讲重构,还明确地讲解了如何在一开始就不要误入歧途。作者显然精于此道。万万不要错过!”

——Howard Katz,Fatdog Software公司的所有者

“在跟许多有意持续改进其应用程序质量和安全,但又苦于找不到相关技术及工具的人们接触之后,我发现了缺少的那一环。重建和重写应用程序的能力是Web设计、开发人员严重欠缺的。把重构引进开发流程,渐进地改变布局或内部结构,可以有效地避免全面重写或整体返工。需要重建、重写或重构Web吗?相信你会对这本书爱不释手。”

——Andre Gironda,tssci-security.com

“本书给出的大幅提升网页质量的提示和窍门难得一见。实际上所有真正的HTML开发人员,不管你所在的公司是何等规模,也不管你是新手还是老手,都能从Elliotte的建议(哪怕只是他的三言两语)中受益匪浅。”

——Matt Lavallee,MLS Property Information Network公司的开发经理


仅仅十余年,Web就从一门大有希望的技术演变成了世界基础设施的重要组成部分。在这个绚丽迷人的年代中,涌现了许多有用的资源。一如往常,我们在追求最佳实践的过程中不断地学习技术,而技术本身的成熟也促使我们更好地使用它。

无论多么复杂的Web应用程序,最终都要通过HTML这种通用的网页描述语言呈现在屏幕上。HTML虽然功能非常有限且用途单一,但它也是一门计算机语言。因此,如果想让系统能够轻松迭代进化,必须编写清晰易懂的HTML。但是与任何一门计算机语言一样,甚至与任何文章一样,第一次编写HTML很难写到位。只要坚定决心,反复尝试,就一定能写出清晰易懂的代码来。

重写代码有引进bug的风险。几年以前,我曾著书讨论过重构技术,它是一种重写代码的严谨的方法,在修订现有软件时能够大幅度降低引入新bug的机会。重构对常规软件语言产生了巨大影响。很多程序员把它作为日常工作的一部分,作为保持代码清晰和提升自己未来工作效率的辅助手段。自动化重构任务的工具也如雨后春笋般涌现,进一步改善了重构的工作流程。

重构能够大幅改善常规的编程,它的基本思想同样可以运用到HTML上。重构的步骤虽然不一样,背后的思想却如出一辙。通过学习如何重构HTML,可以使HTML清晰灵活地适应未来的变化。一旦变化不可避免,可以快速进行改动。这些技术也能让你的网站紧追Web技术潮流,尤其是有助于支持XHTML和CSS。

Elliotte Rusty Harold编写的关于XML技术和处理XML开源软件的著作很早以前就摆上了我的书架,始终占有一席之地。我一向敬重他这位优秀的程序员和作者。他通过本书把重构的益处带入了HTML世界。

——Martin Fowler


万维网成功的一个关键是:任何人都能很容易地创建网页,并将其放在人人皆可访问的地方。人们创建的互相链接的页面越多,他们的网站对更多的人就越有用。Web造就百万富翁的故事也不断激励着Web开发者规划更宏伟的事业。

但随着网站变大,很多人感受到了成长之痛。改过的链接指向不明,页面在不同的浏览器中观感不一,要找到某些东西越来越难,特别是在需要把改动统一地应用到整个网站上的时候,这些问题就更加突出。碰到这些情况时,很多自己建站的人都需要寻求专家的帮助。不过有了本书,你就可以像专家一样自己解决问题;假使你已经是Web专家,也可以变得更专业。

市面上有很多入门级的Web技术书,但本书是第一本融汇中级内容的读物,讨论了创建专业的、可维护的、可访问的网站的所有关键技术。也许你已经是此书涉及的某些主题的专家,但很少有人能够像Elliotte这样透彻理解所有的主题,而且他的讲解非常通俗易懂。(我对XML非常熟悉,但此书告诉我只要略微改变一下自己的CSS习惯,我创建的那些网页都能得到改进。)

在本书的每一个建议中,Elliotte都清晰阐述了动机、遵循该建议可能要做的权衡和实现技巧,每条提示都分析了来龙去脉。他发现问题的能力极强,我险些要拿他灵敏的嗅觉去与普鲁斯特比了,这的确是容易让人联想到的。

我已经读过好几本Elliotte的书,但是他写的书也有我没来得及看的。当我听说他在写这本书时,我知道我绝对应该拿来看看。很高兴的是我能够提前阅读这本书,而且获益匪浅。你也会大有所获的,相信我!

——Bob DuCharme,Innodata Isogen公司的解决方案架构师

普鲁斯特(1871—1922)是法国著名的意识流小说大师,代表作为《追忆逝水年华》。——编者注


重构。什么是重构?为何要重构?

简明扼要地说,重构是在不改变程序行为的基础上进行小的改动,是代码基逐渐改善的过程,通常也需依赖于一些自动化工具的帮助。重构的目标是移除长年积聚下来的烂码,以得到更清晰和更容易维护、除错以及添加新功能的代码。

严格地说,重构实际上并不涉及除错,也不增加新功能。但在实践中,重构的过程总是会碰到需要修正的错误,也会有需要加入新功能的时候。重构通常会化繁为简,化难为易。改善代码的第一步是重组代码。

举个例子,无论是为了开始新的学期,还是从事新项目或新工作而需要彻底整理你的工作区、桌面或者办公室,此时你就会意识到什么是重构。重构让你推陈出新,不会让你白手起家。通过重构你会得到整洁有序的工作空间,从中可以找到所有需要的东西,并因此提升工作效率。

重构的概念最先来自面向对象编程社区,时间至少可以追溯到1990年(William F. Opdyke和Ralph E. Johnson写了一篇文章“Refactoring: An Aid in Designing Application Frameworks and Evolving Object-Oriented Systems”,发表在1990年9月的Proceedings of the Symposium on Object- Oriented Programming Emphasizing Practical Applications [SOOPPA]上),但在此之前似乎并不是很流行。但Martin Fowler在1999年出版的《重构》(Addison-Wesley,1999)一书促使这个术语迅速流行。此后,Eclipse、IntelliJ IDEA和C# Refactory之类的IDE和工具为各种语言(如Java和C#)实现了《重构》一书中提到的很多重构功能,同时还发明了一些新功能。

但是,产生烂码而且需要重构的,不只是面向对象代码和面向对象语言。事实上也不只是编程语言需要重构,几乎所有需要多次开发和维护的、足够复杂的系统都能在重构中获益。原因有两方面。

(1) 随着系统和问题领域知识的不断完善,初始设计者当初未曾想到的细节逐渐暴露出来。没有谁可以在第一次发布系统时就做好一切,必须在生产环境中对系统进行一段时间的观察才能发现问题。

(2) 增加了功能,也编写了支持它的新代码。就算原有系统很好地解决了问题,支持新功能的新代码还是不能很好地契合旧代码。最终,你会面临这样一个问题:旧代码再也无法支持需要添加的新功能。

当你发现系统再也不能支持添加新功能时,有两条路可走:要么抛弃旧系统重建新系统,要么在旧系统的基础上设法改进。在实践中,我们很少能有时间或预算去建立一个全新的系统,来取代正在运作的旧系统,因而,改进和增强现有系统显得更合算。如果可以逐步加入支持,而不是一口气大规模地集成,显然会好得多。

很多包含大量代码的复杂系统都不是使用面向对象语言编写的,甚至不包含程序代码。比如, Scott Ambler和Pramod Sadalage在Refactoring Databases(Addison-Wesley,2006)一书中就演示了如何重构以支持许多大型应用的SQL数据库。

大型网络应用的后端通常是关系型数据库,而前端就是网站。Firefox或IE等浏览器的瘦客户端图形界面(GUI)无处不在,在商业应用中几乎全部取代了胖客户端,比如薪酬发放系统和客户追踪系统。Sun和Google等公司甚至更大胆,使用基于HTML、CSS和JavaScript构建的Web应用程序取代文字处理和电子表格软件等传统的桌面程序。总之,Web和无处不在的浏览器联手创造了全新的应用,如eBay、Netflix、PayPal、Google Reader和Google Maps等。

HTML使这些应用程序得以诞生,也让开发更迅速,但是并没有能降低难度,当然也没有从根本上减小应用程序的复杂性。这些系统中有的已经是第二、第三甚至是第四代了——你会不知道吗?跟其他足够复杂、足够古老的应用程序一样,这些Web应用程序也会包含烂码。代码中新的部分并不能完美地契合旧的部分;系统越来越慢皆因整个结构不合理;黑客游走在新旧部分衔接的裂缝中,暴露了系统的安全问题。是抛弃旧有的应用程序全新开始,还是在原有基础上继续修正?在今天这个迅猛发展的时代,没有人能经得起推倒重来,因此其实你已别无选择,唯一现实的选择就是重构。

怎样才能知道到了重构的时机呢?哪些代码的气味会难闻到让你捂住鼻子?以下列举一些“坏味道”,应该也算是最坏的味道了。

最明显的情形莫过于查看页面的源代码就好像在看希腊语(当然,你在希腊工作的话不算)。看到这样的代码,大部分编码人员都能意识到它们很难看。难看的代码看起来就是难看的。比较代码清单1-1和代码清单1-2,你更愿意看到哪一个呢?我知道你并不需要我告诉你答案,而哪一个又更容易维护和更新是显而易见的。

代码清单1-1 脏乱的代码

<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0" WIDTH="100%">
<TR><TD WIDTH="70">  <A HREF="http://www.example.com/" TARGET=
"_blank"
>
     <IMG SRC="/images/logo-footer.gif"
HSPACE = 5 VSPACE="0" BORDER="0"></A></TD>
  <td class="footer" VALIGN="top"> ©2007 <A HREF="http:// www.example.com/" TARGET="_blank">Example Inc.</A>.
All rights reserved.<br>
   <A HREF="http://www.example.com/legal/index.html"
TARGET="_blank">Legal Notice</A> -
   <A HREF="http://www.example.com/legal/privacy.htm"
TARGET="_blank">Privacy Policy</A> - <A HREF="http://www.example.com/ legal/permissions.html"
TARGET="_blank">

    Permissions</A>
</td>
 </TR></TABLE>

代码清单1-2 整洁的代码

<div id='footer'>
 <a href="http://www.example.com/">
  <img src="/images/logo-footer.gif" alt="Example Inc."
     width='70' height='41' />
 </a>
 <ul>
 <li>© 2007 <a href="http://www.example.com/">Example Inc.</a>.
 All rights reserved.</li>
 <li><a href="http://www.example.com/legal/index.html">
   Legal Notice
 </a></li>
 <li><a href="http://www.example.com/legal/privacy.htm">
  Privacy Policy
 </a></li>
 <li><a href="http://www.example.com/legal/permissions.html">
  Permissions
 </a></li>
 </ul>
</div>

现在,你可能会抱怨我在代码清单1-2中几乎没有重排代码的格式。事实上我改过,比如把一个表格转换成了div元素和列表(list),并删除了部分连字符。然而,与代码清单1-1相比,代码清单1-2实际上更接近于内容的含义。代码清单1-2在此假设使用了一个外部的CSS样式表来提供所有的从代码清单1-1中删掉的样式细节。你将会看到,这是重构页面和整理文档使用的主要技术之一。

同时我也把用于打开新窗口或新标签页的TARGET="_blank"属性甩进太平洋了。这通常不是用户需要的,而且很难称得上是好的方式。有必要的话,让用户使用后退按钮和历史清单就好了,或者让大部分链接总是在相同的窗口中打开。如果用户要在新窗口打开链接,他们可以通过右键菜单等方式轻松做到。应该把选择权交还用户。有时候,清理的大半工作不过是删除那些在不该出现的地方出现的代码。

关于行的长度

代码清单1-2还是不够理想。由于书上的代码行必须控制在页面内,我觉得有点儿受局限。在真实的源代码中,我可以改成一行。但是也别走极端。每行多于80个字符会变得难以阅读,而这本身也有小小的代码坏味道。

但是通过诸如CMS(内容管理系统)这种工具生成的代码,在此有小小的例外。这种情况下,通过查看源代码看到的代码并不是真的源代码,它是编译过的机器格式。这样一来,应该要求输入CMS之前的模板代码本身就有必要做到清晰美观。

尽管如此,如果CMS和Web编辑器等工具能产生清晰、良构的代码会更好。可是经常出人意料的是,你会发现由工具产生的代码只是个开始,而不是结束。工具生成代码之后,你可能还需要为代码增加样式表、脚本和其他各种各样的东西。如此一来,还是需要处理那些原始的标记,此时你会发现整洁的代码更容易处理。

Web可用性在过去几年逐渐得到改善,但没有到达应有的水平。几乎所有最好的网站都开始更关注访问者而非编写者或设计者了。一些旨在改善可用性的简单改动,如放大字号(或者完全不指定字号)和组合表单字段,都能获得丰厚的回报,极大地提高生产力。这对企业内部网站和任何需要销售给客户的站点尤为重要。

一个主流浏览器如果需要花数秒的时间显示一个页面,那么问题就来了。这个问题有些难以判断,因为很多页面载入慢是由于网络拥塞或数据库和HTTP服务器过载导致的。不过就算不能通过修整HTML来解决,这也是问题。但如果是在一个本地文件系统保存的页面也需要花数秒的时间显示,就必须考虑通过重构来提升速度了。

网页外观并不总是要求在不同浏览器中保持完全一致,但使用合适浏览器的任何用户都应该能够访问所有内容和功能。如果页面在Safari、Opera、IE或者Firefox中难以辨认或者干脆没法用,那么就有问题了。比如,你可能会看到一条跟随在内容窗格之后的全屏等宽的边栏,但这个边栏出现在内容的下面而不是上面。通常,页面在编写者的浏览器中看起来是没有问题的,但他并不麻烦自己去检查页面在你所使用的浏览器中是否正确。为保证正确的呈现,应该在所有的主流浏览器中检查页面。

发现诸如“使用IE获得最佳浏览效果”之类的语句时,说明代码有坏味道了,也是到了重构的时候了。看到类似图1-1中的界面时,说明代码已经“腐臭”——并且所有的访问者也嗅到了坏味道。IE占有的市场份额已经少于80%,而且还在迅速下滑。事实上这也可能高估了,因为大部分的爬虫和机器人很不老实,总是把自己伪装成IE,这刚好能解释不成比例的点击量。Mac OS X和Linux的用户甚至没有选择使用IE的机会。只为一个浏览器设计网站的时代已经一去不复返了。

图1-1 沃尔玛网站把非IE用户拒之门外

还有一个常见的变种是要求使用指定的屏幕尺寸,例如,“这个页面在1024×768的屏幕分辨率下具有最佳浏览效果。要改变你的显示器分辨率,打开……”。设计良好的网页不需要任何特定的屏幕尺寸或浏览器。

很多网站都要求开启cookie、JavaScript、Flash、PDF、Java或其他非HTML的技术。尽管它们都有各自的用途,但被网页滥用了。一般情况下,它们并非像大多数设计者想象的那样通用和可靠。作为安全警告的常客,用户常被告知要在这个或那个浏览器中关闭它们,以免遭受黑客攻击。Google或其他搜索引擎机器人也不支持这些技术。因此,应该尽力保证网站大部分网页的功能在这些技术不可用的状态下依然有效。

好在这些代码坏味道表现得十分明显,并且也容易侦测。无论何时,当你看到下面这些提示信息时,它们都是代码坏味道的表现。

其实这些不仅让用户恼火,而且还拒Google于门外,因此在搜索引擎中只能得到一个差劲的排名。

下面是关于清理HTML页面的一个真实例子,它会让你看到更尴尬的情况。

注意


此处有目录,但是动态生成的。请启用JavaScript来显示目录。

虽然处理动态内容的正确方式是使用服务器端模板,但仍需给客户端发送静态HTML。我发现一个网站要求符合以下的几乎所有条件才可使用。

本站使用JavaScript、Cookie、Flash和弹出窗口,并为最新版的IE、NetscapeNavigator(不是Netscape6)和Opera量身定制。

如果再加上屏幕尺寸的要求,那真可以去超级正序连赢赌(superfecta)搏上一把了。

这个站点还有另一个问题:弹出新窗口。滥用的弹出窗口促使了屏蔽功能的广泛部署,正规的网站绝不应依赖于滥弹窗口。

当然,有些功能只能依托于JavaScript或其他的非HTML技术。如果你确实有足够的理由,我并不想劝你停止开发另一个Google Maps或者YouTube。尽量少用新奇的技巧,保证所有功能可以不依赖Java、JavaScript或Flash等技术来实现。以下这条Flickr提示信息就清爽多了。

要充分利用Flickr的功能,请使用开启了JavaScirpt的浏览器,并安装最新版的Macromedia Flash Player

尽管同样看到了提示,但它的不同之处在于,就算禁止了JavaScript和Flash,页面也出现了用户想要看到的内容。用户可能看不到所有内容,或用不上所有功能,但没有被拒之门外。这对用户和搜索引擎(如Google)都友好多了。

从网站开发者的角度讲,我还应花点时间来检视页面,看看能否减少一些对客户端的要求(依赖)。但这不是最紧要的事情。

把你从美梦中唤醒的最大原因莫过于网站被人黑得面目全非了,这也会引起所有人的迅速关注。被黑的原因很多,但最常见的是直接对设计有漏洞的表单处理脚本实施代码注入攻击。

坦白地说,如果只是网站外观受损的话,你算是幸运的,而且应该感谢黑客帮你指出了问题所在。但不要抱侥幸心理,严重的入侵会导致私密数据被窃取或关键信息被删除。

搜索引擎优化是网站重构的主要驱动力之一。跟图片相比,搜索引擎更看重文本;跟后端文本相比,更看重前端文本。它们理解不了表格布局,也不在意cookie或JavaScript。它们更喜欢独特的标题,或是元标签(meta tag)。

出现问题最明显的形式莫过于此了。比如,最近我收到了一封来自访问者的电子邮件。

Cafe au Lait上的“Further Reading”部分中指向“The Next Big Language?”和“Testing Hop Stop”的链接失效了。

祝好

Kent

这让我感到非常吃惊,因为Kent抱怨的部分是使用XSLT转换其他网站的Atom Feed源自动生成的。我检查了来源,它并没有错误。但是Kent说得对,链接确实失效了。我最终确定错误出现在XSLT样式表中。它总是去读取一个元素,大部分情况下这个元素都是一个链接,但也有例外,这是出错的根源。搞清楚了问题,我只用5 min就把网站修好了。

对于这种网站失灵的情况,99%的读者只会抱怨而不会通知你。剩下的1%的抱怨者是最可贵的。你要认真对待并听取他们的意见。每个站点的大部分页面都应该在明显的位置放上联系人的信息。这样在出现问题时,可以让用户能找到你。这些回馈都应该仔细考虑并迅速做出回应。

当然访问者也极有可能给你发送很多跟网站不相关的电子邮件:取消订单、送货日期、链接申请、问题争议等。你需要把技术问题和非技术问题分开,这样才可以适当地安排收发信件。有些网站使用在线表单,并使用预先归类好的问题询问访问者。然而这并不可靠,因为访问者并不总是以开发者的角度来看待网站的。比如,如果客户不能在送货地址表单中输入带连接符的9位数字邮编,开发者会认为这是技术性错误(的确如此)。但是客户极有可能把它看成送货问题,直接报给一个根本理解不了问题,更别说解决问题的部门。或许你需要一个负责分类的员工或小组来判别每一封邮件,并决定谁最适合给出回馈。这是一个重要的功能,不应该外包给出价最低的投标人。

无论如何,不要让问题报告掉进客服的黑洞中。要保证站点用户反馈的问题能直接交到有能力处理它的人手上,同时保证问题被他们注意到。太多的网站使用电子邮件和联系表单,把开发者和实际用户生硬地隔开了。不要浪费这些免费的用户测试,网站可是花了很多钱来雇佣QA团队的。如果有人自愿帮你测试,一定要珍惜并很好地利用这些机会。

应该在什么时候重构呢?在清除和处理过时代码的过程中,何时才算是到了添加新特性的时机?这个问题可能有几个答案,但它们之间并不是互斥的。

重构的第一个时机出现在进行任何重新设计之前。如果你的网站碰到需要重新开发的情况,首要任务是掌控它的内容。此时重构的第一个好处显而易见,你将为新设计的实现创建更牢固的基础。良构、组织良好的页面更容易重新设计样式。

重新设计之前进行重构的第二个好处是,重构过程能帮助开发者熟悉网站。你将会了解页面位置,它们在网站层次上是如何组合起来的,以及哪些是跨页面的通用元素等。同时,还有可能对重新设计产生前所未有的新想法。别急着去实现,把想法都先记录下来(放到问题追踪系统中则更好),在重构完成之后,就可以着手实现了。重构是十分重要的工具,可以帮助网站开发者加速网站的开发。如果他们以前未参与开发这个需要重构的网站,重构的过程会帮助他们了解网站的细节。就算是为网站工作已经超过了10年,重构也能提醒他们一些已经遗忘的东西。无论是哪种情况,重构都能帮助开发者熟悉网站。

新功能可能只出现在新页面,甚至可能只出现在全新的网站上。如果跟旧网站仍有关联,你需要检查一下是否有可借鉴或可重用的东西。样式、图片、脚本和模板或许都可重用。这样确实可以帮你保持新旧网站具有一致的外观。但如果需重用的部分有问题,则应事先清理一番。基于类似的理由,在开发任何基于旧项目的新项目之前,都应该好好揣摩一下重构。比如,如果要实现“一键式购物(one-click shopping)”,先要保证旧的购物车能够满足需要。如果法律部门要求在每一个页面的底部加上限制性附属细则(small print),还得看看每个页面是否都有一个固定的位置可以方便地记录这些信息。没有必要重构所有的东西,但这种改动总是有助于在网站中集成新内容。

最后,或许你会考虑半连续式重构(semicontinuous refactoring)。如果碰到难以一下子解决的问题,不要连续两周无动于衷,在这期间看看还有什么地方是可以重构的。就算是不能解决大问题,也应该马上去做些力所能及的事情来修复一些小问题。尽可能使用敏捷开发方式,编写小部分,测试小部分,重构小部分,如此反复。尽管很多项目都要基于一大堆不良和过时的代码开发,不过如果有未完工项目的话,一定要先保证不让它变成又一个烂码基。一旦看到自己在重复相同的代码,可以把它提取到外部模板或样式表中去。当发现原来的HTML编写者使用了废弃的元素,应该马上使用CSS取而代之(并利用这个机会教育一下这个编写者为何要这么做)。一连串小的但有效的变化累积起来就会带来巨大的效果。

无论做什么,都不要因追求完美而忽视小的改进(勿以善小而不为)。如果眼下的时间足够做一点儿重构,那就只做一点儿。以后有时间还可以做得更多。整体性的重新设计虽然惹人注目令人难忘,但不积跬步又何以至千里?你的目标应该是让代码每天都有新变化。这样坚持几个月,你就会拥有可以骄傲地向人夸耀的清晰代码。

诸如Java之类的编程语言和诸如HTML之类的标记语言的重构有一个关键的区别。与HTML相比,Java变化相对缓慢,C++的变化很少,C就更不用说了。用Java 1.0编写的程序跟Java 6的跑起来没有什么大的不同。Java增加了大量新特性,但大体上能保证一致性。

然而,HTML和相应的技术在同样的时间范围内却发生了巨大的变化。今天的HTML跟1995年的根本不是一码事。新版不仅剔除了一些关键字,也加入了一些新的关键字,在语法和解析算法上也有所改变。尽管现代浏览器如Firefox或IE 7通常可以显示一个过时的页面,但你会发现很多东西根本无法正常运作。进一步讲,浏览器必须处理CSS和ECMAScript等全新组件。

本书给出的大部分重构建议,将会重点围绕升级网站以支持Web标准,尤其是:

这有助于将开发人员从以下困境中解救出来:

这里并没有非此即彼的选择或是孤注一掷的决定。你总是可以从3个方面入手改善网站的特性,用不着追求尽善尽美。持续改进是重构的重要特征。小改动产生小改善,没有必要毕其功于一役。你可以在实现有效的XHTML之前先实现良构的XHTML;在转向CSS之前也可以先实现有效的XHTML;在进一步考虑消除会话(session)或会话cookie之前,可以先把网站完全符合CSS的布局做完。

当然不一定得按照这些顺序来执行这些改动。你可以从建议列表中选择能为应用程序带来最大好处的重构方案。这有可能是不需要XHTML但是急需CSS的情况;又或者需要把应用程序的架构转向REST以提升性能,但并不关心把文档转为XHTML。不过最终选择权在你手中。本书给出了各种建议和方案,帮你权衡各种重构方式的代价和收益。

使用标签汤、基于表格的布局、图像映射和cookie构建Web应用当然是可以的,但扩展它的投入大部分人都担负不起。横向(更多用户)和纵向(更多功能)的扩展均需要一个比较健壮的根基,而这些正是XHTML、CSS和REST所能提供的。

XHTML只不过是XML化的HTML。HTML至少在理论上基于SGML,而XHTML则基于XML。XML是一个比SGML更简洁和清晰的规范。因此,XHTML是HTML简洁、清晰的一个变体。选择XHTML还是选择HTML,这就像眼前摆着一把枪,你是去扣动扳机,还是去做靶子,那感受可是天壤之别。

对用户更友好的XHTML页面是以编写者更难开发为代价的。HTML容许犯错,但XHTML不容许。在HTML中,如果忽略了结束标签或是在这里或那里留下了多余的引号,不会发生什么大事,最多只是有些文本被标记为粗体了,或者不正确地缩进了。即使是最坏的情况,也只是在这、在那少了一些词语而已,页面的大部分还是能显示出来的。这种宽容的本性使得HTML的学习更加容易。因为你编写的HTML即使是不正确的,也不至于发生严重的问题。

但XHTML非常严格。诸如漏掉引号或忽略结束标签这些细微的错误,在HTML中浏览器会默默补上,但在XHTML中就会成为严重的四级警报。在XHTML文档中,就算是小小的错误,浏览器也只能举手缴械,拒绝显示页面,如图1-2所示。这让编写XHTML页面的工作难度加大,特别是在使用纯文本编辑器时。但就跟编写计算机程序一样,一个语法错误能破坏所有东西。错误是没有商量的余地的。

图1-2 Firefox对XHTML页面中错误的反应

那么为什么大家都要选择XHTML呢?这是因为,虽然这些限制加大了编写XHTML的难度(严格的错误处理),但却简化了XHTML的处理。现在接力棒已经从浏览器交到了编写者的手中。浏览器(或者其他要读取页面的设备)不必再试图弄懂混乱的标签汤,并去猜测页面的真正意图。只要页面有一丁点儿的不清晰,浏览器就可以(实际上也是有必要的)放弃并拒绝处理。这能让浏览器的工作更简单。当前大多数浏览器在解析HTML代码时,都把精力投入到纠正页面的错误中去了,有了XHTML后就没有必要这么费心了。

当然,我们绝大部分人都不是浏览器开发者,而且可能永远没有开发浏览器的机会。我们能从XHTML及其严格的错误处理中得到什么好处呢?自然有好几条。首先,尽管大多数人不用去开发浏览器,但大部分人要编写支持网页的程序。这些程序可以是混搭(mashup)、Web Spider、Blog聚合器、搜索引擎、开发工具等,它们都需要读取网页。这些程序处理XHTML要比处理HTML容易得多。

当然,通过Web工作或者编写网页的大多数人都不是传统的程序员,而且不会去编写 Web Spider或Blog聚合器。但有两样很有可能需要他们去编写:JavaScript以及样式表。从数量上来说,这是最普遍的一类读取网页的程序。所有嵌入到网页中的JavaScript程序都需读取网页。所有的CSS样式表(尽管从传统的角度来看,它并不是程序)也需读取页面。如果JavaScript和CSS读取的是XHTML而不是HTML,就更容易编写和除错。实际上,编写有效的XHTML所付出的额外代价,在为JavaScript和CSS除错时会获得更多的时间补偿。

修复XHTML错误是恼人和花费时间的,但这是一个相当直接的过程,并不是那么困难。验证器能列出这些错误,你可以浏览这个错误清单并逐一修正它们。事实上,这种层次上的错误比较明显,可以自动修复它,这会在第3章和第4章中介绍。修复XHTML可能得花费些时间,但这些时间总量是可预估的。这样才不至于让你在病态的HTML中,进行JavaScript或CSS的跨浏览器排错时变得沮丧,看不到完工的尽头。

使用文本编辑器手工编写正确的XHTML也是一个不小的挑战。不过由工具生成标记的话,XHTML会变得更容易。良好的WYSIWYG(所见即所得)HTML编辑器(比如Dreamweaver 8)可以(而且应该)被配置为在默认情况下生成有效的XHTML。标记级别的编辑器(如BBEdit)也可以设置为使用XHTML规则,尽管编写者需要小心对待。很多编辑器都有检查XHTML有效性的选项,可以设置为通过单击一个按钮就可自动修正任何错误。

保证你已经开启编辑器的一些必要选项。与良好的CMS和Wiki类似,Blog引擎也可以根据需要生成XHTML。如果你的编写工具不支持XHTML,尽一切办法找个更好的工具。在21世纪的今天,HTML编辑器或Web发布系统不会不支持XHTML。

如果你的网站是手工搭起的模板系统,有可能得多做些工作,在第3章和第4章中我们会看到这种需求。尽管这种处理手工做得多了一些,但一旦做完就能自动生成有效的XHTML。发布者通过数据库或者Web表单输入内容时,或许完全不需要改变他们的工作流程,特别是在使用如markdown或wikitext等非HTML格式的数据时。系统可以轻松地将它们转换为XHTML。

使用XHTML而不是HTML的第二个理由是跨浏览器兼容性。实际上,在今天的浏览器上XHTML比HTML更具有一致性,大量使用CSS和JavaScript的复杂网页更是如此。尽管浏览器可以修正传统HTML的标记错误,但并不是总能以相同的方式处理。虽然两个不同的浏览器读取的是相同的页面,但会产生不同的内部模块。这给编写跨浏览器的样式表或脚本增加了难度。反之,XHTML并不需要浏览器做大量解释,也减少了浏览器抓狂的机会。诚然,不同的浏览器对CSS、JavaScript和DOM(文档对象模型)兼容性的支持不同,但转向XHTML清除了跨浏览器兼容性的一大障碍。即使不是完整的方案,但也可以修正大量的问题。

第三个理由是在页面中加入未来的新技术。从上面详细列出的理由可知,XHTML是更为强大的平台。HTML善于显示文本和图片,对于简单的表单来说也还不错。但除此之外,浏览器也是Flash、Java和Ajax等其他技术的宿主。很多功能在浏览器中并不容易实现,比如数学公式和音乐曲谱。有些功能本来就是不难的,比如在表单中输入错误的值时提示用户。

为改善这些问题的技术在不断发展,而且还有更多的技术正在开发当中,包括为数学公式准备的MathML,为乐谱准备的MusicXML、为动画准备的SVG(Scalable Vector Graphics,可伸缩矢量图形)和为强大客户端程序准备的XForms等。所有这些都是以XHTML为基础的,但它们都不能正确地基于传统的HTML使用。把页面重构为XHTML可以利用即将到来的新技术。在某些情况下,新技术可以让你做到原先做不到的事。在其他情况下,让你做到现阶段技术所能做到的,而且更快更容易。无论是哪一种情况,只要是其中之一,都值得你去把HTML重构成XHTML。

把表现从内容中分离出来是HTML的基本设计原则。这让你可以为不同的客户端提供相同的内容,但可以由客户端来决定样式,最大程度上适应它们所需要的环境。手机浏览器不具备Firefox等桌面浏览器一样的性能。实际上,手机浏览器很可能根本不去可视化地显示内容,而是只把文档内容读给用户听。

因此,HTML文档应该侧重表达的是文档的意义而不是文档的表现。更重要的是,这种编写风格尊重用户的偏好。访问者可以选择适合自己的字体和颜色,而不是编写者提供的默认值。一种尺码并不能适合所有人。拥有正常视力的30岁飞行员非常容易看清的网站,可能对于一位85岁的老奶奶来说就是模糊不清的;一个漂亮的蓝绿设计对于一个色盲用户来说可能就是很费解的;一个精心设计的表格布局,对于正在花园州高速公路(Garden State Parkway)上兜风时使用手机收听网页的司机来说,只不过是一些杂乱无序的词组。

在HTML中,你不应将一些段落格式CSS化为左对齐的11点的加粗Arial字体,而应该指定它是一个H2标题。自从Netscape出现并发明了font标签和其他一些表现性元素后,人们就马上开始使用——恐怕你也没少用吧。虽然之后W3C以CSS作为回应,但是这种破坏业已产生。网页到处充斥着fontframemarquee以及其他表现性元素。语义元素如blockquotetableimgul的本身意义都被颠覆了,只用来达到布局的目的。坦白地说,虽然运作并不是那么好,但很长一段时期内,这是我们所能做到的最好状态。

但现在不一样。今天的CSS不仅仅是简单的重复,比起使用frame、占位GIF以及图片隐藏文本等做法实现的布局和表现相比,它要优秀得多。CSS布局不仅更精美,而且更少偏差、更高效、更好用,而且页面加载变得更快,显示得更好。多做些努力,CSS就能让页面在多平台多浏览器上运作得更好。

把表现标记挪出页面并放到分离的样式表中去,你就可以从一个简单的页面开始,这种简单性对所有访问者都是清晰的,即使使用的是10年前的浏览器。然后可以为这些页面赋予漂亮的外观效果,改善使用它们的用户的体验。这么一来不会有任何人被拒之门外,页面实现了优雅降级(degrade gracefully)。

这种方式也对开发者有利。首先,它让掌握不同技术的不同开发者可以发挥自己的长处:内容作者不用顾忌最终格式而奋笔直书,设计者可以不了解内容作者的语句就组织和重排页面,程序员可以不妨碍页面的表现而开发脚本,为页面添加功能。CSS让每个人在互不干扰的情况下各司其职发挥所长。

如果CSS对于内容作者和设计者来说是小恩小惠的话,那么对开发者来说简直是天大的恩赐。从程序员的角度来看,把所有的布局和样式挪出来放到一个分离的CSS样式表中后,页面会变得非常简洁。文档树内的元素更少,嵌套深度也更低,编写与页面有关的交互脚本的难度也大幅度下降。

最后的赢家莫过于勤勤恳恳管理整个网站的站长(Web Master)了。移除表现性标记和分离样式表,可以组合通用的样式,并保持整站一致的观感。把默认字体从Arial修改为Helvetica不再需要编辑上千个HTML文档,现在只需在一个独立的样式文件中修改一行就能搞定。

CSS让Web开发者、站长和Web设计者都能遵从DRY原则:别自我重复(Don’t Repeat Yourself)。把通用的规则组合成独立的、可复用的文件,可以简化维护、更新和编辑。甚至最终用户也能从中得益,因为他们只需载入一次网站的样式规则,而不是每个页面都得重新下载一遍。更轻量的页面载入得更快,显示得更流畅。这真是一个共赢的局面。

最后,不要忽略了CSS对网络管理者和会计师的重要性。虽然每个页面中纯表现性的信息可能只占1KB到2KB,但对于上千个页面和百万之巨的用户,总量就会大得惊人。只载入一次样式,不用每个页面都载入额外的表现性标记,就可以节省带宽。ESPN在转向CSS标记时,每天节省了2TB的流量。从这个层次上说,这能转化为实实在在的收益,节省程度大致上是可以估量的。诚然,我们开发的绝大多数站点都不能跟ESPN相提并论,只能幻想着每天能先有2TB的日流量,而我们能够节省日流量则远远少于2TB。但不管怎么说,如果你正经历着流量超载,或是有希望被Digg首页推介而流量大涨,把样式挪到外部的CSS样式表中肯定会大有帮助。

REST(Representational State Transfer,表述性状态转移)是本书提出的出现最早但可能知道的人最少的重构目标。尽管本书侧重于HTML,但也不会忽视HTML的传输协议HTTP,REST是HTTP的架构。

理解HTTP和REST对如何设计Web应用程序非常重要。在页面中放置表单,或使用Ajax在JavaScript程序中传递数据时,使用的就是HTTP。正确使用HTTP可以开发健壮、安全和可扩展的应用程序。但使用不慎的话,你能祈祷的最好结果只是一个功能勉强可用的系统,但可能会发生的最坏情况是非常不妙的:Web Spider会删掉整个站点、购物中心在圣诞购物期间由于繁忙的业务而宕机或者搜索引擎无法索引导致用户无法找到你的网站。

尽管基本的静态HTML页面有着内在的REST方式,但大部分更为复杂的Web应用程序并不是。特别是在应用程序涉及以下常见的特性时,必须考虑使用REST:

人们很容易在这些特性上犯错,现在很多应用程序使用它们的方式也是错比对多。Web不是LAN,那种为少数而且有限C/S系统服务的技术,都不能扩展为可以容纳千万级用户的Web系统。基于会话和持久连接技术的C/S架构对Web并不适用。试图重建它们会败在扩展上,通常会导致损失惨重的后果。

使用HTTP实现的REST拥有一些关键的概念,这会在接下来的内容中讨论。

1. 所有的资源通过URL定位

使用不同的URL给不同的资源打上标签,让它可以被收藏、链接,被搜索引擎索引或印到广告牌上。以“访问http://www.example.com/foo/bar”的方式来寻找资源,相比必须“访问http://www.example.com/,在表单中键入bar,然后单击foo按钮”的方式,是不是更容易更直观呢?

别抵触URL,大部分资源都应该只通过URL定位。比如,客户记录应该有http://example.com/patroninfo/username这样一种URL而非http://example.com/patroninfo。就是说,每个客户都应该有一个独立的URL直接链接到他的记录(当然有密码保护),而不是所有客户都共享同一个URL,通过不同的登录cookie才能改变这个URL下的内容。

2. 通过GET进行诸如查询或浏览等安全无副作用的操作

Google只能索引通过GET访问的页面,用户只能收藏通过GET访问的页面,其他网站只能链接使用GET的页面。如果想提升网站的流量,应该让用户尽可能以GET方式访问网站。

3. 通过POST进行诸如购买或添加评论等非安全的操作

Web Spider按惯例在页面上抓取的是通过GET访问的链接,即使有时被告知是不应抓取的。用户在浏览器地址栏中键入URL,然后修改它们就能看到发生的结果。浏览器预抓取链接页面也是通过GET。如果存在如删除内容、签署合同或重新排序等通过GET执行的操作,有些程序某种程度上并不会征求真实用户的同意就开始执行,这有时会产生十分严重的后果。当Google发现并抓取“删除此页”之类的链接时,整个站点可能就会因此消失,这都是因为使用GET取代了POST。

4. 各个请求彼此独立

客户端和服务器端都可以有自己的状态,而且互不依赖对方。所有必要的信息都在每一次通信中传递。无状态让扩展得以通过缓存和代理服务器来实现,在有必要时同样也支持服务器群代替单台服务器。同样,服务器对相同客户端没有必要连续响应两次。

健壮、可扩展的Web应用程序应该使用HTTP而不是抵触HTTP。常见的C/S应用程序是可以做到的,REST风格的应用程序也可以照单全收,而且能做到充分的可扩展性。但是,这样的实现可能要求系统进行巨大的改动。尽管如此,如果你擅于解决扩展性问题,那么这就可以成为一项重要的重构技术。

从CEO到经理,他们都对HTML故作沉思,拒绝接受重构的提议。这种情况屡见不鲜,虽然他们有很多说法,但通常不外乎:

我们没有时间浪费在清理代码上。现在马上给我实现这个新特性!

对于这条意见我们可以做两种回应。第一,从长远来看,重构可以节省时间;第二,你拥有的时间实际上比想象的多。这两条都是正确的。

从长远来看,重构可以节省时间,但在短期内这也是可能的,因为整洁的代码更容易修复和维护。以岩石作为基础,比以流沙作为基础更为容易和坚固。跟模糊的代码相比,整洁的代码更易查错。实际上,当错误的来源并不明显时,重构通常可以重现问题。重构的过程不仅改变代码本身,也会改变我们对代码的看法,让我们能以前所未有的眼光看待老代码。

当然,为最大程度上节省时间,尽可能地自动化重构是非常重要的。这也是我要强调使用工具(如Tidy和TagSoup)以及基于正则表达式的简单解决方案等方式的原因。尽管一些重构需要花费很大的精力,例如把网站从表格布局转为CSS布局,但除此之外有很多东西都只需单击一下按钮——比如将静态的页面转化为良构的XHTML。很多重构在某种程度上是处于上述两种状态之间的。

对于重构的认知不足还表现在,大部分经理可以拿出更多安排表上计划的时间用于重构。编写新代码不仅不容易,而且还需要大段连续的时间。典型的工作日不是收电子邮件、接电话、开会,就是抽烟休息,就是不能给开发者提供足够长、连续工作的时间段。

反之,重构并不难。它并不需要大段的连续时间。不管是不同时间段加起来满60分钟的重构,还是一段连续60分钟的重构,它们的效果基本一致。60分钟连准备编写代码的时间都不够,而6分钟对开发来说,就更没有价值了。

同时也要考虑开发者的心境。开发者在某段时间的工作效率很高,但在另一段时间可能就不高,这是一个明显的事实。有时你能一下子写数百行代码,但有时你压根就不想碰键盘;有时你可以聚精会神于当前任务,但有时你会因牙痛、将要举行的客户会议和周末计划而分心。编码、设计和其他复杂的任务在分心时并不能做好。但是重构并不复杂,它是一件十分容易的事情。你可以步步为营、循序渐进,就算你处于工作效率不是那么高的时候也无所谓。

更重要的是,我发现当我处于低潮时,重构能使我加快进入状态。先热身,然后再全身而入,这是一种做事方法。先从简单的任务(比如重构)开始做起,让我能从精神上进入状态,进而能挑战更大的问题。

尽管去做

顺便说一句,重构并不是让你进入状态的唯一方式。推动你进入状态的有效方式有很多。编写测试、测量和改善代码覆盖、修正已知错误、使用静态代码分析甚至拼写检查,都可以助你进入状态让你在不适合完成主要任务的时候同样能做一些事。关键在于不要被任何单项的任务所阻滞。无论什么时候总有其他的事情(很可能不止一件)在等着你去做,有时候,只要找出在合适时间段完成的合适任务即可,不必为任务而赶时间。

重构充分体现了那句老俗话:学聪明点儿,别死用功。尽管这句话听起来像是陈词滥调,但用在这里确实很贴切。

cruft,是指软件开发中冗余、过时、不必要、不相关、低质的代码,本书中译者姑且将之译为烂码。——译者注

译者对真正的黑客表示歉意,在本书中,译为黑客是为了通俗易懂,专指搞破坏的 cracker,或许你可以称之为骇客。——译者注

指浏览器上的查看源代码(View Source)功能。——译者注

这里是作者的讽刺说法,自家公司开发的网站,自己都用不了,说明网站的可用性有问题。——译者注

superfecta,美国的一种博彩方式,指博彩者必须选定头4匹马确切名次的赛马赌博。——译者注

现在是Adobe Flash Player了。——译者注

黑客惯用一些奇怪单词,故意去掉某个字母,把字母o改成数字0,把z当s用等,这里故意将Owned写为Pwned表明被攻击了。——译者注

指只需少量鼠标单击就可以进行购物操作,在此模仿一站式购物的译法。——译者注


自动化工具是重构的重要组成部分。尽管可以使用文本编辑器手动执行重构,甚至有时出于演示的目的我也使用这种方式,但实际上绝大多数的时候我们都使用软件来辅助工作。据我所知,到本书写作时为止,还没有可用于HTML的主流的重构浏览器(refactoring browser)。但已经有很多工具可用在重构的过程中。本章会讲解一些工具。

自始至终,我都会在本书中向你展示一些非常强大的工具和技术。伟大的教育家Stan Lee告诉我们:“能力越大,责任越大。”使用这些技术必须对结果负责,不要让破坏变得不可弥补。我给你展示的工具中有些可能会“行为不端”,有些被弄糊涂时还有会出现边际效应。品质不良的HTML实在太多了,因此这里讨论的工具做不到面面俱到。因此,重构HTML至少需要5个步骤。

(1) 确定问题。

(2) 修复问题。

(3) 检验已修复的问题。

(4) 确保没有引入新问题。

(5) 部署解决方案。

错误在所难免,因此不要在任何一个在线的网站上使用这些技术。在做任何改动之前,先准备好一份网站的本地副本。完成本地副本的修改之后,仔细检查所有页面才进行部署。

现在许多大型网站都已经使用测试用机(staging server)或开发用机,在公开发布之前可以部署和检查内容。如果只是个人静态的小网站,直接复制一份到本地硬盘上进行修改就可以了。但无论如何都只在副本中工作,并在部署之前检查所有的变化。如何检查变化则是下一节要讲述的内容。

当然,就算是经过了最仔细的检查,还是难以避免一些错误,而这些错误由最终用户首先发现。有时网站在测试用机上运作得很好,但到了生产机中就有可能因为未知的配置区别,而碰到各种不可思议的问题。因此,对生产环境的网站进行整体备份,以便新部署的网站不能按预期运作时进行恢复,这是一个非常不错的做法。定期、可靠、经过测试的备份是不可或缺的。

最后,你应该非常谨慎地把包括HTML、CSS和图片的代码存储在源码控制系统中。程序员使用源码控制系统有数十年了,但Web开发者和设计者对它还相对陌生,现在是改变的时候了。网站越复杂,那些微乎其微的问题就越有可能被忽略。在重构时,能够恢复到前一个甚至是数月乃至数年之前的版本是非常关键的,这样才能找到是哪次改变引入了错误。源码控制同时提供以时间为标识的备份,因此可以还原到指定时间点的状态。

在Web开发中,我强烈推荐使用Subversion,很大程度上是因为它非常好地支持目录间文件的移动,并且对Unicode的出众支持和对二进制文件的得体支持也非常有帮助。大部分的源码控制系统在默认情况下都为程序员配置妥当,不用去关心文件在目录间的移动。但是Web开发者频繁地改变网站结构(实际上过于频繁了),因此系统必须能够追踪文件移动的历史。如果上级已经帮你配置了如CVS、Visual SourceSafe、ClearCase或者Perforce等其他源码控制系统,你大可随遇而安。但从长远看来,Subversion可能会做得更好,并且可以减少问题的引发。

如何管理Subversion这个主题可以写一本书,而实际上也已经有很多这样的书了(我喜欢的是Mike Mason著的Pragmatic Version Control Using Subversion[The Pragmatic Bookshelf,2006])。很多大型网站专门雇佣负责管理源码控制仓库的人。但先别担心,毕竟配置Subversion或其他源码控制仓库的难度,并不会超过配置Apache或其他Web服务器。只需阅读少量文档,修改某些配置文件,或者求助于新闻组,或者通过Google搜索就可以解决难题。这都是切实可行的,而且这些时间值得付出。

如果有必要,你可以使用命令行在Subversion中提交或获取文件。但如果能使用如BBEdit这种内置支持Subversion的编辑器,事情会变得更简单。如Dreamweaver这种本身不支持Subversion的编辑器也有插件支持Subversion。此外,像TortoiseSVN和SCPlugin这种有界面的产品,可以在Windows系统上的资源管理器或是Mac系统上的Finder上直接集成Subversion。

一些CMS(内容管理系统)也有内置的版本控制。如果你使用的CMS正好有版本控制,或许就没有使用外部仓库的必要了。比如,MediaWiki把所有页面的所有修改都存储起来,这样就可以查看指定页面在任何时间任何位置的状况,而且还能够还原。这是维基百科所安装的MediaWiki模块中很重要的一个,因为修改是一个很现实的问题。不过即便是不对外开放编辑权限的私有网站,也能在保存的完整的历史记录中得到好处。Wiki是Web中使用版本控制最普遍的一种,其他一些(如Siteline等)CMS也提供这样的功能。

即使没有人去遵守,这确实还是HTML的标准。判断网站是否遵守HTML标准的一种方法是,通过验证服务来运行页面。验证结果可以让人有所启发,为你提供修复的明确细节以及一个预估工作量的良好建议。

对于公开页面,可选的验证器是W的标记验证服务,位于http://validator.w3.org/上。只要输入需检查页面的URL,就可以看到结果了。图2-1是这个服务验证我博客的一个示例。

图2-1 W标记验证服务

看起来是我记错了blockquote元素的语法,错把cite当做source属性了。这个服务比我想象中的要好。我修正后重新检查的结果如图2-2所示,现在它是有效的了。

图2-2 想要的结果:有效的页面

通常没有必要检查网站的每个页面,因为大部分的错误都是重复的。一般来说,一旦你确定了某种错误,就有可能找到源头并自动修正问题。比如,上面的例子我只要简单地在其他页面搜索<blockquote source=就可以找出其他犯这个错误的地方。

这个页面现在比刚开始的要更清晰了。接着我检查了一个我所能找到最古老的、很少维护的页面,它产生了144个不同的错误。

私密验证

W诚然可信,但你并不想把内部的私密网页提交到W上。实际上,处理医学、财务和其他私人数据时,这样做可能是违法的。你可以从http://validator.w3.org/docs/install下载一份HTML验证器副本安装到本地,这样你的页面就不必为了验证而越出雷池了。

这种形式的验证对编写是有利的,对于网站的抽查以确定可能的工作量也很有用,即使是单一的页面。但在某种程度上,你必须检查网站的所有页面而不只是少数的几页。因此,自动化的批处理的验证器就显得很有必要了。

W标记验证服务促使了日志验证程序(Log Validator)的诞生(http://www.w3.org/QA/Tools/LogValidator/),它是一个用Perl写的命令行工具,用于检查整个网站。它还可以使用Web服务器的日志文件算出最流行的页面,并从这些页面开始分析。显然,与一年100次点击量的页面相比,你会更在意一个每分钟100次单击的页面。日志验证程序为你提供方便的待修复问题清单,并以页面的严重性和流行性排序。代码清单2-1显示了这样一份记录清单的开头部分。

代码清单2-1 日志验证程序的输出结果

Results for module HTMLValidator
****************************************************************
Here are the 10 most popular invalid document(s) that I could
find in the logs for www.elharo.com.

 Rank  Hits  #Error(s)                Address
------ ------ ----------- -------------------------------------
1    2738  21     http://www.elharo.com/blog/feed/atom/
2    1355  21     http://www.elharo.com/blog/feed/
3    1231  3      http://www.elharo.com/blog/
4    1127  6      http://www.elharo.com/
6    738  3      http://www.elharo.com/blog/networks /2006/03/18/looking-for-a-router/feed/
11   530  3      http://www.elharo.com/journal /fruitopia.html
20   340  1      http://www.elharo.com/blog /wp-comments-post.php
23   305  3      http://www.elharo.com/blog/birding /2006/03/15/birding-at-sd/
25   290  4      http://www.elharo.com/journal /fasttimes.html
26   274  1      http://www.elharo.com/journal/

这份输出清单中前两个页面属于Atom feed文档而非HTML文件,因此可以置之不理。但访问量排行第3和第4的页面就棘手了,因为它们分别是我的博客主页和网站主页,因此毫无疑问需要修复问题。因为访问量排行第5的页面是有效的,所以没有在这份清单中显示出来。访问量排行第11、25和26的页面是XML前的页面,不属于XHTML。它们无效并不奇怪,但因为它们仍有点击量,所以也是需要修复的。

访问量排行第20的页面也肯定有一处错误,不过那是用于发表评论的一个脚本罢了。当验证器试图对它使用方法GET而不是POST时,它接收到的只能是一个空白页面。虽然不是一个大问题,但或许我需要给GET一个完整的评论清单来修复它。或者我更应该简单地配置这个脚本返回HTTP error 405 Method Not Allowed,而不是依赖于200 OK和一个空白文档。

之后就是那些流量不是很大的各个页面了。从顶部开始,并顺路往下开始工作吧。

你也可以使用通用的XML验证器,比如xmllint,它在很多UNIX机器上都默认安装,也可以在Windows平台上使用。它是libxml2的组成部分,可以从http://xmlsoft.org/上下载。

使用通用的XML验证器检查HTML既有优点,也有缺点。优点是可以把良构检查与有效性检查相互分离。通常,先修复良构问题,再去修复有效性问题更容易。实际上这也是本书的组织顺序。良构比有效性更重要。

使用普通XML验证器的第一个不足是,它不会捕捉HTML特定的问题,这些问题在DTD中没有规定得特别清楚。比如,它不会注意到a元素嵌套到另一个a元素的问题(尽管在实践中这个问题不是很常见)。第二个不足是它必须实际地读取DTD,不会设想它所检查文档的任何东西。

使用xmllint检查良构是非常简单的事,只需在命令行中指出需要检查的本地文件的地址或远程文件的URL。使用--noout选项指定不打印文档自身,而使用--loaddtd则允许解析实体引用。比如:

$ xmllint --noout --loaddtd http://www.aw.com
http://www.aw-bc.com/:118: parser error : Specification mandate
value for attribute nowrap
<TD class="headerBg" bgcolor="#99" nowrap align="left">
                        ^
http://www.aw-bc.com/:118: parser error :
attributes construct error
<TD class="headerBg" bgcolor="#99" nowrap align="left">
                        ^
http://www.aw-bc.com/:118: parser error : Couldn't find end of
Start Tag TD line 118
<TD class="headerBg" bgcolor="#99" nowrap align="left">
                        ^
http://www.aw-bc.com/:120: parser error : Opening and ending
tag mismatch: IMG line 120 and A
 Benjamin Cummings" WIDTH="84" HEIGHT="64" HSPACE="0"
VSPACE="0" BORDER="0"></A>
...

在第一次运行这样的报表时,错误信息的数量往往使人气馁。千万不要认输——从头开始,逐一解决。大部分的错误都可归类为某种常见的分类中,本书稍后会讨论到,所以你可以一起解决它们。比如在这个例子中,第一个错误是nowrap属性缺少相应的值。你可以通过搜索nowrap并替换为nowrap="nowrap"轻易搞定。实际上通过多文件的搜索和替换,修复整个网站也不过5分钟。(本章后面我会给出更具体的细节。)

修改完后再次运行验证器。每进行一次修改,你看到的错误应该会更少,尽管偶尔会出现新的错误。简单地重复这个过程,直到消灭所有的良构错误。接下来的问题是,img元素使用的是起始标签(start-tag)而不是空元素标签(empty-element tag)。虽然这是个麻烦的标签,但也可以通过搜索BORDER="0">并替换为border="0"/>修复大部分问题。虽然不能找出img元素的所有问题,但确实可以修复大量类似的问题。

从清单中的第一个错误而不是随机挑一个错误着手修复是非常重要的。通常一个先出现的错误会导致更多的良构问题,忽略起始标签或结束标签引起的问题更是如此。修复先出现的错误往往可以免除后续的错误。

实现良构后,接着是检查有效性。只需在命令行加上--valid选项,比如:

$xmllint –noout –loaddtd –valid valid_aw.html

这很可能会显示更多需要检查和修复的错误,尽管通常都是些小问题。但基本的解决方法还是一样的:从顶端开始顺路而下,直到解决所有的问题。

很多HTML编辑器都有内置的页面验证支持。比如,在BBEdit中,你可以到Markup菜单项上选择Check/Document Syntax来验证正在编辑的页面;在Dreamweaver中,可以使用右键菜单中的Validate Current Document选项(不过要确保验证器设置为XHTML而非HTML)。从本质上说,这些工具只不过是通过如xmllint这样的解析器来检查文档是否有问题。

如果你使用的是Firefox,应该安装Chris Pederick的Web Developer插件(https://addons.mozilla.org/firefox/60/)。这样,你就可以通过Tools/Web Developer/Tools/Validate HTML菜单项快速验证任何页面了,它会把当前页面载入W验证器中。另外这个插件还提供了大量的有用选项。

无论你使用哪一种工具和技术来搜索标记错误,验证都是向XHTML重构的第一步。如果能看到问题之所在,那么问题已经解决了一半。

从理论上讲,重构不应破坏任何未曾破坏的代码,然而在实践中并不是那样。在某种程度上,本书后面会向你展示哪些改动是安全的。但无论是人还是工具都会犯错误,重构也有可能引入新错误,所以重构过程就需要一个良好的自动化测试套件(suite)。进行任何重构之后,你更希望可以通过单击一个按钮和扫视一眼就能看到是否有代码被破坏了。

尽管测试驱动开发(test-driven development)在传统程序员中取得了巨大的成功,但在Web开发者中还是不太常见,尤其对前端开发者而言。实际上,网站的任何自动化测试都可能只是特例而不是规定,尤其是在对待HTML上。现在是让Web开发者开始编写和运行测试套件,以及使用测试驱动开发的时候了。

测试驱动开发的基本要点如下所述。

(1) 给某个功能写测试。

(2) 编写最简单的尽可能运作的代码。

(3) 运行所有测试。

(4) 如果测试通过,跳到第(1)点。

(5) 否则,跳到第(2)点。

为达到重构目的,这个过程尽可能地自动化是非常重要的。特别是:

为Web应用程序编写测试比为传统应用程序编写测试要困难得多。部分原因是Web应用程序的测试工具并没有传统应用程序的工具那么成熟,也因为涉及呈现而且要指出呈现是否正确,对计算机来说这是非常困难的(对人来说是非常容易的,但我们的目标是把人类从重复中解救出来)。因此Web测试或许达不到像Java或.NET程序测试那样的覆盖面。尽管如此,有测试总比没测试好,而且实际上你可以进行更多的测试。

你会发现,向Web标准(比如XHTML)重构代码能简化测试。更进一步说,为良构和有效的XHTML页面写测试比为乱构的(malformed)页面写测试容易得多。这是因为与乱构的页面相比,良构页面更方便处理代码。这样检查浏览器的呈现结果也更容易,因为浏览器对良构的页面一视同仁,但对乱构页面的处理各有千秋。因此重构的一大益处就是提升了可测试性,也让测试驱动开发成为可能。实际上,由于大量网站未曾经过测试,所以在进行下一步之前,你可能得先进行足够的重构才可能进行测试。

测试网页的工具很多,有合适的,也有糟糕的,有免费的,也有昂贵的。有些是为程序员设计的,有些是为Web开发者准备的,也有些是给业务领域的专家使用的。它们包括:

实际上,这些工具具备的先进性,有助于富有经验的敏捷程序员(agile programmer)开发少量的初始测试和测试框架。一旦有了适当的自动化测试套件,增加更多的测试就易如反掌了。

JUnit(http://www.junit.org/)是Java框架的标准单元测试框架,也是其他大量特定框架(比如HtmlUnit和HttpUnit)的基石。说它不能用来测试Web应用程序是没有理由的,你把Java代码当做是浏览器的网页代码就好了,实际上也没有想象中那么困难。

举个例子,最常见的基本测试之一就是检测网站的每一个页面是否良构。你当然可以只通过XML解析器处理页面,并观察是否抛出异常来达到。但对于网站的每一个页面,如果只需写一个通用的测试方法,你就可以不用前面说到的手工检查,从而实现测试的自动化了。

代码清单2-2演示了一个简单的JUnit测试,用来检查我的博客的良构性。它所做的就是把URL交给XML解析器,并观察它是否会出错。如果没有出错,则通过了测试。这段代码要求classpath中有Sun JDK 1.5或以上版本和JUnit 3.8或以上版本。在其他的环境中,可能需要稍做修改才能运行。

代码清单2-2 一个用于检查网站良构的JUnit测试

import java.io.IOException;
import junit.framework.TestCase;
import org.xml.sax.*;
import org.xml.sax.helpers.XMLReaderFactory;

public class WellformednessTests extends TestCase {

  private XMLReader reader;

  public void setUp() throws SAXException {
    reader = XMLReaderFactory.createXMLReader(
    "com.sun.org.apache.xerces.internal.parsers.SAXParser");
  }

  public void testBlogIndex()
   throws SAXException, IOException {
    reader.parse("http://www.elharo.com/blog/");
  }

}

你可以在如Eclipse或NetBeans等IDE内执行这个测试,或是使用下面的命令行来运行:

$ java -cp .:junit.jar
 junit.swingui.TestRunner WellformednessTests

如果通过了所有的测试,你会看到如图2-3所示的绿色条栏。

图2-3 通过了所有测试

要测试更多页面的良构性,就要添加更多的方法,其中每个方法都跟testBlogIndex方法基本相同,只是URL不同而已。当然你还可以编写更复杂的测试,也可以通过为解析器设置http://xml.org/sax/features/validation特性测试有效性,然后加上错误处理程序以便在碰到错误的时候抛出异常。

你可以使用DOM、XOM(XML对象模型)、SAX或者其他一些API载入页面并检查它的内容,比如可以编写测试来检验页面的所有链接是否都是可用的。如果你使用TagSoup作为解析器,还可以为非良构的HTML页面编写这类测试。

你还可以使用HttpURLConnection类来提交表单,也可以使用Java 6内建的Rhino引擎来运行JavaScript。这都是一些非常底层的内容,虽然有一定难度,但绝对值得去做,卷起袖子开工吧。

如果开发者不对网站做常规的改动,你可以配置定期运行的测试套件,这样在错误不意出现的时候能给你发送电子邮件。(期望每一个编写者或设计者在提交之前都运行全部的测试套件或许是不现实的。)你也可以使用如Hudson或Cruise Control之类的产品持续运行套件。但这也有可能会使你的日志充满了大量的测试内容,因此你可能认为在开发服务器上运行这些测试更为合适。

其他语言和平台也有很多类似的测试框架,如Python有PyUnit、C++有CppUnit、.NET有NUnit等。使用标准的xUnit形式是它们的共同点之一。无论哪一个,只要你和你的团队用得舒心,那对编写Web测试套件来说就是好的。Web服务器并不关心使用什么语言来编写测试。有了一键测试的装备和足够的支持编写测试的HTTP客户端后,你就可以随心所欲了。

HtmlUnit(http://htmlunit.sourceforge.net/)是一个用于测试HTML页面的开源JUnit扩展。对于使用过JUnit测试驱动开发的Java程序员来说,应该很熟悉和很顺心。跟纯粹的JUnit相比,HtmlUnit主要的优势有两个。

例如,HtmlUnit在客户端返回页面之前会运行由onLoad处理程序指定的JavaScript,跟浏览器的执行行为一致。如果只是像代码清单2-2那样简单地使用XML解析器载入页面,那是不会运行JavaScript的。

代码清单2-3演示了HtmlUnit的应用,它检查页面的所有链接是否可用。其实我也可以使用原始的解析器和DOM来实现,但复杂性多少会有所增加。特别是,使用如getAnchors这样的方法搜索页面所有a元素是非常有用的。

代码清单2-3 检查页面链接的HtmlUnit测试

import java.io.IOException;
import java.net.*;
import java.util.*;
import com.gargoylesoftware.htmlunit.*;
import com.gargoylesoftware.htmlunit.html.*;

import junit.framework.TestCase;

public class LinkCheckTest extends TestCase {

  public void testBlogIndex()
   throws FailingHttpStatusCodeException, IOException {
    WebClient webClient = new WebClient();
    URL url = new URL("http://www.elharo.com/blog/");
    HtmlPage page = (HtmlPage) webClient.getPage(url);
    List links = page.getAnchors();
    Iterator iterator = links.iterator();
    while (iterator.hasNext()) {
      HtmlAnchor link = (HtmlAnchor) iterator.next();
      URL u = new URL(link.getHrefAttribute());
      // Check that we can download this page.
      // If we can't, getPage throws an exception and
      // the test fails.
      webClient.getPage(u);
    }
  }

}

这个测试不仅仅是单元测试而已。它检查页面的所有链接,而真正的单元测试可能只检查一个。进一步说,它跟外部服务器之间产生了连接,这对单元测试来说是不常见的。无论怎么说,这是一个值得尝试的测试,它让我们了解到,如果一个外部网站由于重整页面而导致链接失效,那么就该修复页面了。

HttpUnit(http://httpunit.sourceforge.net/)也是一个开源JUnit扩展,用于测试HTML页面。同样,对于使用过JUnit进行测试驱动开发的Java程序员来说,HttpUnit也是最合适的,并且它在很多方面都跟HtmlUnit非常相近。有些程序员喜欢HttpUnit,但有些喜欢HtmlUnit。如果真要找出它们之间的区别,那就是HttpUnit偏底层些,它更倾向于原始的HTTP连接,而HtmlUnit更像浏览器。如果需要考虑JavaScript的测试,HtmlUnit的支持要好一些。当然,两者的重叠还是很多的。

代码清单2-4演示了HttpUnit测试,用于验证页面中H1标头的唯一性,以及其文本是否跟网页的标题匹配。虽然并不是所有页面都必须这样,但也存在必须一致的情形。例如对于新闻报纸类网站来说,这是十分聪明的做法。

代码清单2-4 检查标题和唯一的H1标头是否匹配的HttpUnit测试

import java.io.IOException;
import org.xml.sax.SAXException;
import com.meterware.httpunit.*;
import junit.framework.TestCase;

public class TitleChecker extends TestCase {

  public void testFormSubmission()
   throws IOException, SAXException {

    WebConversation wc = new WebConversation();
    WebResponse   wr = wc.getResponse(
     "http://www.elharo.com/blog/");
    HTMLElement[]  h1 = wr.getElementsWithName("h1");
    assertEquals(1, h1.length);
    String title = wr.getTitle();
    assertEquals(title, h1[0].getText());

  }
}

也可以使用HtmlUnit来做到,代码清单2-3中的例子自然也可以使用HttpUnit来编写。如何抉择很大程度上取决于个人偏好。当然,这类框架不止这两个。还有更多其他的测试框架,包括不是用Java编写的。用你所爱,但无论怎样,一定要选择一个。

JWebUnit是位于HtmlUnit和JUnit之上的高层API。通常来说,JWebUnit测试涉及更多的断言(assertion)和更少直接的Java代码。因此,这些测试在一定程度上更容易编写,也无需太多的Java经验,并且对于Web开发者来说或许更方便使用。此外,测试可以随着你单击链接、提交表单并在一个Web应用程序的完整路径中进行,在多个页面中也非常容易扩展。

代码清单2-5演示了一个JWebUnit测试,用于检查网站的搜索引擎。它会填写主页的表单并提交,然后检查预期的结果是否会出现。

代码清单2-5 提交表单的JWebUnit测试

import junit.framework.TestCase;
import net.sourceforge.jwebunit.junit.*;

public class LinkChecker extends TestCase {

  private WebTester tester;

  public LinkChecker(String name) {
    super(name);
    tester = new WebTester();
    tester.getTestContext().setBaseUrl(
     "http://www.elharo.com/");
  }

  public void testFormSubmission() {

    // start at this page
    tester.beginAt("/blog/");

    // check that the form we want is on the page
    tester.assertFormPresent("searchform");

    /// check that the input element we expect is present
    tester.assertFormElementPresent("s");

    // type something into the input element
    tester.setTextField("s", "Linux");

    // send the form
    tester.submit();

    // we're now on a different page; check that the
    // text on that page is as expected.
    tester.assertTextPresent("Windows Vista");
  }

}

FitNesse(http://fitnesse.org/)其实是一个Wiki,它的目的是让业务用户能够以表格的形式编写测试。业务用户喜欢用电子表格。FitNesse的基本概念是,测试可以使用类似电子表格的形式来编写。因此,它不用使用Java,而是在Wiki中用表格来编写FitNesse测试。

为网站安装和配置FitNesse确实需要程序员。一旦运行起来并编写了一些夹具(fixture)样例后,有领悟能力的业务用户就有可能编写更多的测试。FitNesse也非常适合于一个协作的坏境中,程序员和商业用户可以一起定义业务规则和编写测试。

对于Web应用程序的验收测试,可以安装Joseph Bergin的HtmlFixture(http://fitnesse.org/FitNesse.HtmlFixture)。它也是基于HtmlUnit的,它提供的说明非常有用,可以作为Web应用程序的测试指导,比如填写表单、提交表单和检查页面文本等。

代码清单2-6演示了一个简单的FitNesse测试,用于检查head中的http-equiv元标签是否正确指定为UTF-8。代码前3行设置了类路径,第4行为空行,第5行确定夹具类型是HtmlFixture。(测试Web应用程序当然还有更多其他类型,但HtmlFixture是最常见的。)

然后载入一个来自http://www.elharo.com/blog/的外部页面。在这个页面中,我们把注意力集中到一个名为metaid值为charset的元素上。这就是我们测试的主题。

这个测试接着检查这个元素的两个属性。首先检查content属性,并断言它的值是text/html;charset=utf-8。接着检查meta元素的http-equiv属性,并断言它的值是content-Type

代码清单2-6 对<metaname="charset"http-equiv="Content-Type"
content="text/html;charset=UTF-8"/>的FitNesse测试

!path fitnesse.jar
!path htmlunit-1.5/lib/­*.jar
!path htmlfixture20050422.jar

!|com.jbergin.HtmlFixture|
|http://www.elharo.com/blog/|
|Element Focus|charset  |meta|
|Attribute   |content  |text/html; charset=utf-8|
|Attribute   |http-equiv |content-type|

这个测试会嵌入到一个Wiki页面中。你可以按如图2-4所示通过在浏览器中单击测试按钮来运行它。如果所有的断言都通过并且没有其他的错误,测试会在运行完毕后显示为绿色,否则会显示为粉红色。你还可以使用页面的其他Wiki标记来描述测试。

图2-4 一个FitNesse页面

Selenium是一个开源的基于浏览器的测试工具,跟单元测试相比更侧重于功能测试和验收测试。与HttpUnit和HtmlUnit不同,Selenium测试直接在浏览器内运行。被测试的页面被嵌入到iframe中,而且Selenium的测试代码是使用JavaScript编写的。尽管用于编写测试的IDE只能在Firefox上使用,但它基本上是跨浏览器和跨平台的,运行界面如图2-5所示。

图2-5 Selenium IDE

尽管在Selenium中可以远程控制手工编写的测试,但它确实是一个偏传统的GUI记录和回放工具。这对于已经编写好但又不适用于测试驱动开发的应用程序是非常合适的。

对于习惯使用JavaScript和HTML的前端开发者来说,Selenium可能用得更舒心。对专业测试员来说也可能更适合,因为它跟一些常用的客户端GUI测试工具非常相似。

代码清单2-7演示了Selenium测试,用于验证在Google中搜索“Elliotte”的结果中,www.elharo.com是否出现在第一页上。把这个脚本录入Selenium IDE中并作了少量的手工编辑后,你就可以在浏览器中载入和运行了。跟上述给出的例子不同,这既不是Java代码,也不需要很多的编程技巧来维护。Selenium更像是宏(macro)语言而不是编程语言。

代码清单2-7 测试elharo.com是否排在“Elliotte”搜索结果的前列

<html>
<head>
<meta http-equiv="Content-Type"
    content="text/html; charset=UTF-8">
<title>elharo.com is a top search results for Elliotte</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">New Test</td></tr>
</thead><tbody>
<tr>
    <td>open</td>
    <td>/</td>
    <td></td>
</tr>
<tr>
    <td>type</td>
    <td>q</td>
    <td>elliotte</td>
</tr>
<tr>
    <td>clickAndWait</td>
    <td>btnG</td>
    <td></td>
</tr>
<tr>
    <td>verifyTextPresent</td>
    <td>www.elharo.com/</td>
    <td></td>
</tr>

</tbody></table>
</body>
</html>

很明显,代码清单2-7是一个真正的HTML文档,可以在Firefox中用Selenium IDE打开它并运行测试。因为这个测试直接在浏览器内运行,所以Selenium能帮助你找出在某个浏览器中才会出现的错误。给浏览器CSS、HTML和JavaScript更广泛的支持是非常有用的,但HtmlUnit、HttpUnit和JWebUnit之类的测试框架使用的是它们自身的JavaScript引擎,跟浏览器的引擎行为并不总能保持一致。Selenium使用的不是外在的模仿而是浏览器本身真正的引擎。

这个IDE还能将测试导出为C#、Java、Perl、Python或是Ruby代码,因此可以把Selenium测试集成到其他环境中去,这对测试的自动化特别重要。代码清单2-8展示了与代码清单2-7相同的测试,只不过它是使用Ruby编写的。不过对于直接在浏览器中运行测试发生的跨浏览器错误,这种方式是捕获不了的。

代码清单2-8 自动化测试“Elliotte”搜索结果中elharo.com是否在排在前列

require "selenium"
require "test/unit"

class GoogleSearch < Test::Unit::TestCase
 def setup
  @verification_errors = []
  if $selenium
   @selenium = $selenium
  else
   @selenium = Selenium::SeleneseInterpreter.new("localhost",
    4444, *firefox", "http://localhost:4444", 10000);
   @selenium.start
  end
  @selenium.set_context("test_google_search", "info")
 end

 def teardown
  @selenium.stop unless $selenium
  assert_equal [], @verification_errors
 end

 def test_google_search
  @selenium.open "/"
  @selenium.type "q", "elliotte"
  @selenium.click "btnG"
  @selenium.wait_for_page_to_load "30000"
  begin
    assert @selenium.is_text_present("www.elharo.com/")
  rescue Test::Unit::AssertionFailedError
    @verification_errors << $!
  end
 end
end

因为你正在重构,所以我假设你已经有了一个网站或应用程序。如果跟我所看到的大部分情况一样,这个网站或应用程序的前端测试非常有限,但别因这种情况泄气。拿起你喜欢的工具,为一些基本功能编写少量的测试。即使少,有测试也总比没有好。早期的测试是线性的。你编写的测试明显改善了代码覆盖率和质量。不过千万别认为必须测试所有的代码。如果能进行测试,那当然好,不能就算了,因为还有其他的事情可做。

在重构网站的一个具体的页面、子目录或路径之前,可以花一个小时为该部分编写两三个测试。如果没有别的,这都是些冒烟测试(smoke test),它们让你可以了解建模(mock up)是否完毕。等有了充分的时间后再对测试进行扩展。

如果找到错误,在修复前一定先为它编写一个测试。这能帮你掌握错误修复的时间,并防止在做了其他修改后,这个错误又不经意地出现。因为前端测试并非一元的,除了一些错误代码的具体之处外,该测试很有可能还间接测试到其他地方。

最后,除重构之外的新特性和新开发,先想方设法编写一些测试。这保证网站新部分能进行测试,并且通常也会把旧页面和旧脚本的问题暴露出来。

自动化测试对开发健壮、可扩展的应用程序是至关重要的。开发测试套件可能一开始让人灰心丧气,但值得去做。一旦你把第一个测试框架配置好了,把第一个测试也写好了,接下来的测试就容易多了。就像可以通过小型、自动化的重构线性地改善网站一样,你也可以通过每周添加一个测试来改善测试套件。比你预料的要早的是,你会拥有一个坚固的测试套件,它会告诉你在出现问题时需要修复的东西,帮你保证网站的可靠性。

手工检查或者改动一个网站的每一个文件,即使是一个小型网站的少量文件,也是一件十分乏味和费劲的事情。让计算机搜索错误,可以的话同时自动修复错误,这显然是更高效的做法。很多工具都支持这种方式,包括诸如grepegrepsed等命令行工具,诸如jEdit、BBEdit、TextPad和PSPad等文本编辑器,当然也包括诸如Java、Perl和PHP等编程语言。所有这些工具都提供了一门专门的搜索语法规则,被称为正则表达式。尽管在不同的工具中它的表现略有不同,但基本语法几乎是一致的。

为达到说明的目的,本节中我将使用jEdit文本编辑器作为搜索和替换工具。选择它的原因在于它提供非常多适合你的特性,GUI也非常合理,而且还是开源的。由于它是用Java编写的,所以本质上说能够运行于任何平台上。你可以从http://jedit.org/上下载jEdit文本编辑器的副本。

然而,这里我展示的技术当然不会只局限于一个编辑器上。我在工作中常用到的是界面稍为优美的BBEdit,但它只能运行在Mac上。当然还有很多其他的选择,如果你喜欢某一种工具,那就用它吧。其实你需要的是:

满足上述条件的工具应该就够用了。

正则表达式的第一个目的是查找所有可能是错误的东西。比如,最近我发现在一个文件中,我老是把2006拼错为20066,而且这个错误可能不止一处,所以我需要通过搜索该字符串来检查这个错误。

在jEdit中,你可以使用Directory菜单中的Search/Search执行一个多文件搜索。选中这个菜单项后出现了如图2-6所示的对话框,图中示例有所调整。

图2-6 jEdit多文件搜索

幸好这个特定的问题看起来已经解决掉了,但我还发现了另一个更严重的问题。由于一些未知因素,我在其中一个网站中的链接中使用了双等号,如下所示:

<a href=="../../index.html">Cafe au Lait</a>

因此,链接无论在哪儿都是失效的。第一步是要找出涉及此问题的所有文件。在这个例子里,错误的字符串是常量,并且在正确的文本中出现的可能性也很小,所以非常容易进行搜索。图2-7的HyperSearch结果显示,这个问题在476个文件中出现了4475次。

图2-7 jEdit搜索结果

在错误不多的情况下,你只用单击每个错误以打开文件,并手工修复就行了。有时这也是必需的,甚至是最简单的解决方案。但是在错误量成千上万时,你就有必要使用工具来修复了。在上面的例子中,解决方法是不言自明的:在Replace with文本框中输入href=,再单击Replace all按钮就行了。

执行此类操作时必须十分小心,否则小错也会酿成大祸。错误的搜索和替换可能是错误问题的根源。在执行整站的操作之前,你应该先在少量的文件上进行搜索和替换,对你的正则表达式测试一番。

更重要的是务必在网站的备份副本上进行操作,在每次更改后务必运行测试套件,并在文件被改动的情况下当场核对,以保证正确性。如果发生了某些错误,编辑器的撤销功能将会非常有用。但并不是所有的编辑器都能有一个足够大的缓冲区(buffer)支持多重撤销,所以处理不了上千次的改动。假如编辑器不支持多重撤销,马上删除正在编辑的副本并用一个原始的副本进行替换,以防止搜索出错。跟其他复杂的代码一样,有时你不得不多花点时间对正则表达式进行排错。

通常你不关心搜索的具体内容,但必须了解它的通用模式。举例来说,要查找刚过去的几年的公元表示法,实际上是要搜索以200开头的四位数字。查找name=value这种形式的属性对时,并不能保证是name=valuename='value'还是name="value"格式。搜索所有不管是否有属性的<p>起始标签。这些都是使用正则表达式的绝佳场合。

在正则表达式中,特定的字符和模式可以表示其他的字符集合。比如,\d表示任意的数字。因此要搜索2000年到2009年中的任意一年,可以使用200\d这样的正则表达式,它能匹配2000、2001、2002等一直到2009这些年份。

但是200\d这个正则表达式也会匹配12000、200032、12320056或其他可能不是年份的字符串。(准确地说,它匹配符合200\d的字符串子串,而不是整个字符串)。因此,你可能希望指明需要匹配前后都有空白的字符串。元字符\s匹配空白,所以现在我们可以将正则表达式重写为\s200\d\s,这样才可以实现只匹配看起来是本世纪头十年中年份的字符串。

当然这还是不能保证匹配到的字符串形式就一定是年份,它也有可能是价格、人数、分数、电影标题等。你需要看一下匹配列表,检查是否是你想要的结果。尤其是在如此简单的情况下,误报的情况需要加以注意。当然,更进一步地完善正则表达式可以避免误报,或者手工删除偶然的匹配,都是可行的。

这里有更多的方法。比如我们在这个搜索中可以使用\b200\d\b。元字符\b匹配单词的开头或结尾,完全不会选中任何字符。这就可以避免需要选中单词前后的空白,也可以识别在句子末尾的诸如“This is 2008.”中的年份。但是它不能区别是英文句号还是数学中的小数点,所以也会匹配到“in 中的2005。

你可以简单地使用或操作符|来分隔年份,比如:

2000|2001|2002|2003|2004|2005|2006|2007|2008|2009

但单词分界问题还是避免不了。

有时你得暂时停止使用搜索。特别是由CMS、模板页面或者其他程序自动生成的内容时,搜索只能用来找错,定位程序在哪里产生了错误的标记。然后必须改正程序错误才能生成正确的标记。在这种情况下,你就不必太担心误报了,因为所有的更改都是手工执行的。这种搜索只找错不修错。

如果你没有停止使用搜索,并且继续替换,这需要谨慎行事。正则表达式比本书中的例子更棘手,涉及HTML就更不用说有多么狡猾了。不管怎么说,对于清理HTML它都是极有价值的工具。

注解


如果你并没有太多的正则表达式经验,请参考附录A中更多的例子。同时我推荐Jeffrey E. F. Friedl的Mastering Regular Expressions,3rd Edition(O’Reilly,2006)。

正则表达式对于独立的自定义的修改来说确实不错,但对于大量的修改来说难免乏味和困难。特别是,它们更大程度上是用来处理纯文本的,而不是处理半结构化的HTML文本的。为了批量改变和自动修复常见的错误,需要有能识别HTML标记的工具。第一个这样的工具是Dave Raggett的Tidy(http://www.w3.org/People/Raggett/tidy/),它是一个原创的HTML修复器。Tidy作为一个简单的多平台的命令行工具,可以用来修正大量的HTML错误。

为了达到本书的目的,你需要使用-asxhtml命令行选项。例如,这个命令会把index.html转化成一个良构的XHTML并把结果保存到原文件上(-m选项的作用)。

$tidy –asxhtml –m index.html

坦白地说,你有可能做出比在所有HTML文件上运行一遍Tidy就结束更糟糕的事,请不要停止继续往下阅读。Tidy的有些选项能够更好地改善代码,但也有处理不了或者处理不当的地方。比如,当在一个我至少5年没动过的页面上使用-asxhtml命令时,产生的错误信息如下所示:

line 1 column 1 - Warning: missing <!DOCTYPE> declaration
line 7 column 1 - Warning: <body> attribute "bgcolor" has
invalid value "#fffffff"
line 16 column 2 - Warning: <table> lacks "summary" attribute
line 230 column 1 - Warning: <table> lacks "summary" attribute
line 14 column 91 - Warning: trimming empty <p>
Info: Document content looks like XHTML 1.0 Transitional
5 warnings, 0 errors were found!

这些问题Tidy通常处理不了。它本应该提供DOCTYPE,因为我指定了XHTML这个已知的DOCTYPE模式。它也不知道该如何处理bgcolor="#fffffff",这个问题源于一个应当删除的多余的f,或者说整个bgcolor属性应该删掉并使用CSS替换。

提示


一旦发现了类似的问题,那么这个问题完全有可能出现在多个文档中。一个文件中出现的问题,就有必要在整个目录树中进行搜索和替换,不要放过任何一个可能出现的地方。多用代码的复制和粘贴,有些错误就只会出现一次。

后面的两个问题是缺少summary属性的table。这是一个关于可访问性的问题,你应该修复它。Tidy实际上还会打印出更多的细节:

The table summary attribute should be used to describe
the table structure. It is very helpful for people using
non-visual browsers. The scope and headers attributes for
table cells are useful for specifying which headers apply
to each table cell, enabling non-visual browsers to provide
a meaningful context for each cell.

For further advice on how to make your pages accessible
see http://www.w3.org/WAI/GL. You may also want to try
"http://www.cast.org/bobby/" which is a free Web-based
service for checking URLs for accessibility.

表格摘要当然是好东西,并且你应该加上它。但Tidy本身不能帮你做摘要,你需要自己来。

最后一个警告信息是,Tidy发现一个空的段落元素而且将它抛弃。这个常见问题可能是骗人的,绝对需要再检查一遍。在这种情况下(你将看到的大部分情况也是如此),它指的是<p>标签被当做结束标签而不是起始标签来使用。就是说,像这样的标记:

Blah blah blah<P>
Blah blah blah<P>
Blah blah blah<P>

Tidy看成了这样:

Blah blah blah
<P>Blah blah blah</P>
<P>Blah blah blah</P>
<P></P>

所以它把最后的空段落元素丢弃了。但几乎可以肯定的是,你需要的代码是这样的:

<P>Blah blah blah</P>
<P>Blah blah blah</P>
<P>Blah blah blah</P>

这种问题很难搜索和替换,尽管XHTML严格的验证会至少警告你问题所发生的文件。你可以使用XSLT(稍后会讨论)来修正部分问题,但是问题不是太多的话,手工编辑这些文件会更安全,而且也不见得很繁重。

如果指定--enclose-text yes选项,Tidy会把所有未闭合的文本包裹到一个p元素中。比如:

$tidy –asxhtml --enclose-text yes example.html

Tidy还能提醒你其他一些需要手工修复的严重问题,包括:

另一个重要的选项是-clean。它把如ifont等废弃的表现性元素替换为CSS标记。比如当我在相同的文档上使用-clean时,Tidy为我的文档添加下面这些CSS规则:

<style type="text/css">
/*<![CDATA[*/
 body {
 background-color: #fffffff;
 color: #000000;
 }
 p.c2 {text-align: center}
 h1.c1 {text-align: center}
/*]]>*/
</style>

同时也为之前使用了center的元素加上了必要的class属性。比如:

<h1 class="c1">Java Virtual Machines</h1>

Tidy处理CSS只能做到这一步,但你还是可以重新审视这些文件,看是否能提取一个通用的外部文件供网站文档共享,而不是在每个单独的页面都引入这些CSS规则。更进一步说,你应该考虑使用一些语义更清楚的类名称取代Tidy那些多少有些平凡的默认名称。

比如,有时我使用i元素来指明我谈论而不是使用某个词,比如:

<p><i>May</i> can be very ambiguous in English, meaning might,
can, or allowed, depending on context.</p>

这里斜体并不表示强调,所以使用em来替换并不合适。相反应该替换为一个带classspan,像这样:

<p><span class='wordasword'>May</span> can be very ambiguous
in English, meaning might, can, or allowed, depending on
context.</p>

接着,为样式表添加一条CSS规则,使它变为斜体样式:

span.wordasword { font-style: italic; }

虽然Tidy不可能聪明到这个地步,它需要你的帮忙才行,但它终究是一个不错的开始。

令人吃惊的是,Tidy不善于检测HTML文档的字符集编码,尽管大部分HTML文档相对丰富的元数据都精确地指出了编码。如果你的内容不是ASCII或ISO-8859-1(Latin-1),你最好使用--input-encoding选项告诉Tidy文档的编码。比如要把文档保存为UTF-8编码,可以这样运行Tidy:

$tidy –asxhtml --input-encoding utf8 index.html

不指定编码的话,Tidy默认生成的是ASCII文本。如果可以,它会把非ASCII字符转义为指定的实体符号,否则转为数字字符。虽然Tidy支持多种常见的编码,但我只推荐UTF-8。使用--output-encoding选项能实现这个目的:

$tidy –asxhtml --output-encoding utf8 index.html

输入的编码可以跟输出的不一致。然而如果想要一致的编码,只需指定-utf8选项:

$tidy –asxhtml -utf8 index.html

从多个方面综合考量,我强烈建议使用ASCII或UTF-8编码,因为其他编码不能保证文档在不同操作系统和语言环境下能够可靠地迁移。

Tidy当然还有很多不会对HTML本身产生修改的选项,它们能让文档看起来更整洁,从而使文本编辑器中代码的编辑更容易。

-i选项会缩进文本,这样就可以容易地看到元素的嵌套层次。Tidy足够聪明,不会缩进那些空白是有效的元素,比如pre元素。

-wrap选项会以指定的列数限制文本,通常80列左右比较合适:

$tidy –asxhtml -utf8 –i –wrap 80 index.html

Tidy对运行在PHP、JSP和ASP等技术上的页面的支持是有限的。基本上,它会忽略PHP、ASP或JSP部分中的内容,只能对剩下的HTML标记作处理。但这非常麻烦,特别是大部分的模板系统并不会注意元素边界。如果代码生成的是半个元素,或者只是一个起始标签,而又在最后使用文本标签来结束,就能把Tidy搞迷糊。我并不推荐直接在这些页面上使用Tidy。

相反,从网站上下载经过模板引擎处理的已经显示完毕的页面,在这些抽样页面上运行Tidy,然后把结果跟原有页面进行比较。通过观察区别所在,通常可以找出模板中需要修改的地方,然后手工完成这些修改。

虽然这不需要更多的手工劳动和脑力,但如果是由一个模板负责生成多个静态页面的话,比起大量静态HTML页面的半自动化处理来,这个过程更迅速。

TidyLib是Tidy的C库版本,你可以将它集成到你的程序中。例如,这对编写处理整站的脚本来说是有用的。但从我个人角度看来,对于简单脚本来说C并不是很方便。我通常是编写shell或Perl脚本直接调用Tidy的命令行。

John Cowan的TagSoup(http://home.ccil.org/~cowan/XML/tagsoup/)是一个用Java编写的开源HTML解析器,它实现了XML或SAX的一些简单API。Cowan说它是一个“用Java编写的兼容SAX的解析器,不是解析良构或有效的XML,而是解析世界上大量存在的劣质、污秽和粗野的HTML,虽然很多时候远远处理不了。TagSoup是为那些使用形似合理的软件处理这些问题的人们而设计的。通过提供SAX界面可以把标准的XML工具应用到甚至最坏的HTML上。”

TagSoup并没有计划做成一个供最终用户使用的工具,但还是提供了一些基本的命名行界面。还可以将它跟很多能从SAX中接受输入的XML工具直接挂钩,然后输入HTML,就能得到良构的XHTML,例如:

java -jar tagsoup.jar index.html
<?xml version="1.0" standalone="yes"?>
<html lang="en-US" xmlns="http://www.w3.org/1999/ xhtml"><head><title>Java Virtual Machines</title><meta
name="description" content="A Growing
list of Java virtual machines and their capabilities">
</meta></head><body bgcolor="#ffffff" text="#000000">

<h1 align="center">Java Virtual Machines</h1>
...

你可以稍微改善输出,只需加上--omit-xml-declaration-nodefaults命令行选项:

$ java -jar tagsoup.jar --omit-xml-declaration
    -nodefaults index.html
<html lang="en-US" xmlns="http://www.w3.org/1999/ xhtml"><head><title>Java Virtual Machines</title><meta
name="description" content="A Growing
list of Java virtual machines and their capabilities"></meta>
</head><body bgcolor="#ffffff" text="#000000">

<h1 align="center">Java Virtual Machines</h1>
...

这会删除一些可能会让某些浏览器困惑的代码

还可以用--encoding选项指定输入文档的字符编码。比如,如果你知道文档是用Latin-1、ISO8859-1写的,则可以如此运行:

$ java -jar tagsoup.jar --encoding=ISO-8859-1 index.html

TagSoup的输出永远是UTF-8。

最后,你可以使用--files选项来复制输入文件为以.xhtml为后缀的新文件。否则,TagSoup直接打印到标准输出中,但可以将输出重新放置到任何合适的地方。TagSoup不能像Tidy那样直接改动文件。

然而,TagSoup主要是作为一个库来用的。跟Tidy比起来,来自命令行模式的输出需要解决一些问题,包括:

TagSoup不能保证XHTML绝对有效(尽管能保证良构),有些东西它也是处理不了的。最重要的是,XHTML要求所有的img元素都有一个alt属性。如果alt的值是空的,那么这个图片完全是表现性的,屏幕阅读器(screen reader)应当要忽略它。反之,alt有内容的话,屏幕阅读器就会以内容替代图片的显示。TagSoup没有办法知道省略了alt的图片是否为表现性的,所以它不会插入这个属性。类似地,它也不会插入表格的摘要。你需要手工完成这些需求,在使用TagSoup后马上进行验证,以尽快确定问题所在。

虽然有这些限制,但TagSoup确实可以为你做很多低投入高产出的工作。

对最终用户来说,TagSoup和Tidy最大的不同之处在于理念上的差异。Tidy对问题有时会放弃以及征求你的帮助,有些不知道该怎么去修复的问题,它也不会尝试去修复。TagSoup则永不放弃,它最终一定会输出良构的XHTML,虽然不总能产生有效的XHTML,但还是能帮你不少忙。基于同样的理由,TagSoup不会提示它处理不了而让你能够手工去修复的问题。它假设你不在乎它的做法。如果你在乎的话,你或许更愿意使用Tidy,Tidy更细心,在不是非常确定文档的意思时,它不会给出方案,而TagSoup一定要给出结果才罢休。

程序员的选择也会因它们的不同而不同。第一,TagSoup是用Java写的,而Tidy用C写的。这对你来说或许就是选择的理由(虽然Tidy有一个叫JTidy的Java的移植版)。另一个重要的区别是,TagSoup是以流模式(streaming mode)来操作的。就是说,它一次只对文档的一小部分进行操作,而不是整个文档,这使得它速度非常快,并且可以操作更大的文档。但这样一来,它就不能在头部添加应用到文档末尾的样式规则了。因为非常大的HTML文档还是很少见的(通常几百千兆就到顶了),我认为Tidy这样的全文档处理更强大。

在HTML转换为良构的XHTML后,XSLT(Extensible Stylesheet Language Transformations,可扩展样式表语言转换)是众多可运行在HTML文档上的XML工具之一。实际上,它是我最中意的工具之一,也是解决各种任务要做的第一件事。例如,我用它自动生成很多内容,比如通过HTML页面的零星整理生成RSS和Atom feed。为了能在文档上使用XSLT实际上也是重构文档为良构的XHTML的最大理由之一。XSLT可以查到文档需要修复的问题,并能自动化一些修复工作。

用XSLT来重构XHTML通常不需太多的改动。因此,大部分重构样式表是以代码清单2-9中的标识转换开始的。

代码清单2-9 XSLT中的标识转换

<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
 version='1.0'>
 xmlns:html='http://www.w3.org/1999/xhtml'
 xmlns='http://www.w3.org/1999/xhtml'
 exclude-result-prefixes='html'>

 <xsl:template match="@*|node()">
  <xsl:copy>
   <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
 </xsl:template>

</xsl:stylesheet>

虽然这只不过是将整个文档从输入复制到输出,但之后你就可以按照需求用一些额外的规则修改这个基本的样式表。例如,假如要把废弃的<i>元素转换为<em>元素,你可以添加如下规则:

<xsl:template match='html:i'>
 <em>
   <xsl:apply-templates select="@*|node()"/>
 </em>
</xsl:template>

需要注意的是,在match属性中XPath表达式必须使用命名空间前缀,尽管需要匹配的元素使用的是默认的命名空间。在转换XHTML文档时,这是一个常见的疑问。在XPath表达式中使用XHTML命名空间时,记住你必须总要赋予它一个前缀。

注解


介绍XSLT的好文章在书中或网上都可以找到。首先推荐我写的两篇。The XML 1.1 Bible(Wiley,2003)第15章包括了XSLT的深度介绍,它也可在www.cafeconleche.org/books/bible3/chapters/ch15.html上找到。Elliotte Harold和W. Scott Means著的XML in a Nutshell,3rd Edition(O’Reilly,2004)提供更为详细的介绍。最后,如果想了解得更深入,我推荐Michael Kay 的XSLT: Programmers Reference(Wrox,2001)和XSLT 2.0: Programmers Reference(Wrox,2004)。

原文即With great power comes great responsibility,语出《蜘蛛侠》。Stan Lee是美国漫画大师。驰骋银幕的美国超级英雄“蜘蛛侠”、“神奇四侠”、“X战警”、“绿巨人”以及2008年的《钢铁侠》,都是他老人家的大作。——译者注

由于操作系统的不同,行结束符也不同,Windows用\r\n,UNIX和Mac OS X用\n。——译者注

whitespace,包括空格、回车、制表符等。——译者注

IE 6浏览器对于有xml声明的XHTML,会导致怪异模式(quirks mode)。——译者注

一般情况下应该是<br/>这样的标签,但TagSoup会把它转变为<br></br>的形式。——译者注


相关图书

HTML+CSS+JavaScript完全自学教程
HTML+CSS+JavaScript完全自学教程
零基础入门学习Web开发(HTML5 & CSS3)
零基础入门学习Web开发(HTML5 & CSS3)
HTML CSS JavaScript入门经典 第3版
HTML CSS JavaScript入门经典 第3版
HTML+CSS+JavaScript网页制作 从入门到精通
HTML+CSS+JavaScript网页制作 从入门到精通
从0到1:HTML5 Canvas动画开发
从0到1:HTML5 Canvas动画开发
从零开始:HTML5+CSS3快速入门教程
从零开始:HTML5+CSS3快速入门教程

相关文章

相关课程