CSS 对于现代网站的用户体验至关重要,其地位不亚于决定着网站外形的 HTML 和让网站动起来的 JavaScript。本书作为 CSS 代码重构指南,不仅展示了如何编写结构合理的 CSS,以构建响应式、易于使用的网站,还介绍了如何用重构方法创建可读性更强和更易于维护的 CSS 代码。不论你是刚开始开发自己的第一个 CSS 项目或是清理现有项目的代码,本书提供的多种宝贵方法都可以帮你建设一个符合优秀建构设计原则的 CSS 代码库。
- 了解什么是代码重构及其与 CSS 之间的关系
- 探索 Web 浏览器如何使用级联方法决定为哪个元素应用什么样式
- 编写可预测、易维护和可扩展的 CSS,提升代码复用能力
- 理清不同类型的 CSS 样式及其使用场景
- 确定对哪些浏览器和设备进行测试,以维护好 CSS
- 学习如何合理组织样式,重构 CSS 和评估重构效果
史蒂夫 · 林德斯特伦(Steve Lindstrom)早在 1999 年出于个人爱好开发了自己的第一个网站,那时他还在中学读书。后来他赴佛罗里达州墨尔本市求学,从佛罗里达理工学院获得了计算机科学学士学位。Steve 曾在国防、旅游科技领域从事软件开发工作,最近开始涉足电子商务领域。工作之余,他喜欢学习烹饪和喝咖啡。
本书内容 译者序人们常将前端技术 HTML、CSS 和 JavaScript 亲切地称为“三剑客”,大抵说的是它们并肩支撑起了数以亿计的网站,英勇、侠义之气概不亚于桃园结义的刘关张。但若将网站比作一个少女,HTML 可视作她的身躯,CSS 必然是伊人的着装打扮,而 JavaScript 则是其言谈举止。胭脂铅粉易得,化妆技巧难学。CSS 何尝不是这样一门技术,稍微有点 CSS 知识的前端新人,都可以借助可视化编辑工具,随心所欲地加一串串行内样式,实现自己想要的效果。如此看来,CSS 很简单。但实际上,CSS 又很难,后台开发人员看到前端高手写的一堆堆代码就头大,觉得 CSS 好难啊!改动一处样式,结果页面布局大乱,修改起样式来如履薄冰,这哪里是哪里呀!样式明明加上了,可千呼万唤就是不生效!伺候好一个浏览器,别家的又乱了,这还能按时发布吗?!
CSS 的难,部分原因正是其简单造成的。代码写起来简单、灵活,且有近乎万能的行内样式,实在不行还有更厉害的 !important 声明,所以可以尽管“大胆”地写:定义冗长的选择器,大段粘贴样式,CSS 和 JavaScript 中混用选择器,弃而不用的代码也不删掉,不同用途的样式混在一起。虽然应用样式的目的达到了,但是代码的可读性、性能疏于考虑,华丽的外表下惨不忍睹。
想必很多前端朋友都已经认识到上述问题,并开始有意识地积累 CSS 重构知识。本书刚好适合你。本书是 CSS 重构这个垂直领域的系统指南,作者 Steve Lindstrom 有近二十年的网站开发经验。该书首先从软件架构的高度和软件工程的视角介绍了 CSS 重构的必要性、重构的原则和重构时间点的选择,接着讲解了级联方法、选择器特指度的计算方法以及 CSS 编码规范,然后又介绍了如何测试网站在多种设备和浏览器上的展示效果。你将学到如何用 Gemini 测试框架、PhantomJS 无头浏览器自动化截图,比较重构前后视觉效果上的差异。最后,作者详细介绍了代码的组织方式、重构策略以及重构的评价标准。读完本书,始可与言 CSS 重构已矣。从这本薄书中你收获到的将是一整套 CSS 重构理论加方法指南。此后,你可将自己已掌握和新掌握的知识纳入这个体系,以重构的理念贯穿始终,构建起自己的前端知识大厦,最终输出高性能的 CSS 代码。本书适合编写 CSS 的前端开发人员,新手宜于学习、实践和佐证自己的想法,高手可当是相互切磋。
我周围有很多前端工程师朋友,工作中也会与他们打交道。他们或为网站,或为 Android、iOS 应用编写 CSS 样式。我知道有不少前端朋友之前从事其他行业,从培训班毕业后直接找的工作。我在此郑重向感觉自己缺乏软件工程知识和实践的朋友推荐本书,书中提到的方法能将你引向大路。互联网技术这个行当相对不那么看重各种背景,只要踏实、上进、肯钻研,再加上技术过硬,都能找到一个好去处。
我跟前端还是蛮有渊源的。十年前,我开始傻乎乎地用记事本写 HTML、CSS,添加幼稚的样式,为实现的朴素效果激动不已。读研期间的一次大作业,我曾选择前端技术作为研究方向。我还曾把翻译 HTML 技术文章作为翻译实践。我妻子司韦韦曾从事前端工作。我见证了她转行学习前端到工作的整个过程,我常常不由地为她的华丽转身而惊叹!她在短短几年之内,从一家小外贸公司的职员蜕变为“宇宙中心”一家知名外贸电商的前端工程师。时代造人,不能不让人感慨。技术发展的脚步,我们不能不察;技术发展的速度之快,我们不能不奋起直追。还记得她刚工作时,一个夏日的周末,我们同去中关村图书大厦读书,然后又一起去北大静园草坪游玩。云销雨霁,我们拣地热井旁的大青石拂去雨水而坐,她兴奋地跟我说,她在制作页面方面的很多想法与刚读到的书中的想法如出一辙,我跟着她一起高兴。一本好书的力量是无穷的。我希望本书所讲内容也能给广大前端工程师带来知识、信心和力量。
感谢作者 Steve Lindstrom 将自己多年总结得来的宝贵经验分享给我们。感谢图灵公司的朱巍等诸位编校人员,本书的顺利出版离不开你们幕后的辛勤付出,感谢谢婷婷编辑在译文语言风格等方面的诸多指导。感谢我的同学肖铮,前端这个职位我第一次是从他那里听来的。感谢通过前端技术结识的同事和朋友,他们分别是张琳琳、王海霞、邵有生、赵伟轩、曹倩倩等。感谢 2009 级计算机辅助翻译专业的同学对我翻译工作的支持,同窗几载,受益甚多。在翻译本书的过程中,女儿有时会顽皮地钻到我面前,一定要敲敲键盘才肯罢休。姑娘,你这是长大了要做前端吗?感谢我的家人,我得以心无旁骛做自己喜欢的事,全仰仗众人的携助。
本人学识有限,且翻译仓促,书中难免会有错误、不当和疏漏之处,敬请读者批评指正。
杜春晓2017年2月
前言刚开始学 CSS 那会儿,我发现它的句法(组成一门编程语言的规则和结构)掌握起来很简单,因为代码的编写方式有现成的规则可循。然而,我发现组织好 CSS,使其易于维护,则难度比较大。比这更难的是整理之前写的、结构不清晰的 CSS。我写这本书的初衷就是想把自己在试错过程中学到的一切分享给更多人。若是我刚开始学 CSS 那会儿,市面上有这样一本书就好了。
目标读者虽然我希望所有 CSS 开发人员都能从中受益,但本书主要是写给那些勉强能够编写可用的用户界面的读者的,他们缺乏经验或视野不够开阔,不能从全局理解自己编写的代码是怎么组织在一起的。目标读者懂得怎样编写 CSS 语句,但也许不理解某些做法背后的原因。他们可能也不知道怎么安排代码的架构,使其成为易维护、易扩展并且便于合作开发的软件。
本书的目标本书旨在阐明 CSS 的一些比较微妙的知识点,便于新手理解。我还想讲讲为什么编写和测试 CSS 很难,以及为什么说花时间重构 CSS 是值得的。
本书主题如下:
什么是重构,它的好处是什么,它与软件架构之间什么关系
级联、选择器的优先级和盒子模型等常被误解的 CSS 知识
如何以更明智的方式编写代码并保持一致性,从而编写出质量更高的 CSS
如何用编码标准和模式库维护高质量的 CSS
CSS 的测试方法
CSS 的组织方法
CSS 的重构策略
衡量重构是否成功的标准
你可以将本书中的知识立刻付诸实践,去编写质量更高的 CSS 代码,从而提高代码库的质量。倘若团队协同工作,代码库也会因此更容易维护。若在实践过程中用到书中讲的概念,我鼓励你再次阅读相关章节,彻底把它搞清楚。
本书不涉及的内容本书重点讲解的这些概念,其技术性不一定很强。因此,有很多主题不在本书讲解范围之内,其中包括以下几个。
CSS属性编写 CSS 需要掌握 CSS 属性的相关知识,但是本书不讲。也许本书会时不时地建议你该使用什么属性,但是为了能够系统地学习这些属性,建议你参考 Eric Meyer 的《CSS 权威指南》、Christopher Schmitt 的《CSS Cookbook 中文版》或其他权威网站,比如 Mozilla Developer Network。
HTML结构安排构建用户界面既需要 HTML 又需要 CSS,它们会相互影响。我们会讨论将 CSS 从 HTML 中分离出来的方法,但是不会讨论 HTML 编写方式及其结构安排的利弊。
前端性能搭建网站,前端性能很重要,这个主题也非常有趣。可是由于本书只讲 CSS 重构,因此我们只是简要讲讲前端性能。这个话题涉及面很广,囊括了其他多个主题。Steve Souders写了几本性能方面的书,都非常不错。Paul Irish、Nicole Sullivan和 Stoyan Stef anov三人也在这个方面做出了很多了不起的工作。此外,谷歌提供的一些指南和工具,对于提升前端性能也很有帮助。
CSS框架CSS 框架变动比较频繁,在实现上强行加入了自己定义的规则,因此本书也不讲。然而,我希望你在读完本书后能够看着任意一个框架的源代码自己去评价一下其实现的好坏。
小众浏览器Web 浏览器数量惊人,但我们只讨论主流浏览器,比如 Microsoft Edge(之前的 IE)、 Safari、Chrome 和 Firefox 以及它们的移动浏览器版本,因为这些浏览器占据了绝大多数市场份额。
术语本书假定目标读者具备一定的 CSS 知识,但他们对有些术语可能不熟悉,因此书中会对一些术语作出解释。下面这些术语更为基础,先在这里列出来。
选择器是指为一个或一组元素添加样式时所使用的模式。
声明块是一组规则,表示为 HTML 元素应用什么属性,属性取什么值。
属性表明为选中的元素应用什么样式。我们需要为属性指定取值。
规则集由一个或多个选择器和一个声明块组成。
例 P-1 的 CSS 代码要求浏览器为所有段落应用蓝色、16px(像素)大小的样式。p 为选择器,告诉浏览器为哪个元素添加样式。左右花括号之间的所有内容为声明块。该声明块包括两条声明语句:第一条为 color 属性赋值 #1200FF,第二条为 font-size 属性赋值 16px。整个这一段代码即为规则集。为了方便理解,我们在图 P-1 中标明了规则集的各个组成部分。
例 P-1 规则集示例
p { color: #1200FF; font-size: 16px;}
图 P-1:CSS 规则集图解
配套网站提供的内容本书的配套网站提供以下内容:
本书各示例的代码文件
偶尔会发博客文章
好文章、演讲稿和其他资源的链接
勘误和纠错信息
本书是要帮你完成工作的。一般来说,如果本书提供了示例代码,你可以把它用在你的程序或文档中。除非你使用了很大一部分代码,否则无需联系我们获得许可。比如,用本书的几个代码片段写一个程序就无需获得许可,销售或分发 O'Reilly 图书的示例光盘则需要获得许可;引用本书中的示例代码回答问题无需获得许可,将书中大量的代码放到你的产品文档中则需要获得许可。
我们很希望但并不强制要求你在引用本书内容时加上引用说明。引用说明一般包括书名、作者、出版社和 ISBN。比如:“CSS Refactoring: Architect Your Stylesheets for Success by Steve Lindstrom (O'Reilly). Copyright 2017 Steve Lindstrom, 978-1-491-90642-2”。
如果你觉得自己对示例代码的用法超出了上述许可的范围,欢迎你通过 permissions@oreilly.com 与我们联系。
排版约定本书使用了下列排版约定。
黑体
表示新术语或重点强调的内容。
等宽字体(
constant width)表示程序片段,以及正文中出现的变量、函数名、数据库、数据类型、环境变量、语句和关键字等。
加粗等宽字体(
constant width bold)表示应该由用户输入的命令或其他文本。
等宽斜体(
constant width italic)表示应该由用户输入的值或根据上下文确定的值替换的文本。
Safari(原来叫 Safari Books Online)是面向企业、政府、教育从业者和个人的会员制培训和参考咨询平台。
我们向会员开放成千上万本图书以及培训视频、学习路线、交互式教程和专业视频。这些资源来自 250 多家出版机构,其中包括 O'Reilly Media、Harvard Business Review、 Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Adobe、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett 和 Course Technology。
更多信息,请访问这里。
联系我们请把对本书的评价和问题发给出版社。
美国:
O'Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
中国:
北京市西城区西直门南大街 2 号成铭大厦 C 座 807 室(100035)
奥莱利技术咨询(北京)有限公司
我们为本书做了一个网页,把勘误信息、示例代码和其他附加信息列在了上面。点击前往
对于本书的评论和技术性问题,请发送电子邮件到:
bookquestions@oreilly.com
要了解更多 O'Reilly 图书、培训课程、会议和新闻的信息,请访问这里
我们在 Facebook 的地址如下:
http://facebook.com/oreilly
请关注我们的 Twitter 动态:
http://twitter.com/oreillymedia
我们的 YouTube 视频地址如下:
http://www.youtube.com/oreillymedia
致谢写作本书的过程让我真切地感受到了自己的不足。
一天晚上,我心血来潮,毛遂自荐,给 O'Reilly Media 发了一封简单的邮件,附上了我对于这本书的构思。邮件发出去之后,我忐忑不安起来,但很快就把这事抛诸脑后了,因为我压根就没指望收到回信。
后来,我却收到了 O'Reilly 的回信,还是好消息。这令我更加不安起来,我怕自己被冲昏了头,但是跟 Simon St. Laurent、Brian MacDonald 和 Meg Foley 的合作非常愉快。我尤其感激本书的编辑 Meg Foley,因为她非常通情达理,不断地鼓励我,尽管我一次又一次没能在截止日期前交稿。我之前表示过歉意,现在容我再说一次——对不起,Meg !
本书中的技巧、策略和想法,是我多年来从各处收集来的。书中的大部分概念都不是我提出来的,因此,首先我想感谢前人贡献了这些概念。加入 Web 开发社区非常有好处,因为社区有很多聪明人,他们愿意跟别人分享自己的想法。我希望能通过此书为社区尽绵薄之力。
写一本书比我预想的难多了,写作过程很耗时,而且需要我独自完成,幸好工作时周围有好友相伴。每当写不下去时,我就想起 Andy Denmark,他的职业道德超好,我从他那里得到了不少鼓舞。Thor Denmark 还教我如何在顺境和逆境中都保持积极的态度,使我不至于陷入困境无法自拔。Nate Racklyeft 和 Josh Hudner 阅读了本书的初稿,并给出了大量宝贵的反馈意见,极大提高了本书的质量。Erin Wallace 从事设计工作,整天跟组织、过程和细节打交道,她的反馈意见也非常有价值,有助于我润色书稿,使其更容易理解。他们每天都关注着我,促使我每天都比前一天进步一点。为此,我对他们感激不尽。
此外,我还要郑重感谢 Christopher Schmitt,他审阅了本书。得知他审阅我的作品时,我觉得他的名字很熟悉,果不其然,我的案头上就摆着他的两本大作。Christopher,你的批注对我帮助很大。我刚开始学 CSS 时,你的著作让我受益匪浅。非常感谢你抽出时间帮助一个陌生人。希望我有机会报答你!
如果不感谢培养我、影响我、使我变成今天这个模样的家人,致谢部分就不完整。我的父母总是鼓励我读书,我现在仍努力多读书、少看电视。小时候,父亲就送给了我一本 O'Reilly 图书(我的第一本 O'Reilly 图书,我记得是讲 C 语言的)。我选择 IT 作为职业,很大程度上源于父亲的鼓励和他从事的职业。母亲和哥哥也是我的灵感源泉,并给予我巨大鼓舞,你们对我的关爱难以言谢。
最后,我想感谢咖啡。我爱你,咖啡。
第 01章:重构和架构(上)-
-
- 1.1 什么是重构
- 1.2 什么是软件架构
- 1.2.1 优秀架构是可预测的
- 1.2.2 优秀架构可提升代码复用性
- 1.2.3 优秀架构可扩展
- 1.2.4 优秀架构可维护
- 1.2.5 软件架构和重构
- 1.3 需要重构的原因
- 1.3.1 需求变更
- 1.3.2 架构设计不合理
- 1.3.3 低估困难
- 1.3.4 忽视最佳实践
- 1.4 什么情况下应该重构代码
- 1.5 什么情况下不应该重构代码
- 1.6 我能重构自己的代码吗
-
本章是 CSS 重构之旅的起点,将介绍重构是什么,以及它与软件架构之间有什么关系。我们还将讨论重构的重要性以及你的代码也许需要重构的原因。我们将通过两个重构的示例,帮助你理解这些概念。
1.1 什么是重构重构是指在不改变代码行为的前提下,重写代码,使其更加简洁、易于复用。如果你正在写代码,那么重构是你应该掌握的一项核心技能,因为不管你想不想,有时你都不得不重构代码。你也许已重构过代码,只是自己没有意识到。既然重构不改变代码的行为,那么学习重构之前你想先弄清楚重构的必要性也是可以理解的。但是,在回答这个问题之前,需要先来理解软件架构。
1.2 什么是软件架构就像生物一样,软件系统通常由很多较小的部件组成,每个部件擅长做一件事。将这些部件组合起来,一起工作,可形成更大的软件系统。术语软件架构用来描述软件项目的各个不同部件之间的组合方式。
从简单的网站到复杂的宇宙飞船控制系统,每一种软件都有自己的架构,不管开发人员有意还是无意为之。然而,最好的架构通常在编码工作开始之前做过缜密规划。下面是优秀架构所具备的一些最重要的特点。
1.2.1 优秀架构是可预测的软件架构可预测是指可以对软件的工作方式和结构做出准确的假设。可预测性表明预先的规划是合理的,并有助于节省开发时间,因为可以避免下列问题:
- 组件的功能是什么
- 某一段代码在何处
- 新代码加到何处
在可预测的架构中,人们可以做出精确的假设,不熟悉代码的开发人员也能够更快地理解该架构。
1.2.2 优秀架构可提升代码复用性代码复用是指在多处使用同一代码而无需重写。代码复用优势明显,不用重写已有代码,可以加快开发速度。同理,解决某一问题所需的代码越少,维护所有用该代码实现的功能所需的时间就越少。例如,你在一段代码中发现了一处 bug,而由于项目多处用到该代码,故将 bug 带到了多处。你只需在一个位置修复该 bug,就可以相应修复其他各处的 bug。
1.2.3 优秀架构可扩展可扩展性是优秀架构所遵循的一项原则,在具备该特点的系统上增加新功能很容易。大多数软件无法在一天之内开发完成,因此软件架构适于增量开发,且不需要做大的结构性变化,这一点非常重要。如果项目开发过程需要频繁地对架构做出较大的改动,发布将非常困难。
1.2.4 优秀架构可维护跟可扩展性非常类似,可维护性对于架构也很重要。对于可维护的优秀架构,修改其现有功能很容易。随着时间的推进,需求也许会发生变化。迫于需求变动的压力,你将修改代码。可维护性软件是指你修改一处代码时,没必要大规模改动其他代码。
1.2.5 软件架构和重构概括来讲,重构有助于维护和提升软件架构。重构就是指调整代码结构、使其更具意义的一套技术。重构可使代码可预测、可复用、可扩展和可维护。当你的软件架构具备了上述特点时,它对目标用户而言将更可靠,你在其上继续开发也会更加愉快!
1.3 需要重构的原因为什么当初不把代码写正确,这样日后不就没必要重构了?尽管我们一心想设计和编写最高质量的代码,但是随着时间的推移,一些因素将发生变化,导致需要重构代码。我们一起来看看其中几个原因。
1.3.1 需求变更软件系统随着需求的变动而进化。软件在编写时是为了满足一组需求,可能没有考虑另一组需求,(没有写出来,也不应该写出来)。因此,需求变更时,代码也必须随之改变。此外,如果软件开发还有时间要求,有时会为优先满足功能的实现而走“捷径”,进而可能会影响代码质量。
1.3.2 架构设计不合理即使你知道什么是优秀架构,投入大量时间事先规划好一切并不总是可行的。开发之初,你若不知道各组件的组合方式,开发过程也许需要重构。非常常见的做法是,快速开发一个新功能(可能会为实现功能而走“捷径”),以验证它对用户是否有吸引力。如确实能够吸引用户,再将代码整理干净;如达不到预期效果,则删除代码。
1.3.3 低估困难预估软件开发需要多长时间很难,但不幸的是,我们常常根据估计结果来安排开发计划。如果项目开发周期被低估,将迫使开发人员“为了完成而完成”,导致他们快速编写代码,而不会花很多时间思考。如果该情况经常发生,即使最完美的代码也可能会变为一大盘子“意大利面条式代码”,难以理解和管理。
1.3.4 忽视最佳实践跟上每一种最佳实践的发展步伐很难,当你的工作涉及多种技术和人员管理时更是如此。如果你们是一个团队协同工作,而你忽视了最佳实践,我希望有同事能提醒你。如果错过使用最佳实践的机会,日后你也许需要重新审查你的代码并进行一定程度的重构。
紧跟最佳实践的难点所在
技术发展日新月异,因此之前的最佳实践也许优势不再。例如,2011 年之前,在网站上展示具有圆角的容器,需要为每个角挂一张图片,将图片嵌入到 HTML 之中,然后用 CSS 为图片定位,确保各元素排列有序。如今这项技术已过时,因为现在的浏览器用 CSS 属性 border-radius 就能实现圆角。如果你不持续更新代码来使用这些最佳实践,你的技术债务将随时间的发展而增加,代码将变得非常糟糕。
结合代码的上下文重构代码会更加容易。因此,如果你修复的 bug 或开发的新功能用到了已有代码,重构是最好的选择。处理小任务时顺便重构代码,不至于把一切搞乱,他人若修改你重构过的代码也能从中受益。不断坚持重构代码,代码质量将达到卓越,前提是你的改动符合优秀架构的特点。
然而,有时你会遇到一段有很多依赖的代码,也许需要决定是否对其重构。重构有很多依赖的代码,就像抽衣服上的线:抽得越多,散开得越多。类似地,对于具有很多依赖的代码,你改动得越多,需要更新的依赖就越多。遇到这种情况,如果时间很紧,先把工作完成也许更适合,然后再匀出些时间,回头审视并重构代码。然而,如果开发过程中重构某些小功能不至于严重影响到开发计划,你也许可以考虑及时重构它们。
1.5 什么情况下不应该重构代码知道什么情况下不应该重构,甚至比知道什么情况下应该重构更为重要。重构名声不太好,因为有时看起来软件开发人员只是为了重写而重写。代码也许是别人写的,没必要重构,但患有“不是我写的症”的开发人员一定要对其重构,因为他们认为不是自己写的代码就不是好代码。或者,有一天开发人员心血来潮,不再喜欢之前的代码编写方法(也许之前类名使用下划线而不是连字符,现在想改过来),因此他们钻入重构的兔子洞以求止痒。很多情况下,这些工作被视为“磨洋工”,它让人们感觉效率很高,事实上并非如此。第 5 章将讨论如何通过编写一套编码规范形成编码计划。那时,你将更加清楚,仅当重构能够改善架构或使代码符合编码规范时,才应进行重构。
1.6 我能重构自己的代码吗如果你正在开发个人项目,答案为响亮的“能!”。但是如果你为组织工作,且不处在管理岗位,答案也许没有那么肯定。理想情况下,每个组织都理解重构的重要性,但现实往往并非如此。如果同事缺乏重构方面的技术知识,你也许可以尝试教教他们,别忘了推荐我这本书!
对软件项目的代码质量负责的聪明人很可能理解重构的意义,但是不能理解的人可能会持有以下意见:
- 花时间重写代码,却又看不到功能上的变化,既浪费时间,又浪费钱;
- 如果代码还能正常工作,没必要修复;
- 你应该当初就把代码写正确。
如果别人持有以上理由,而你对重构有足够的信心,我建议你重构代码,只要你能够保证开发进度,并且小心谨慎,不破坏其他功能。如果你听到这些理由,我敢打赌他们从未参加过代码评审会议,因此你的改动可能不会被注意到。然而,如果只是为重构而重构,你也许需要考虑等到有必要改动时再重构;不成熟的优化往往跟技术债务同样糟糕。
第 01章:重构和架构(下)-
-
- 1.7 重构示例
- 1.7.1 重构示例1:计算电子商务订单的总价
- 1. 单元测试
- 2. 重构 getOrderTotal
- 1.7.2 重构示例2:重构CSS的简单示例
- 1.7.1 重构示例1:计算电子商务订单的总价
- 1.8 总结
- 1.7 重构示例
-
你对重构的好处和时机(以及何时不能重构)有了整体理解后,我们可以开始讨论怎样重构代码了。
尽管本书是讲代码重构的,但我们首先通过一段计算电子商务订单总价的代码和改变 HTML 元素样式的代码分析这个概念,这样理解起来会更容易。因此,我们的第一个示例将展示重构基本的 JavaScript 代码的方法,该代码计算电子商务订单的总价。第二个示例将重构一部分 CSS 代码。
代码示例
长篇累牍的代码,或长达几页,或散布于多个文件,难以理解,故本书代码示例将使用简短的代码片段。第一个示例的所有 JavaScript 代码可以嵌入到 HTML 文件中,方便运行。
对于更复杂的示例,定义元素整体外观和样式的 CSS 将置于单独的 CSS 文件中。
本书代码中用到的行内样式(位于 和 标签之间)直接服务于当前示例,用来解释一个单独的概念。
本书所有代码均可从配套网站下载:https://www.cssrefactoringbook.com。
1.7.1 重构示例1:计算电子商务订单的总价例 1-1 包含一段 JavaScript 代码,用户提供以下内容后,可计算电子订单的总价:
- 所购商品的单价
- 每种商品的购买数量
- 每种商品的单位运费
- 顾客的地址信息
- 可选择使用的、能降低订单价格的折扣码
例 1-1 计算电子商务订单总价
/** * 打过折、加入运费和税费之后,计算订单总价。 * * @param {Object} customer——顾客信息,关于下订单者的一组信息。 * * @param {Array.} lineItems——数组,包括所购商品、商品数量及每种商品的单位运费。 * * @param {string} discountCode——可选择使用的折扣码,加入运费和税费之前使用该码。 */var getOrderTotal = function (customer, lineItems, discountCode) { var discountTotal = 0; var lineItemTotal = 0; var shippingTotal = 0; var taxTotal = 0; for (var i = 0; i < lineItems.length; i++) { var lineItem = lineItems[i]; lineItemTotal += lineItem.price * lineItem.quantity; shippingTotal += lineItem.shippingPrice * lineItem.quantity; } if (discountCode === '20PERCENT') { discountTotal = lineItemTotal * 0.2; } if (customer.shiptoState === 'CA') { taxTotal = (lineItemTotal - discountTotal) * 0.08; } var total = ( lineItemTotal - discountTotal + shippingTotal + taxTotal ); return total;};
使用例 1-2 中的数据,调用 getOrderTotal 函数,得到总价。程序输出 Total: $266。例 1-3 解释了为什么会输出这个结果。
例 1-2 用测试数据运行 getOrderTotal 函数
var lineItem1 = { price: 50, quantity: 1, shippingPrice: 10};var lineItem2 = { price: 100, quantity: 2, shippingPrice: 20};var lineItems = [lineItem1, lineItem2];var customer = { shiptoState: 'CA'};var discountCode = '20PERCENT';var total = getOrderTotal(customer, lineItems, discountCode);document.writeln('Total: $' + total);
例 1-3 解释为什么 getOrderTotal 函数输出 Total: $266
discountTotal = 0lineItemTotal = 0shippingTotal = 0taxTotal = 0# FOR循环第1次迭代:lineItemTotal = 0 + (50 * 1) = 50shippingTotal = 0 + (10 * 1) = 10# FOR循环第2次迭代:lineItemTotal = 50 + (100 * 2) = 250shippingTotal = 10 + (20 * 2) = 50# discountCode为“20%”,计算discountTotal:discountTotal = 250 * 0.2 = 50# customer.shiptoState为“CA”,计算 taxTotal:taxTotal = (250 - 50) * 0.08 = 16total = 250 - 50 + 50 + 16 = 266
1. 单元测试
运行完计算过程,得到计算结果,一切似乎按照预期进行。为了确保每次都能得到正确结果,现在来编写单元测试。简单来讲,单元测试是指执行另一段代码的一段代码,以保证代码按照预期工作。单元测试应该用来测试单一功能,以缩小任何可能发现的问题的根本原因的范围。另外,发布任何新代码之前,都应该为你的项目编写一组单元测试并运行,以便尽早发现和修复引入系统的新 bug。
例 1-2 的输入数据可用来编写单元测试(见例 1-4),我们断言函数返回预期值(266)。运行完测试代码,将输出测试成功和失败的次数,对于没有通过测试的,将输出期望值和实际值。
例 1-4 为 getOrderTotal 函数编写的单元测试
var successfulTestCount = 0; var unsuccessfulTestCount = 0; var unsuccessfulTestSummaries = []; /** * 断言getOrdertotal()函数计算正确。 */ var testGetOrderTotal = function () { // 设定期望得到的结果 var expectedTotal = 266; // 设定测试数据 var lineItem1 = { price: 50, quantity: 1, shippingPrice: 10 }; var lineItem2 = { price: 100, quantity: 2, shippingPrice: 20 }; var lineItems = [lineItem1, lineItem2]; var customer = { shiptoState: 'CA' }; var discountCode = '20PERCENT'; var total = getOrderTotal(customer, lineItems, discountCode); // 比较函数的计算结果与期望得到的结果 if (total === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetOrderTotal: expected ' + expectedTotal + '; actual ' + total ); } }; // 运行测试 testGetOrderTotal(); document.writeln(successfulTestCount + ' successful test(s)'); document.writeln(unsuccessfulTestCount + ' unsuccessful test(s)'); if (unsuccessfulTestCount) { document.writeln(''); for(var i = 0; i < unsuccessfulTestSummaries.length; i++) { document.writeln('- ' + unsuccessfulTestSummaries[i] + '
'); } document.writeln('
'); }
执行 testGetOrderTotal 函数,断言成功通过,如图 1-1 所示。
图 1-1:成功通过单元测试
然而,如果以后出于某种原因引入了 bug,导致计算 discountTotal 所用的乘数从 0.2 变为 -0.2,单元测试结果就会发生变化,我们将会得到图 1-2 所示的结果。
图 1-2:失败的单元测试
单元测试非常强大,可以保证你的系统按照预期工作。重写代码时,它的帮助尤其大,因为使用断言可帮助你确认代码的行为没有发生变化。
现在你理解了计算电子商务订单总价的代码,并实现了相应的单元测试代码,下面一起看看对其重构能带来哪些好处。
2. 重构 getOrderTotal仔细分析 getOrderTotal 函数,不难发现该函数内实现了多种计算:
- 从总价中减去的折扣
- 订单中所有商品的总价
- 总运费
- 总税额
- 订单总价
如果上述五项中任意一项的计算过程引入 bug,单元测试(testGetOrderTotal)将告诉我们出错了,但是不会明确指出 bug 的位置。这正是单元测试应该测试单一功能的主要原因。
为了让代码所实现功能的粒度更细,上面提到的这些计算都应该抽取出来,作为单独的一个函数,并且用能够描述其功能的名称作为函数名,请见例 1-5。
例 1-5 抽取代码片段,形成新函数
/** * 计算所有line items的总价。 * * @param {Array.} lineItems——数组,包括所购商品、商品数量及每种商品的单位运费。 * * @returns {number}——所有line items的总价。 */ var getLineItemTotal = function (lineItems) { var lineItemTotal = 0; for (var i = 0; i < lineItems.length; i++) { var lineItem = lineItems[i]; lineItemTotal += lineItem.price * lineItem.quantity; } return lineItemTotal; }; /** * 计算所有line items的总运费。 * * @param {Array.} lineItems——数组,包括所购商品、商品数量及每种商品的单位运费。 * * @returns {number}——所有line items的运费。 */ var getShippingTotal = function (lineItems) { var shippingTotal = 0; for (var i = 0; i < lineItems.length; i++) { var lineItem = lineItems[i]; shippingTotal += lineItem.shippingPrice * lineItem.quantity; } return shippingTotal; }; /** * 计算一个订单的总价按照折扣减去了多少钱。 * * @param {number} lineItemTotal——所有line items的总价。 * * @param {string} discountCode——可选择使用的折扣码,加入运费和税费之前使用该码。 * * @returns {number}——订单总价按照折扣减去了多少钱。 */ var getDiscountTotal = function (lineItemTotal, discountCode) { var discountTotal = 0; if (discountCode === '20PERCENT') { discountTotal = lineItemTotal * 0.2; } return discountTotal; }; /** * 计算一个订单应缴纳的总税费。 * * @param {number} lineItemTotal——所有line items的总价。 * * @param {Object} customer——顾客信息,关于下订单者的一组信息。 * * @returns {number}——一个订单应缴纳的总税费。 */ var getTaxTotal = function () { var taxTotal = 0; if (customer.shiptoState === 'CA') { taxTotal = lineItemTotal * 0.08; } return taxTotal; };
我们应该为每个新函数编写如例 1-6 所示的单元测试。
例 1-6 用 JavaScript 为新抽取出来的函数编写的单元测试
/** * 断言getLineItemTotal的计算结果符合预期。 */ var testGetLineItemTotal = function () { var lineItem1 = { price: 50, quantity: 1 }; var lineItem2 = { price: 100, quantity: 2 }; var lineItemTotal = getLineItemTotal([lineItem1, lineItem2]); var expectedTotal = 250; if (lineItemTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetLineItemTotal: expected ' + expectedTotal + '; actual ' + lineItemTotal ); } }; /** * 断言getShippingTotal的计算结果符合预期。 */ var testGetShippingTotal = function () { var lineItem1 = { quantity: 1, shippingPrice: 10 }; var lineItem2 = { quantity: 2, shippingPrice: 20 }; var shippingTotal = getShippingTotal([lineItem1, lineItem2]); var expectedTotal = 250; if (shippingTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetShippingTotal: expected ' + expectedTotal + '; actual ' + shippingTotal ); } }; /** * 确保使用有效的折扣码时,GetDiscountTotal的计算结果符合预期。 */ var testGetDiscountTotalWithValidDiscountCode = function () { var discountTotal = getDiscountTotal(100, '20PERCENT'); var expectedTotal = 20; if (discountTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetDiscountTotalWithValidDiscountCode: expected ' + expectedTotal + '; actual ' + discountTotal ); } }; /** * 确保使用无效的折扣码时,GetDiscountTotal的计算结果符合预期。 */ var testGetDiscountTotalWithInvalidDiscountCode = function () { var discountTotal = get_discount_total(100, '90PERCENT'); var expectedTotal = 0; if (discountTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetDiscountTotalWithInvalidDiscountCode: expected ' + expectedTotal + '; actual ' + discountTotal ); } }; /** * 确保顾客住在加利福尼亚时,GetTaxTotal的计算结果符合预期。 */ var testGetTaxTotalForCaliforniaResident = function () { var customer = { shiptoState: 'CA' }; var taxTotal = getTaxTotal(100, customer); var expectedTotal = 8; if (taxTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetTaxTotalForCaliforniaResident: expected ' + expectedTotal + '; actual ' + taxTotal ); } }; /** *确保顾客不住在加利福尼亚时,GetTaxTotal的计算结果符合预期。 */ var testGetTaxTotalForNonCaliforniaResident = function () { var customer = { shiptoState: 'MA' }; var taxTotal = getTaxTotal(100, customer); var expectedTotal = 0; if (taxTotal === expectedTotal) { successfulTestCount++; } else { unsuccessfulTestCount++; unsuccessfulTestSummaries.push( 'testGetTaxTotalForNonCaliforniaResident: expected ' + expectedTotal + '; actual ' + taxTotal ); } };
最后,我们用这些新抽取出来的函数改写 getOrderTotal 函数,请见例 1-7。
例 1-7 用新抽取出来的函数改写 getOrderTotal 函数
/** *打过折、加入运费和税费之后,计算订单总价。 * * @param {Object} customer——顾客信息,关于下订单者的一组信息。 * * @param {Array.} lineItems——数组,包括所购商品、商品数量及每种商品的单位运费。 * * @param {string} discountCode——可选择使用的折扣码,加入运费和税费之前使用该码。 */ var getOrderTotal = function (customer, lineItems, discountCode) { var lineItemTotal = getLineItemTotal(lineItems); var shippingTotal = getShippingTotal(lineItems); var discountTotal = getDiscountTotal(lineItemTotal, discountCode); var taxTotal = getTaxTotal(lineTtemTotal, customer); return lineItemTotal - discountTotal + shippingTotal + taxTotal; };
分析完上述代码,我们观察到:
- 函数比以前更多了;
- 单元测试比以前更多了;
- 每个函数实现一个特定功能;
- 每个函数都有一个单元测试;
- 多个函数组合起来可以实现更复杂的计算。
总体来讲,重构之后代码结构更加合理。getOrderTotal 函数内部计算各种价格的代码被抽取出来,作为一个个单独的函数,而且每个函数都有相应的单元测试。这意味着当代码中引入 bug 时,更容易定位受影响的功能。此外,如果总税额或运费需要更换计算方式,而现有功能已经提供了可用的单元测试,那么更换之后,可方便地用单元测试加以验证。
例 1-8 的代码用来展示网站的标题栏。
例 1-8 网站标题栏代码
Ferguson's Cat Shelter San Francisco's Premiere Cat Shelter
打开浏览器,加载 index.html,将看到如图 1-3 所示的内容。
图 1-3:网站标题栏截图
在第一个重构示例中,重构之前我们编写了单元测试,以保证重构没有改变代码的行为。重构 CSS 时,确保重写代码没有改变展示效果同样重要。但无法直接测试,因为重构 CSS 带来的是视觉上的变化,而不是明确的数值上的变化。第 5 章将讨论保持视觉效果上的对等性所用到的一些技术。现在,在重构前截图作为参考即可。
重构网站标题
显然,例 1-8 的代码还有提升空间,因为用 标签表示的标题栏样式内嵌在 style 属性之中。通过元素的 style 属性或将样式置于 标签之间,将样式嵌入到 HTML 代码之中,这种样式叫作行内样式。
行内样式复用性不高,这一点非常类似于例 1-1 重构前、函数体内执行多种计算的函数。使用 style 属性设置的样式,只可用于当前元素。嵌入到 标签中的样式,只可用于当前页。
因为大多数网站包含多个页面,并且每个页面都可能有一个标题栏,所以标题的样式应该抽取出来作为单独的 CSS 文件(该例中的 style.css),以用于多个网页,并被浏览器缓存。 style.css 文件的内容请见例 1-9,例 1-10 为抽掉行内 CSS 的 HTML 代码。
例 1-9 将标题栏 CSS 抽取出来作为 style.css 文件
h1 { font-family: Helvetica, Arial, sans-serif; font-size: 36px; font-weight: 400; text-align: center;}
例 1-10 抽掉行内 CSS 的 HTML 代码
Ferguson's Cat Shelter San Francisco's Premiere Cat Shelter
刷新浏览器,很快就会发现页面显示效果没有变化,我们由此得出以下结论:
- 抽取行内 CSS 可提升复用性;
- 分离代码功能(样式和结构)可增强代码可读性;
- 回归测试可手动用 Web 浏览器完成,或通过比较重构后的界面和重构前的截图完成。
将样式抽取出来作为单独的文件,可提升代码的复用性,因为抽取出来的样式可用于多个文件。将 CSS 置于一个独立于 HTML 代码的文件后,HTML 和 CSS 都更加清晰易读,因为 HTML 中不会包含冗长的样式定义,并且 CSS 可以按照符合逻辑的方式以声明块的形式组织在一起。最后,测试重构是否改变了页面;可手动用浏览器重新加载页面,与重构前的截图进行对比。
虽然该示例很简单,但大量类似的小改动累积起来,效果将十分可观。
1.8 总结通过本章的学习,我们知道了什么是重构以及它与软件架构之间的关系。我们还了解了重构为什么重要,以及何时应该重构。最后,通过两个重构的例子了解了单元测试。下一章介绍级联。要编写 CSS,级联是需要理解的最重要的概念。
第02章:级联 第03章:编写更优质的 CSS(上) 第03章:编写更优质的 CSS(下) 第04章:为样式分类(上) 第04章:为样式分类(下) 第05章:测试(上) 第05章:测试(下) 第06章:代码的组织和重构策略 附录阅读全文: http://gitbook.cn/gitchat/geekbook/5a39b64f75e5a577886d58d6
