写点什么

用 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:005037

评论

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

打造IM生态,WorkPlus个性定制让企业业务管理再升级

WorkPlus

五分钟实现pdf分页

程序员架构进阶

PDF 2月春节不断更 源码搭建 2月日更 pdfbox

《自定义工作流配置,springboot集成activiti,前端vue,完整版审批单据》

金陵老街

基于Prometheus和Grafana实现对SpringBoot 应用的监控

皮特王

监控 Grafana Prometheus 大屏展示 告警平台

由ChatGPT引发的关于AI的一些思考

xiaoboey

AI ChatGPT

热点面试题:协商缓存和强缓存的理解及区别?

Immerse

JavaScript https 面试题 HTTP 前端面试题

10w+训练标签?成本太高!PaddleNLP情感分析赋能消费“回暖”

飞桨PaddlePaddle

paddle nlp 飞桨

静态导航页设计与开发

AR7

团队管理 导航网站 vue next

一文读懂 Zebec Chain 的“先行网络” Nautilus 链

股市老人

软件开发如何做好需求管理?方法+工具

爱吃小舅的鱼

产品经理 管理工具 软件需求管理

设计模式-组合模式和建筑者模式详解

C++后台开发

数据结构 设计模式 组合模式 后端开发 Linux服务器开发

INFINI 产品更新啦 20230210

极限实验室

elasticsearch 极限实验室 极限网关 infini gateway INFINI Console

状态机设计中的关键技术

timerring

FPGA

架构训练营模块五作业

gigifrog

架构训练营

携手共进丨九科信息入围PKS体系生态企业展播

九科Ninetech

自动驾驶过冬,需要点燃“降本增效”的炉火

脑极体

自动驾驶

泼辣修图2023最新版本修图工具功能介绍

茶色酒

泼辣修图2023

微信客服接口的返回数据

HoneyMoose

状态机设计中的关键技术

timerring

FPGA

软件测试/测试开发 | Web测试方法与技术之JavaScript 讲解

测试人

软件测试 自动化测试 测试开发 Web自动化测试 web测试

一文读懂 Zebec Chain 的“先行网络” Nautilus 链

鳄鱼视界

如何通过极狐GitLab 平滑落地 Java 增量代码规范?

极狐GitLab

Java DevOps 代码规范 极狐GitLab checkstyle

私有化即时通讯软件可以保证员工的通讯安全吗?

WorkPlus

架构实战 6 - 电商微服务拆分

架构实战营 「架构实战营」

微信 API 中调用客服消息接口提示错误返回限制

HoneyMoose

状态机设计中的关键技术

timerring

FPGA

广告商、影视剧和晚会用的流行歌曲,版权都是怎么买的?

HIFIVE音加加

音乐 三体 版权 影视 热点

代码分享 | 情人节表白黑科技

鼎道智联

代码 情人节 爱心代码

CrossOver2023永久版虚拟机软件下载

茶色酒

CrossOver2023

SpringBoot 三大开发工具,你都用过么?

程序员大彬

springboot

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