What's new in Rails 5

今年的 RailsConf 上宣布了 Rails 5 即将发布。DHH 介绍了一些新版本 Rails 将要带来的功能。

他再一次阐述如何用 Rails 打造一个统一的 Web 应用程序。不管你是否赞同他的观点,事实是现代的 Web 应用程序已经不仅仅是 HTML 和 CSS 了, Rails 世界也看到了这一点,这也是为什么 ActionCable 出现在 Rails API 中。

Web 的世界变化了,Rails 也要跟着变化。让我们来快速看看 Rails 5 中都要带来什么样的改变。

Support for Ruby 2.2.2 or newer

Rails 5 是 Rails 的一个重要版本。它废弃或添加了一些 API,同时改进了一些现存的 API。并且吸收了新版本 Ruby 的一些优势。

Rails 5 仅支持 Ruby 2.2.2 及更高版本。

在 Rails 程序中,我们经常使用 symbols。但是在过去的 Ruby 版本中,symbols 不会被垃圾回收,以至于可能被 DOS attacks 造成内存耗尽。

Ruby 2.2.0 引入的变化之一就是垃圾回收可以回收 symbols 了,参见这里

Rails 5 使用 Ruby 2.2.2 的另外一个原因是,新的 Incremental GC 会帮助 Rails 程序减少内存使用。

我们从 Rails 5 的源码中发现,它同样使用了 Ruby 2.0 带来的新特性,比如:Keyword ArgumentsModule#prepend。关于 Keyword Arguments 和 Module#prepend 请阅读下面的相关链接。

API Deprecation and

通过查看 Rails 的 commits,我们可以看到被删除的和将被废弃的 API。

被删除的 API 包括:

ActionMailer

  • #deliver#deliver!
  • email views 中的 *_path 的辅助方法

ActiveRecord

  • 仍然支持 protected_attributes gem 和 activerecord-deprecated_finders gem

ActionPack assertions

  • assert_templateassigns() 断言将被废弃,并且被移入了 rails-controller-testing gem

需要注意的是 Rails 5 目前仍然包含一些无用的代码和测试。此外,mocha 从 Rails 中移除,推荐使用 Minitest 原生的方法来替代。

Performance improvements

基于 Ruby 2.2.2,Rails 5 的性能得到了改善,使用更少的内存,在 GC 上消耗的时间也更少。仅这一点就给性能带来了巨大的提升,但它并不是 Rails 5 性能得到改善的唯一原因。

核心团队决心打造更好更快的框架:减少对象分配,冻结字符串,去除不必要的依赖,优化常规操作等等都让 Rails 5 变得更快。

以下的 commits 集中在性能上。通过这些例子,我们将会学习到如何减少对象分配或代码优化的经验,这可以帮助我们改善自己的应用程序。

So, what is new in Rails 5?

上面介绍了 Rails 5 将会如何变快,下面介绍它的新特性和新 API。

or method in ActiveRecord::Relation

ActiveRecord::Relation 终于有 #or 方法了,它允许我们写出如下的查询:

Book.where('status = 1').or(Book.where('status = 3'))
\# => SELECT * FROM books WHERE (status = 1) OR (status = 3)

#or 方法允许我们将两个查询关联到一起。它也支持 model 的 scope 方法。

class Book < ActiveRecord::Base
  scope :new_coming, -> { where(status: 3) }
end

Book.where('status = 1').or(Book.new_coming)
\# => SELECT * FROM books WHERE (status = 1) OR (status = 3)

当有多个 #or 关联的时候

User.where(a).where(b).or(User.where(c)) #=> (A && B) || C
User.where(a).or(User.where(b)).where(c) #=> (A || B) && C

belongs_to is required by default

现在 Rails 有一个新的设置选项 config.active_record.belongs_to_required_by_default = true。当保存一个 model 对象时,如果与它 belongs_to 关联的对象不存在,则会触发 validation error。

config.active_record.belongs_to_required_by_default = false 会恢复旧版本 Rails 的行为。我们也可以通过 belongs_tooptional: true 参数来禁用验证。

class Book < ActiveRecord::Base
  belongs_to :author, optional: true
end

ActiveRecord’s attribute API

这个新的 API 让我们可以在 ActiveRecord models 层面,覆盖一个属性的类型。

设想一下,我们在数据库中给某个字段定义为 decimal 类型,但是在程序中我们只关心该字段的整数部分。

利用新的 attribute 方法,我们可以这样做:

class Book < ActiveRecord::Base
end

book.quantity # => 12.0

class Book < ActiveRecord::Base
  attribute :quantity, :integer
end

book.quantity # => 12

在 model 层,我们用 integer 覆盖掉了从数据库结构中自动继承来的 decimal 类型。当每次 model 和数据库交互的时候,底层仍然会使用 decimal 类型来存储数据。

现在,利用 ActiveRecord::Type::Value 按照它的规则实现 #cast, #serialize, #deserialize 就能定义我们自己的类型。

自定义类型也会使用 ActiveModel::Dirty 来跟踪模型中的变化。当然,这些新属性也可以是虚拟的,所以无需在数据表中有对应字段。

has_secure_token landed in ActiveRecord

ActiveRecord model 现在有一个很容易使用的 token attributes 功能。它经常被用到如下场景:邀请码 invitation token 或者 重置密码 password reset token。

class Invite < ActiveRecord::Base
  has_secure_token :invitation_code
end

invite = Invite.new
invite.save
invite.invitation_code # => 44539a6a59835a4ee9d7b112
invite.regenerate_invitation_code # => true

SecureRandom 可以生成 24 个字母长度的 token。

我们需要通过如下方法生成 token 字段。通过这个迁移,也会同时给 token 创建唯一索引。

$ rails g invite invitation_code:token

MySQL ActiveRecord adapter gets JSON support

如果你的 Rails 应用程序的数据库是 MySQL 5.7.8+,那么你的数据库原生支持 JSON 数据类型。

Rails 5 的 ActiveRecord models 也支持这一数据类型。

Render a template outside controllers

Rails 5 允许我们在 controller 外 render templates 或 inline code。这个特性在我们使用 ActiveJob 和 ActionCable 时非常重要。

该特性由 ActionController::Renderer 实现,这一模块默认已经被混入 ApplicationController 类。

# render inline code
ApplicationController.render inline: '<%= "Hello Rails" %>' # => "Hello Rails"

# render a template
ApplicationController.render 'sample/index' # => Rendered sample/index.html.erb within layouts/application (0.0ms)

# render an action
SampleController.render :index # => Rendered sample/index.html.erb within layouts/application (0.0ms)

# render a file
ApplicationController.render file: ::Rails.root.join('app', 'views', 'sample', 'index.html.erb') # =>   Rendered sample/index.html.erb within layouts/application (0.8ms)

下面演示如何在 render 的时候插入 assignslocals:

# Pass assigns
ApplicationController.render assigns: { rails: 'Rails' }, inline: '<%= "Hello #{@rails}" %>' # => "Hello Rails"

# Pass locals
ApplicationController.render locals: { hello: 'Hello' }, assigns: { rails: 'Rails' }, inline: '<%= "#{hello} #{@rails}" %>' # => "Hello Rails"

假如,我们要使用像 root_url 一样的路由方法或者 environment 之类的环境变量之类的该怎么办呢?参考如下:

ApplicationController.render inline: '<%= root_url %>' # => "http://example.org/"

# Inspect ActionController::Renderer environment
ApplicationController.renderer.defaults # => {:http_host=>"example.org", :https=>false, :method=>"get", :script_name=>"", "rack.input"=>""}

# To modify this environment we have to explicitly create a renderer
renderer = ApplicationController.renderer.new(
    http_host: 'michelada.io'
  ) # => #<#<Class:0x007fdf9985a338>:0x007fdf947981c0 @env={"HTTP_HOST"=>"michelada.io", "HTTPS"=>"off", "SCRIPT_NAME"=>"", "rack.input"=>"", "REQUEST_METHOD"=>"GET", "action_dispatch.routes"=>#<ActionDispatch::Routing::RouteSet:0x007fdf93d29450>}>

renderer.render inline: '<%= root_url %>' # => "http://michelada.io/"

Better Minitest test runner

自动 Rails 移除了 test_unit 之后,我们都用 Minitest 来写测试。大家都很喜欢 Minitest 的简洁,而且它的功能也和 RSpec 不相上下,Minitest 提供了我们测试所需要的一切。

唯一的问题是 runner 看起来太简陋了。不过现在我们不用担心了,Rails 改进了这块 参考这里

现在,当你执行 bin/rails test -h 的时候,你会得到如下的帮助:

  • 根据模式过滤执行测试
  • 仅执行指定文件的某一行代码进行测试
  • 仅执行某一文件或某个目录下的测试文件
$ bin/rails test -h
minitest options:
    -h, --help                       Display this help.
    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
    -v, --verbose                    Verbose. Show progress processing files.
    -n, --name PATTERN               Filter run on /regexp/ or string.

Known extensions: pride, rails
    -p, --pride                      Pride. Show your testing pride!

Usage: bin/rails test [options] [files or directories]
You can run a single test by appending a line number to a filename:

    bin/rails test test/models/user_test.rb:27

