2007年12月7日,星期五

Rails 2.0:完成!

作者:David

经过大约一年的精心打磨,Rails 2.0 终于正式发布。这是一个令人惊叹的版本,包含了大量精彩的新功能、大量的修复以及令人难以置信的优化。我们甚至移除了不少冗余代码,使整个框架更加精炼和一致。

这对于 Ruby on Rails 来说也是一个重要的里程碑。我个人已经在这个框架上工作了大约四年半,也有不少贡献者与我一同走过了差不多的历程。能够看到我们在此期间取得了如此大的进步,真是令人欣慰。我们证明了最初的炒作是值得的,并且我们能够坚持下来并不断突破界限。

在深入介绍功能之前,我想再次向所有为这个版本的发布做出贡献的人们致以最诚挚的感谢。感谢 Rails 核心团队的成员,感谢成百上千位提交了补丁的贡献者,感谢在过去一年里参与社区活动的每一个人。这个版本的发布是大型开源开发的一个胜利,你们每个人都可以为自己扮演的角色感到无比自豪。干杯!

在抒发完感慨之后,让我们来深入探讨这个精彩的版本,看看其中一小部分新内容。

Action Pack:资源

这是 2.0 版本中大部分工作所在之处。我们在 RESTful 风格方面取得了大量改进。首先,我们用斜杠代替了分号来分隔自定义方法。所以 /people/1;edit 现在变成了 /people/1/edit。我们还为路由资源添加了命名空间功能,这使得限制如管理员界面等功能变得非常容易。

map.namespace(:admin) do |admin| admin.resources :products, :collection => { :inventory => :get }, :member => { :duplicate => :post }, :has_many => [ :tags, :images, :variants ] end

这将为您提供像 inventory_admin_products_url 和 admin_product_tags_url 这样的命名路由。为了跟踪这些命名路由的不断增加,我们添加了“rake routes”任务,它将列出 routes.rb 创建的所有命名路由。

我们还引入了一个新约定,所有基于资源的控制器默认都将是复数形式。这使得单个资源可以在多个上下文中映射,但仍然引用同一个控制器。例如:


  # /avatars/45 => AvatarsController#show
  map.resources :avatars
  
  # /people/5/avatar => AvatarsController#show 
  map.resources :people, :has_one => :avatar

Action Pack:多视图

除了资源方面的改进,多视图也得到了增强。我们已经有了 #respond_to 方法,但我们将其提升了一个层次,使其能够深入到模板。我们将模板的格式与其渲染引擎分离开来。因此,show.rhtml 现在变成了 show.html.erb,这是为声明了 format.html 的 show 操作默认渲染的模板。您现在还可以拥有类似 show.csv.erb 的内容,它针对 text/csv,但同样使用默认的 ERB 渲染器。

所以,模板的新格式是 action.format.renderer。一些例子:

  • show.erb:所有格式使用同一个 show 模板
  • index.atom.builder:使用 Builder 格式(以前称为 rxml)来渲染 application/atom+xml MIME 类型的 index 操作
  • edit.iphone.haml:使用自定义的 HAML 模板引擎(默认不包含)来渲染自定义 Mime::IPHONE 格式的 edit 操作

说到 iPhone,我们现在更容易声明仅用于内部路由的“伪”类型。例如,当您想要一个仅用于 iPhone 的特殊 HTML 界面时。只需要这样做:


  # should go in config/initializers/mime_types.rb
  Mime.register_alias "text/html", :iphone

  class ApplicationController < ActionController::Base
    before_filter :adjust_format_for_iphone
  
    private
      def adjust_format_for_iphone
        if request.env["HTTP_USER_AGENT"] && request.env["HTTP_USER_AGENT"][/(iPhone|iPod)/]
          request.format = :iphone
        end
      end
  end
  
  class PostsController < ApplicationController
    def index
      respond_to do |format|
        format.html   # renders index.html.erb
        format.iphone # renders index.iphone.erb
      end
    end
  end

我们鼓励您在 config/initializers/mime_types.rb 文件中声明自己的 MIME 类型别名。此文件默认包含在新应用程序中。

Action Pack:记录标识

紧随资源的新驱动力,控制器和视图处理 URL 的方法得到了简化。我们添加了许多约定,可以动态地将模型类转换为资源路由。示例:


  # person is a Person object, which by convention will 
  # be mapped to person_url for lookup
  redirect_to(person)
  link_to(person.name, person)
  form_for(person)

Action Pack:HTTP 拥抱

