BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

用Rails创建高质量Web应用

| 作者 胡振波 关注 0 他的粉丝 发布于 2010年7月23日. 估计阅读时间: 17 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

越来越多的企业开始选择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》的整理和总结,稍有修改和变动。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

Re:经验之谈 by see sai

赞一个.^-^

谢谢分享 by 王 延成

非常棒,谢谢.

谢谢分享 by yaning zhang

rails作为一个web框架越来越成熟,配合ruby强大的dsl能力,很好。谢谢分享。

学习了 by Wang Audrey

写得很好!

谢谢分享 by 曹 庆冲

受教了!!!谢谢!!!

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

5 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT