今年的 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 Arguments 和 Module#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_template
和 assigns()
断言将被废弃,并且被移入了 rails-controller-testing
gem
需要注意的是 Rails 5 目前仍然包含一些无用的代码和测试。此外,mocha
从 Rails 中移除,推荐使用 Minitest
原生的方法来替代。
基于 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_to
的 optional: 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
的时候插入 assigns
和 locals
:
# 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
Turbolinks 3
自从 Rails 4 起 Turbolinks 就成为了 Rails 的一部分,不管你喜欢或讨厌这个功能,这不在本文的讨论范围。
Rails 5 将使用新版本的 Turbolinks,借助 HTML5 的 custom data attributes,我们可以更快的渲染 Rails 程序。
新版本中最有意义的改变要数局部替换功能。客户端可以告诉 Turbolinks,有哪部分内容改变需要被替换,哪些不需要。
Turbolinks 查看 HTML5 custom attributes 的 data-turbolinks-permanent
和 data-turbolinks-temporary
属性,来决定是否替换 DOM。
客户端使用 Turbolinks.visit
或 Turbolinks.replace
实现更新 DOM。 visit
和 replace
的区别是:前者会触发一个 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_to
和 render
,它们也都接受 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
并删除 JQuery
和 Turbolinks
gems。同样,config/application.rb
和 aplication_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 的话,这并不会太难。
相关链接