写点什么

用 Rails 创建高质量 Web 应用

  • 2010-07-22
  • 本文字数:4717 字

    阅读完需:约 15 分钟

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

2010-07-22 00:005111

评论

发布
暂无评论
发现更多内容

【我和openGauss的故事】kettle连接openGauss 5.0.0 数据库

daydayup

openGauss数据库源码解析系列文章——安全管理源码解析(四)

daydayup

数字鸿沟,让气候脆弱者更脆弱

脑极体

AI气象

数智双擎,算融未来”,2023东湖算力与大数据创新大会圆满召开

彭飞

网上正规实体平台现场同步yscy898

新百盛娱乐yscy898

上线规则 微咨询 Fil币现在进场合适吗? 简单查询

局域网与Kubernetes内部网络如何互通

程序员半支烟

k8s

【我和openGauss的故事】体验openGauss 5.0极简版一主一备部署,延时回放和主备切换功能

daydayup

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

西柚子

【我和openGauss的故事】openGauss初体验

daydayup

【我和openGauss的故事】openGauss主备集群节点的添加与删除

daydayup

第二届广州·琶洲算法大赛报名截止 3300多支队伍将展开激烈角逐

新消费日报

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

BlockChain先知

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

鳄鱼视界

15. 系统内置模块

茶桁

Python 模块

成为大主播的必懂知识:直播源码推流

山东布谷网络科技

直播推流 直播源码

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

大瞿科技

【我和openGauss的故事】openGauss5.0企业版集群一主一备安装V1.0

daydayup

网上正规实体现场同步平台

新百盛娱乐yscy898

【我和openGauss的故事】构建openGauss开发编译提交一体化环境

daydayup

SpringBoot3数据库集成

Java 架构 springboot SpringBoot3

局域网与Kubernetes内部网络如何互通

程序员半支烟

k8s

一个SAT求解器及其JavaScript实现

Yuet

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

威廉META

C++语法中bitset位图介绍及模拟实现

芯动大师

openGauss数据库源码解析系列文章——安全管理源码解析(三)

daydayup

2023-08-12:用go语言写算法。实验室需要配制一种溶液,现在研究员面前有n种该物质的溶液, 每一种有无限多瓶,第i种的溶液体积为v[i],里面含有w[i]单位的该物质, 研究员每次可以选择一瓶

福大大架构师每日一题

左程云 福大大架构师每日一题

从 Zebec Protocol 长期布局看,ZBC 通证的潜在应用场景

股市老人

局域网与Kubernetes内部网络如何互通

程序员半支烟

k8s

【我和openGauss的故事】openGauss 3.1.1企业版主备集群升级至5.0.0操作指南

daydayup

【我和openGauss的故事】openGauss索引推荐功能测试

daydayup

用Rails创建高质量Web应用_Ruby_胡振波_InfoQ精选文章