最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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

评论

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

22道js输出顺序问题,你能做出几道

loveX001

JavaScript 前端

新一代通信协议—— RSocket

老周聊架构

响应式编程 2月月更 rsocket

如何快速理解事务隔离

Dinfan

数据库 innodb 事务隔离

一文看懂:近期不断 “狂飙” 的 ChatGPT | 社区征文

架构精进之路

ChatGPT

一文深度解读音视频行业技术发展历程

阿里云视频云

云计算

ChatGPT看技术发展趋势| 社区征文

二哈侠

人工智能 openai ChatGPT

问:React的setState为什么是异步的?

beifeng1996

前端 React

Vue的computed和watch的区别是什么?

bb_xiaxia1998

Vue 前端

一次线上OOM问题分析

艾小仙

Java OOM 问题排查 排查方法

chianmaker交易初探

liwh1227

区块链 共识算法 联盟链架构

研发效能度量标准与实践

思码逸研发效能

研发效能

“堆内存持续占用高 且 ygc回收效果不佳” 排查处理实践

京东科技开发者

前端 堆内存 回收器 JavaScrip 企业号 3 月 PK 榜

前端经典面试题(有答案)

loveX001

JavaScript 前端

前端常见vue面试题(必备)

bb_xiaxia1998

Vue 前端

推荐系统[四]:精排-详解排序算法LTR (Learning to Rank)_ poitwise, pairwise, listwise相关评价指标,超详细知识指南。

汀丶人工智能

自然语言处理 推荐系统 搜索算法

美团前端常见react面试题(附答案)

beifeng1996

前端 React

老生常谈React的diff算法原理-面试版

beifeng1996

前端 React

前端必会react面试题

beifeng1996

前端 React

Vue.$nextTick的原理是什么-vue面试进阶

bb_xiaxia1998

Vue 前端

号码隐私保护服务:保障亿万消费者的隐私安全

阿里云视频云

云计算

YOLOv5全面解析教程⑤:计算mAP用到的Numpy函数详解

OneFlow

人工智能 深度学习

N皇后问题的回溯法实现

老王同学

c++ 八皇后 回溯法

Python:Excel自动化实践入门篇 乙【送图书活动继续】

eng八戒

Python Excel Python自动化办公

根据文本描述生成视频,Tune-A-Video 效果惊艳

Zilliz

计算机视觉

美团前端二面面试题

loveX001

JavaScript 前端

NLP 双数组字典树(double array trie) 基于darts-java改进,增加词性存储。

alexgaoyh

elasticsearch nlp darts-java 词性 double array trie

浅析大促备战过程中出现的 fullGc,我们能做什么?

京东科技开发者

JVM 内存 GC java 企业号 3 月 PK 榜

面试官:说说Event Loop事件循环、微任务、宏任务

loveX001

JavaScript 前端

C++入门简单实例

老王同学

c++ 入门

ChatGPT 不仅是 AI 的成功,也是云计算的成功 | 社区征文

多颗糖

云计算 AI 云原生 ChatGPT

前端一面常见vue面试题合集

bb_xiaxia1998

Vue 前端

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