正如您可能已经猜到的,Rails 2.0 的 Action Pack 致力于更紧密地与 HTTP 及其所有优点结合。资源、多重表示,但还有更多。我们添加了一个新模块来处理 HTTP 基本身份验证,这被证明是 over SSL 进行 API 身份验证的绝佳方式。它非常简单易用。这是一个例子(ActionController::HttpAuthentication 中还有更多)。


  class PostsController < ApplicationController
    USER_NAME, PASSWORD = "dhh", "secret"

    before_filter :authenticate, :except => [ :index ]

    def index
      render :text => "Everyone can see me!"
    end

    def edit
      render :text => "I'm only accessible if you know the password"
    end

    private
      def authenticate
        authenticate_or_request_with_http_basic do |user_name, password| 
          user_name == USER_NAME && password == PASSWORD
        end
      end
  end

我们还大大简化了将 JavaScript 和样式表文件组织成逻辑单元的方法,而不会因为请求大量文件而产生 HTTP 开销。使用 javascript_include_tag(:all, :cache => true) 将会在生产环境中将 public/javascripts/.js 合并成一个 public/javascripts/all.js 文件,同时在开发环境中保持文件分离,以便您可以在不清除缓存的情况下进行迭代开发。

同样,我们添加了一个选项来“欺骗”那些不愿意自行执行请求流水线的浏览器。如果您将 ActionController::Base.asset_host 设置为 “assets%d.example.com”,我们将自动将您的资源调用(如 image_tag)分发到 asset1 到 asset4。这允许浏览器同时打开更多的连接,并提高应用程序的感知速度。

Action Pack:安全

开箱即用地使创建安全应用程序更加容易始终是一个令人愉悦的事情,在 Rails 2.0 中,我们正在从多个方面实现这一点。最重要的是,我们现在内置了处理 CRSF 攻击的机制。通过在所有表单和 Ajax 请求中包含一个特殊的令牌,您可以防止外部应用程序发出请求。所有这些在新的 Rails 2.0 应用程序中都默认启用,您可以使用 ActionController::Base.protect_from_forgery(有关更多信息,请参阅 ActionController::RequestForgeryProtection)轻松地在现有应用程序中启用它。

我们还使处理 XSS 攻击变得更加容易,同时仍然允许用户在您的页面中嵌入 HTML。旧的 TextHelper#sanitize 方法已从黑名单(非常难以维护安全)方法转变为白名单方法。如果您已在使用 sanitize,您将自动获得更好的保护。您还可以使用 sanitize 调整默认允许的标签。有关详细信息,请参阅 TextHelper#sanitize。

最后,我们增加了对 HTTP only cookies 的支持。并非所有浏览器都支持它们,但您可以在支持它们的浏览器中使用它们。

Action Pack:异常处理

许多常见的异常在共享级别进行捕获比在每个操作中进行捕获效果更好。这以前可以通过重写 rescue_action_in_public 来实现,但您需要自己编写 case 语句并调用 super。太麻烦了。所以现在我们有一个名为 rescue_from 的类级别宏,您可以使用它来声明性地将特定异常指向一个给定的操作。示例:


  class PostsController < ApplicationController
    rescue_from User::NotAuthorized, :with => :deny_access
    
    protected
      def deny_access
        ...
      end
  end

Action Pack:Cookie 存储会话

Rails 2.0 中的默认会话存储现在是基于 Cookie 的。这意味着会话不再存储在文件系统或数据库中,而是由客户端以一种无法伪造的哈希形式保存。这不仅比传统的会话存储更快,而且还无需维护。无需 cron 作业来清除会话,您的服务器也不会因为您忘记了而突然在 tmp/session 中拥有 500K 个文件而崩溃。

如果您遵循最佳实践并将会话使用限制在最低限度,例如仅存储 user_id 和 flash,则此设置效果很好。但是,如果您计划在会话中存储核弹发射代码,则默认的 Cookie 存储是一个糟糕的选择。虽然它们无法伪造(所以 is_admin = true 是没问题的),但其内容是可见的。如果这对您的应用程序来说是个问题,您可以随时切换回传统的会话存储(但首先要将该需求视为代码异味)。

Action Pack:新的请求剖析器

确定实际使用中的瓶颈可能很困难,但我们通过新的请求剖析器使其变得容易得多,该剖析器可以跟踪整个使用脚本并报告聚合结果。您可以使用它这样做:


  $ cat login_session.rb
  get_with_redirect '/'
  say "GET / => #{path}"
  post_with_redirect '/sessions', :username => 'john', :password => 'doe'
  say "POST /sessions => #{path}"
  $ ./script/performance/request -n 10 login_session.rb

