越来越多的企业开始选择 Rails 作为 Web 应用的框架。Rails 曾经还主要是一些轻公司的选择,但今天一些“重”企业(比如保险、金融等行业的企业)也开始把 Rails 纳入内部应用甚至外部应用的考虑范围。我最近服务过的客户是国外某大型保险公司,该公司就选择了 Rails 来创建他们的保险销售网站。
选择 Rails 的原因,是因为它快速构建的能力,是因为它是 Web 开发的 DSL。但是否选择了 Rails 就代表了高效开发?是否在 Rails 上创建的 Web 应用就一定是高质量的?答案是否定的。从我参与过的几个 Rails 项目来看,质量可谓是参差不齐,开发速度也是判若云泥。而开发的效率低下的原因,则常常是应用本身质量的低下和设计的拙劣。
在本文中,我将逐一讨论几个影响 Web 应用质量的因素。同时,我们也可以从中领悟到 Rails 为创建高质量的 Web 应用所做的努力、它的各种设计给我们的启示,以及 Rails 3 的改进所代表的意义和趋势。
MVC
我们都知道 Rails 是一个 MVC 架构模式的 Web 框架,MVC 各部分的职责也很清楚。但问题在于我们是否真的遵循了 MVC 架构模式做到了各部分职责的明确分离?是否遵循了单一职责的原则?
在大多数代码里面,这种混沌不清的状态存在于 model 和 controller 之间:controller 承担了太多本应由 model 承担的职责。其中比较典型的例子是内嵌(多)对象表单。比如,Album 和 Photo 之间是一对多的关系,我们要创建一个含有多个 photo 的 album。在 Rails 2.3 之前,我们可能会写出类似的代码:
AlbumsController def create album = Album.new params[:album] album.photos << Photo.new params[:album][:photo] ...
如果是一个涉及更多种类型对象的表单,这里的代码可能会更加复杂。但在 AlbumsController 里面,我们真正想关心的只是 Album 的创建,而不是 Photo 或其它关联对象的创建。而且从 Album 的角度看,创建过程中 photo 跟其它 attributes 没有区别,应该得到一致地处理。
Rails 2.3 之后,我们就可以很简单地达到这个目的。在 Album 里面做这样的声明:
class Album < ActiveRecord::Base ... accepts_nested_attributes_for :photos end
然后,controller 中的代码就可以被简化为:
AlbumsController def create album = Album.new params[:album] ...
从这个例子中可以看到 Rails 在推进 MVC 三部分之间职责明确上所做的努力和进步。很多人可能会说,我们的代码没有这样的问题。但 MVC 三部分之间职责开始模糊,往往出现在业务逻辑变得复杂之后。我们应该经常审视我们的代码,做到真正的职责单一。
REST
现今的互联网应用已经很难是一个独立的个体,互联网应用之间的交互越来越多。所以,建立 REST 架构风格的互联网应用变得越来越重要。Rails 的 router 很好地支持了 REST 风格的外部接口设计。Rails 3 所做的一个很大改进就是 router 的改进,以强调 REST 风格的接口设计。
REST 也让我们以资源的角度看待应用中的数据,我们的代码设计因此也产生了一些变化。当需要增加一个 invoice 的 PDF 文件下载功能的时候,我们一般会向 InvoicesController 添加这么一段代码:
InvoicesController def download_pdf ... send_data(generate_pdf(@invoice), :type => 'application/pdf') end
这段代码至少存在两个问题:第一个问题,就是我们前面所述的职责明确问题。PDF 的 generate 属于 Invoice 而不是 controller 的职责,所以我们应该把 PDF 生成的逻辑移到 Invoice 内部。第二个问题,则是语义是否恰当的问题 。如果我们以资源的角度看待 Invoice 的话,PDF 跟 HTML 或者 XML 一样,只是 Invoice 的另一种表现形式而已。而表现一个资源,在 show action 中处理最为恰当。
重写之后,代码如下:
def show ... respond_to do |format| format.html format.pdf { render :pdf => @invoice.pdf } end end
重写之后的代码不仅更符合 REST 的风格,而且更加简洁优美。
JavaScript
随着 RIA 的普及以及 HTML5 时代的即将来临,JavaScript 的江湖地位正在与日俱增。从 Google 的一些应用就可以看出业界对于 JavaScript 态度的一些变化。比如 Gmail,它提供了在无 JavaScript 支持环境下的普通版本和有 JavaScript 支持的全功能版本──这是一种渐进式增强的设计理念。但随后几年推出的 Google Doc,已经完全放弃了对无 JavaScript 环境的支持。从这些变化可以看出,JavaScript 已经是 Web 应用的“必需品”。甚至有人把 JavaScript 称为当今最重要的编程语言,从某种意义上这种说法也不过分。
很久以来,我们一直以“脚本”的态度看待JavaScript。程序员对JavaScript 的重视程度很不够,业界对程序员的JavaScript 能力要求也不高。现在,必须做出这种态度的转变。
Rails 3 所做的很大一个改进就是:Unobtrusive JavaScript(非侵入式的 JavaScript),以实现对 HTML 和 JavaScript 代码的分离。比如:
<%= link_to "delete", album_path(@album), :method => :delete, :confirm => "Are you sure?"%>
在 Rails 3 之前,它生成的代码应为(代码进行了省略):
<a href="/albums/1" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; ...
可以看到,生成的 HTML 内嵌了大量的 JavaScript 代码,这是一种不好的做法。Rails 3 所做的其中一个改变,就是分离 HTML 和 JavaScript 代码,生成的 HTML 中内嵌的 JavaScript 代码消失了:
<a href="/albums/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">delete</a>
那么 JavaScript 代码到哪里去了?它们都被放到了一个叫做 Rails.js 的文件中。
跟服务端 MVC 要求职责分离一样,这个原则也应该体现在客户端的代码上。HTML、CSS 和 JavaScript 应该职责明确地各自负责数据、显示和行为。同时,这种分离也对程序员的 JavaScript 能力提出了更高的要求。
性能
从一个请求(Request)的数据传输角度看,数据一般会经历从数据库到服务器,最后到客户端这么一个过程(可能还有其它层次)。数据离客户端越近,响应速度肯定越快。因此,缓存是提升性能的一大利器。
而客户端缓存是离用户最近的地方。关于客户端缓存的一条原则是:不要缓存动态 HTML 页面,但永久缓存一切其它文件类型。Rails 对静态文件的处理很好地体现了这条原则。比如下面这段代码:
stylesheet_link_tag("application")
它生成的 HTML 是:
<link href="/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css"/>
其中application.css?1232285206
的后缀是这个文件的时间戳。那么客户端就可以放心地永久缓存这个静态文静。因为文件一旦更新,客户端就会认为这是一个新的请求,即会去获取最新的文件。
Rails 还在其它很多方面提供了简便的方法使性能优化变得简单,比如服务端缓存机制等。但大多数时候,性能问题源自于我们自己的实现或者设计问题。比如对于 Active Record Query Interface 的的滥用,多数时候性能问题都可以通过完善数据库查询来得到很大的改进。对于数据库查询,我们应该经常关注几个问题,比如:获取的数据结果集中是否有大量无用数据,数据库查询次数是否可以减少等等。
用户体验
用户体验是 Web 应用非常重要的元素,Rails 作为 Web 开发的 DSL 在这方面也有很多关注。 在 Web 应用中我们经常遇到的一个问题是:submit button(提交按钮)被多次点击。如果没有被恰当处理,就会引起表单的多次提交。用 Rails 的 form helper 方法可以很简单地避免这个问题:
submit_tag "Submit", :disable_with => "Submitting..."
代码中的:disable_with
选项的作用是:在 button 被点击之后把它 disable 掉,并且把 button 的文字替换成“Submitting”。一个简单的选项带来了显而易见的好处:不仅避免了多次点击的问题,而且显式地告诉了用户表单正在被提交当中。
Rails 提供了很多便捷的方法,让提升用户体验变得非常容易。作为程序员,我们也应该对用户体验有更多关注,比如如何设计更好的交互来避免 AJAX 所带来的种种用户体验问题等等。
安全
Web 应用面临着很多安全隐患,比如 Session 定置(Session Fixation)、跨站请求伪造(CSRF)和日志信息泄露(Logging)问题。在 Rails 中我们可以用简单到只有一行代码的方式来避免这些安全问题。下面是各安全隐患以及对应策略。
Session 定置
攻击者通过某种方式强制用户使用他所掌握的 Session ID,在用户登录之后攻击者即可使用此 Session ID 窃取用户的信息。解决方案:
reset_session
在登录逻辑中添加此段代码,以在登录之前重置 session,这样便可以防止攻击者通过 Session Fixation 攻击来获得用户信息。
跨站请求伪造
CSRF 是指在页面中注入一些恶意代码或者链接──指向用户使用的其它站点,比如站点 A。当用户访问被污染的页面时,如果刚好站点 A 仍处于有效认证期,则用户在站点 A 的数据就会被侵犯。解决方案:
protect_from_forgery :secret => "123456789012345678901234567890..."
此代码会在非 get 请求中添加一个 security token,如果 token 不一致,则请求将失败。这种方式可以有效防止 CSRF,当然前提是我们正确地使用了 HTTP method。
日志信息泄露
默认情况下,Rails 会把所有的请求信息都记录在日志文件中。那么攻击者就可以通过窃取日志文件,以得到一些秘密信息,比如登录密码、信用卡信息等等。解决方案:
filter_parameter_logging :password
这行代码就可以过滤那些不希望被日志文件记录的信息,比如 password 等,从而避免通过日志来泄露敏感信息。
Web 应用还面临着很多其它安全问题,比如 SQL 注入,XSS 等等。我们应该更多关注 Web 应用所面临的安全问题,并尽可能避免。何况,在 Rails 中要避免大多数问题,方法都很简单。
业务模型
最后一个问题虽然与 Rails 甚至技术的关系并不大,但是却关系到一个 Web 应用质量的最关键问题:创建的 Web 应用是否符合业务模型。我们曾经在一个电子商务应用的开发过程中遇到这么一个问题:整个购买流程的最后一步是支付页面,用于完成支付并生成收据的 PDF 文件。产品交付之后,客户开始抱怨支付页面的性能问题:响应时间超过了容忍度。于是我们试图改进支付页面的性能,但因为支付页面涉及的逻辑和业务实在过多,性能提升很困难。
但当我们重新审视支付页面的业务逻辑时,我们发现这个页面其实包含了两部分功能:支付和 PDF 文件的生成。而从业务角度看,PDF 文件的生成不属于支付过程,而是支付完成之后的逻辑。而且,并不是所有的用户都需要生成 PDF,强制在支付的同时生成 PDF 是一种资源的浪费。所以,我们把支付页面进行了拆分:把生成 PDF 文件的功能移到了支付成功页面,而且只有在客户点击相应链接之后才会生成 PDF。简单的改动之后,不仅性能问题得到了解决,而且应用也更加符合真实的业务流程。
当我们遇到问题或者举步维艰之时,停下来思考一下:我们对业务的理解是否出现了问题,我们的设计是否出现了问题。很多时候我们都可以在这里找到答案。重新思考业务逻辑或者重新设计之后,实现可能会简单很多,甚至也许问题本身都已经不复存在了。
小结
以上谈到的各个元素关注了代码质量、用户体验、性能、设计等等问题。也许这些并没有涉及到什么高深的技术问题,但在一个项目中,我们大多数时候面临的都不是高深的技术难题,而是这些平常的点点滴滴。一个高质量的 Web 应用,正是从这些点点滴滴开始。
关于作者
胡振波, ThoughtWorks 公司咨询师。多年企业级应用开发经验,敏捷开发的一名忠诚实践者和思考者。关注编程技术、互联网发展。
注:本文为 RubyConf China 2010 的演讲《 Build Hi-Q Web Apps on Rails 》的整理和总结,稍有修改和变动。
评论