You can run multiple files and directories at the same time:

    bin/rails test test/controllers test/integration/login_test.rb

Rails options:
    -e, --environment ENV            Run tests in the ENV environment
    -b, --backtrace                  Show the complete backtrace

执行测试后,会给你提供如下错误报告:

$ bin/rails test
Run options: --seed 41988

# Running:

F

Finished in 0.028552s, 35.0239 runs/s, 35.0239 assertions/s.

  1) Failure:
UserTest#test_the_truth [/Users/marioch/Development/proyectos/edge/cinco/test/models/user_test.rb:5]:
Failed assertion, no message given.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

Failed tests:

bin/rails test test/models/user_test.rb:4

自从 Rails 4 起 Turbolinks 就成为了 Rails 的一部分,不管你喜欢或讨厌这个功能,这不在本文的讨论范围。

Rails 5 将使用新版本的 Turbolinks,借助 HTML5 的 custom data attributes,我们可以更快的渲染 Rails 程序。

新版本中最有意义的改变要数局部替换功能。客户端可以告诉 Turbolinks,有哪部分内容改变需要被替换,哪些不需要。

Turbolinks 查看 HTML5 custom attributes 的 data-turbolinks-permanentdata-turbolinks-temporary 属性,来决定是否替换 DOM

客户端使用 Turbolinks.visitTurbolinks.replace 实现更新 DOMvisitreplace 的区别是:前者会触发一个 GET 请求,从服务器取回 HTML 片段,当 replace 执行时用来替换 DOM

这两个函数,都可以传递一组 id 的 Hash 或者 Array,用来标示哪些 HTML 元素需要改变或保持不变。

Action Result
Turbolinks.visit(url, { change: [‘entries’] }) Will replace any DOM element with custom attribute data-turbolinks-temporary and any element with its id listed in change.
Turbolinks.visit(url) Will keep only DOM elements with custom attribute data-turbolinks-permanent and replace everything.
Turbolinks.visit(url, { keep: [‘flash’] }) Will keep only DOM elements with custom attribute data-turbolinks-permanent and any element with its id listed in keep, everything else will be replaced.
Turbolinks.visit(url, { flush: true }) Will replace everything

We can trigger the same functionality from the server-side with redirect_to and render, both can receive change, keep and flush as options but redirect_to can also receive turbolinks with true or false to force a redirect with or without Turbolinks.

我们也可以像 server 端一样实现 redirect_torender,它们也都接受 change, keep, flush 参数。redirect_to 也接受 turbolinks 参数以决定是否利用 Turbolinks 重定向。

不管你是否喜欢 Turbolinks,都可以在项目中试试它是否适合你的需求。

Rails API

Rails API 是 Rails 提供的创建 API 服务的方法。它通过移除不必要的中间件,确保 Rails API 应用速度更快。

Rails 5 这一 gem 被集成进了 Rails 框架中,这意味着你不必再单独引入一个 gem 了。我们可以通过如下方式创建一个 API 应用:

$ rails new michelada-api --api

该应用在 Gemfile 中添加 ActiveModelSerializers 并删除 JQueryTurbolinks gems。同样,config/application.rbaplication_controller.rb 文件添加 config.api_only = true 并去掉 CSRF protection。所有 Controller 继承自 ActionController::API。

# application.rb file
module MicheladaApi
  class Application < Rails::Application
    config.api_only = true
  end
end

# application_controller.rb file
class ApplicationController < ActionController::API
end

更多细节请参考本文末尾的相关链接。

ActionCable

ActionCable 是 Rails 5 出现的新特性,它是基于 web sockets 的实时通信框架。

一个 ActionCable 可以提供一个或多个频道,消费者可以通过 web socket 连接来订阅这些频道。每个频道实时广播消息给所有订阅者。

目前 ActionCable 依赖 Redis 的 PubSub 和 Ruby 端的 faye-websocket、celluloid,当然在未来可能发生变化。

一个含有 ActionCable 的应用需要下面 3 中基本要素:

  • Connection: Is a class derived from ActionCable::Connection::Base, this is where authorization and connection establishments happen.
  • Channel: This is what we will expose via web sockets and it is derived from ActionCable::Channel::Base
  • Client: Is a javascript library to become an ActionCable client.

ActionCable 还需要一个额外的服务来响应 web sockets 连接。

如果你需要详细了解具体用法,那么这里有一个例子 Action Cable Examples

Conclusions

本文并没有涵盖 Rails 5 的最终全部特性,让我们一起期待新版本的 Rails 最终发布吧。

是时候准备升级我们的代码了,如果你是 Rails 4 的话,这并不会太难。

相关链接

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Oct 4, 2015

奉献爱心