您将收到一份详细的 HTML 和文本报告,说明时间花费在哪里,从而能够很好地了解在哪里可以加快应用程序的速度。

Action Pack:其他

另外值得注意的是 AtomFeedHelper,它使用增强的 Builder 语法使创建 Atom Feed 更加简单。简单示例:


  # index.atom.builder:
  atom_feed do |feed|
    feed.title("My great blog!")
    feed.updated((@posts.first.created_at))
  
    for post in @posts
      feed.entry(post) do |entry|
        entry.title(post.title)
        entry.content(post.body, :type => 'html')
  
        entry.author do |author|
          author.name("DHH")
        end
      end
    end
  end

我们进行了一些性能改进,因此资产标签调用现在更便宜,并且我们正在缓存简单的命名路由,使其速度更快。

最后,我们将 in_place_editor 和 autocomplete_for 移至官方 Rails SVN 上的插件。

Active Record:性能

Active Record 进行了大量的修复和小调整,但大功能方面相对较少。不过,我们添加了一个非常简单的查询缓存,它能够识别同一请求中相似的 SQL 调用并返回缓存的结果。这对于难以通过 :include 或其他机制处理的 N+1 情况特别有用。我们还大幅提高了 fixtures 的性能,这使得大多数基于正常 fixture 使用的测试套件的速度提高了 50-100%。

Active Record:性感的迁移

现在有一种新的替代格式,可以以更有效的方式声明迁移。以前,您会写:

create_table :people do |t| t.column, “account_id”, :integer t.column, “first_name”, :string, :null => false t.column, “last_name”, :string, :null => false t.column, “description”, :text t.column, “created_at”, :datetime t.column, “updated_at”, :datetime end

现在您可以这样写:

create_table :people do |t| t.integer :account_id t.string :first_name, :last_name, :null => false t.text :description t.timestamps end

Active Record:酷炫的 fixtures

Active Record 中的 fixtures 近来受到了不少批评。批评的一个关键点在于处理 fixtures 之间的依赖关系。不得不通过其主键的 id 来关联 fixtures 是一件很麻烦的事情。现在这个问题已经得到了解决,您可以这样编写 fixtures:


  # sellers.yml
  shopify:
    name: Shopify

  # products.yml
  pimp_cup:
    seller: shopify
    name: Pimp cup

正如您所见,不再需要声明 fixtures 的 id,并且不再使用 seller_id 来引用关系,只需使用 seller 和 fixtures 的名称即可。

Active Record:XML 输入,JSON 输出

Active Record 在一段时间以来一直支持序列化到 XML。在 2.0 版本中,我们还增加了反序列化功能,因此您可以说 Person.new.from_xml(“David”) 来获得您期望的结果。我们还增加了序列化到 JSON 的功能,它支持与 XML 序列化相同的语法(包括嵌套关联)。只需调用 person.to_json 即可开始使用。

Active Record:减重

为了让 Active Record 更精简、更强大,我们移除了 acts_as_XYZ 功能,并将它们放入 Rails SVN 仓库中的独立插件。因此,假设您使用的是 acts_as_list,您只需要执行 ./script/plugin install acts_as_list 即可,一切都会照常进行。

更进一步,我们将所有商业数据库适配器都推入了各自的 gem。因此,Rails 现在只提供 MySQL、SQLite 和 PostgreSQL 的适配器。这些是我们容易且愿意访问进行测试的数据库。但这并不意味着商业数据库被抛弃了。相反,它们现在可以拥有独立的发布计划,而不是与主 Rails 发行版绑定。这也许是件好事,因为商业数据库通常需要更多地处理异常和复杂的配置才能正常工作。

商业数据库适配器现在存在于遵循相同命名约定的 gem 中:activerecord-XYZ-adapter。因此,如果您 gem install activerecord-oracle-adapter,您将立即在您机器上的所有 Rails 应用程序中获得 Oracle 作为适配器选择。您无需更改应用程序中的任何一行代码即可使用它。

这也意味着新的数据库适配器更容易在 Rails 世界中获得关注。只要您按照发布的约定打包您的适配器,用户只需安装 gem 即可开始使用。

Active Record:带点语法糖的 with_scope

ActiveRecord::Base.with_scope 现在是 protected 的,以 discourage 人们在控制器中(尤其是在过滤器中)滥用它。取而代之的是,现在鼓励您仅在模型本身中使用它。这就是它的设计目的,也是它逻辑上仍然适合的地方。当然,这都是关于鼓励和 discourage。如果您已经权衡了利弊,仍然想在模型之外使用 with_scope,您始终可以通过 .send(:with_scope) 来调用它。

ActionWebService 退出,ActiveResource 进驻

Rails 在 SOAPREST 的争论中选择了一方,这可能不会让人感到意外。除非您绝对需要出于集成目的使用 SOAP,否则我们强烈不建议您这样做。作为自然延伸,我们已将 ActionWebService 从默认捆绑中移除。它只需要 gem install actionwebservice 即可安装,但它传递了一个重要的信息。

与此同时,我们将新的 ActiveResource 框架从 Beta 版移出,并将其纳入默认捆绑。ActiveResource 就像 ActiveRecord,但用于资源。它遵循类似的 API,并配置为与使用资源驱动方法的 Rails 应用程序“开箱即用”。例如,一个标准的脚手架将可以通过 ActiveResource 访问。

ActiveSupport

ActiveSupport 中没有太多新内容。我们添加了许多新方法,例如 Array#rand 用于从数组中获取随机元素,Hash#except 用于过滤掉哈希中不需要的键,以及对 Date 的大量扩展。我们还通过 assert_difference 使测试更加方便。除此之外,基本上都是修复和调整。

Action Mailer

对于 Action Mailer 来说,这是一个相当小的更新。除了少数错误修复之外,我们还添加了注册替代模板引擎的选项,以及测试套件中的 assert_emails,其工作方式如下:

  1. 断言块内发送的电子邮件数量
    assert_emails 1 do
    post :signup, :name => ‘Jonathan’
    end

Rails:调试器回来了

为了将所有这些内容联系起来,Rails 总体上进行了一系列改进。其中我最喜欢的是调试器形式的断点回归。它是一个真正的调试器,而不仅仅是 IRB 的转储。您可以来回跳转,列出当前位置,等等。这一切都得益于 ruby-debug gem 的慷慨贡献。因此,您需要安装它才能使用新的调试器。

要使用调试器,只需安装 gem,将“debugger”放在应用程序的某个地方,然后使用 —debugger 或 -u 启动服务器。当代码执行到 debugger 命令时,您就可以在运行服务器的终端中直接使用它了。无需 script/breakpointer 或其他任何东西。您也可以在测试中使用调试器。

Rails:清理您的环境

在 Rails 2.0 之前,各处的 config/environment.rb 文件都会塞满各种一次性配置细节。现在您可以将这些元素收集到自包含的文件中,并将它们放在 config/initializers 下,它们将自动加载。新的 Rails 2.0 应用程序附带两个示例:inflections.rb(用于您自己的复数规则)和 mime_types.rb(用于您自己的 MIME 类型)。这应该确保您除了默认设置外,不需要在 config/environment.rb 中保留任何东西。

Rails:更轻松的插件顺序

既然我们已经将相当多的东西从 Rails 中提取出来并放入了插件,您可能还会有其他依赖这些功能的插件。这可能需要您先加载,例如,acts_as_list,然后再加载您自己的 acts_as_extra_cool_list 插件,以便后者扩展前者。

以前,这需要您在 config.plugins 中命名*所有*插件。当您只想说“我只关心 acts_as_list 在其他所有插件之前加载”时,这很麻烦。现在您可以通过 config.plugins = [ :acts_as_list, :all ] 来实现这一点。

以及成百上千的其他改进

上面我所讲的只是 2.0 全套功能中很小一部分。Rails 2.0 中包含了字面意义上的数百个错误修复、调整和功能增强。所有这些都来自于大量热情的贡献者的辛勤工作,他们孜孜不倦地以微小但重要的方式改进框架。

我鼓励您仔细阅读 CHANGELOGs,了解所有更改的更多信息。

那么我如何升级?

如果您想将您的应用程序迁移到 Rails 2.0,您应该首先将其迁移到 Rails 1.2.6。这会包含 2.0 中移除的大部分内容的弃用警告。因此,如果您的应用程序在 1.2.6 上运行良好且没有弃用警告,那么它很有可能可以直接运行在 2.0 上。当然,如果您使用了分页功能,您需要安装 classic_pagination 插件。如果您使用 Oracle,您需要安装 activerecord-oracle-adapter gem。以此类推,适用于所有提取的功能。

那么我如何安装?

要通过 gem 安装,请执行:

gem install rails -y

……如果您在安装时遇到问题(找不到 gem),暂时可以从我们自己的仓库获取 gem:

gem install rails -y —source http://gems.rubyonrails.org

要从 SVN 标签尝试,请使用(根据您当前的 Rails 版本,您可能需要运行此命令两次):

rake rails:freeze:edge TAG=rel_2-0-1

注意:之所以是 2.0.1,是因为我们在发布 2.0.0 后很快发现了一个小问题。