测试 Rails 5 的新功能

安装

rvm use 2.2.2@rails5
cd ~/Documents/Githubs
git clone --depth 1 git@github.com:rails/rails.git
cd rails
gem install bundler
bundle
railties/exe/rails new --edge fiverails
mv fiverails ~/Documents/Githubs/
cd ~/Documents/Githubs/fiverails
bundle
bundle exec rails s

如果遇到 gem install pg -v '0.18.4' 无法安装的问题,需要先安装 postgresql:

brew install postgresql
ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

Rails 5 的变化

Upcoming Deprecations

  1. to_xml 方法从核心代码中删除了,现在 object.to_xml 会直接抛错
  2. render nothing: true 将从 Rails 5.1 中删除,所以现在也不推荐使用了。新的写法是 head: ok具体 参考这里
  3. before_filter 将从 Rails 5.1 中删除,一律改用 before_action
# rails 4.x
def nothing
  render nothing: true
end

# rails 5.1
def nothing
  head :ok
end

Upcoming Changes to Existing Features

g model post
g model comment post:belongs_to
class CreateComments < ActiveRecord::Migration[5.0]
  def change
    create_table :comments do |t|
      t.belongs_to :post, index: true, foreign_key: true

      t.timestamps
    end
  end
end

从这里我们可以看到,Rails 在创建数据库结构的时候,越来越注意外键约束的问题了。

g model comment post:belongs_to:optional
class CreateComments < ActiveRecord::Migration[5.0]
  def change
    create_table :comments do |t|
      t.belongs_to :post, index: true, foreign_key: true optional: true

      t.timestamps
    end
  end
end

New ActiveRecord Features

or 方法,可以参考另外一篇转载 What’s new in Rails 5

可以看到 Rails 5 改进了验证错误提示,增加了 details 变量。

Comment.new.tap(&:valid?).errors

# Rails 5.x
#=> #<ActiveModel::Errors:0x007f993429c618 @base=#<Comment id: nil, post_id: nil, created_at: nil, updated_at: nil>, @messages={:name=>["can't be blank"], :post=>["must exist"]}, @details={:post=>[{:error=>:blank}]}>

# Rails 4.x
#=> #<ActiveModel::Errors:0x0000010e380bf8 @base=#<Comment id: nil, post_id: nil, created_at: nil, updated_at: nil>, @messages={:name=>["can't be blank"], :post=>["must exist"]}>

Rails 5 提供了更安全的 token 机制,数据库结构默认增加了索引以及唯一性约束。同时添加了动态方法 regenerate_xxx

bin/rails g model MyModel my_token:token

class CreateMyModels < ActiveRecord::Migration[5.0]
  def change
    create_table :my_models do |t|
      t.string :my_token

      t.timestamps
    end
    add_index :my_models, :my_token, unique: true
  end
end

class MyModel < ApplicationRecord
  has_secure_token :my_token
end
m = MyModel.new
m.regenerate_my_token
(0.1ms)  begin transaction
SQL (0.4ms)  INSERT INTO "my_models" ("my_token", "created_at", "updated_at") VALUES (?, ?, ?)  [["my_token", "4HedGeNQG8JRQ4NsTFywvxte"], ["created_at", 2016-01-12 07:29:26 UTC], ["updated_at", 2016-01-12 07:29:26 UTC]]
(3.4ms)  commit transaction

我们不用再关心手动创建 token,Rails 会帮我们处理好一切。

MyModel.create
=> #<MyModel id: 2, my_token: "GqzBgs8NAUcwX1FH5hEGKvE2", created_at: "2016-01-12 07:31:11", updated_at: "2016-01-12 07:31:11">

Rails 4.x 的时候,如果我们想让模型中的 callback 失败,只需要在方法中返回 false 或者 nil,而 Rails 5 开始需要更改为 throw :abort

class Post < ApplicationRecord
  before_save :do

  def do
    throw :abort
    # return false NOT works in rails 5
  end
end
Post.create #=>
(0.1ms)  begin transaction
(0.0ms)  rollback transaction

Rails API

cd ~/Documents/Githubs/rails
railties/exe/rails new --edge --api fiverailsapi
mv fiverailsapi ~/Documents/Githubs/
cd ~/Documents/Githubs/fiverailsapi
bundle
bundle exec rails s
rails g scaffold project name ends_at:timestamp

我们有一个如下的界面

当用户添加评论之后,我们希望把新评论追加到评论列表的末尾。利用 SRJ 我们可以这么写

# app/views/comments/create.js.erb

$("#comments .new-comment").before("<%= escape_javascript(render(partial: 'task/comment', locals: { comment: @comment })) %>")

这样的缺点是我们没办法同时更新原来评论列表中的内容,比如:发布于 xxx 分钟前。

现在我们可以这么写

# app/views/comments/create.js.erb
Turbolinks.visit("<%= project_task_path(@comment.task.project, @comment.task) %>", { change: [ 'comments'] })
%ul.list-unstyled#comments
  - @task.comment.each do |comment|
    = render partial: "comment", locals: { comment: comment }

  %li.new-comment
    = form_for [@task.project, @task, @task.comments.build], remote: true do |f|
      %p
        = f.text_area :body, class: "form-control", placeholder: "Post your comment here."
      %p
        = f.submit 'Post Comment', class: "btn btn-primary"

ActionCable

首先做程序基础骨架

railties/exe/rails new campfire --edge
mv campfire ~/Documents/Githubs/
cd ~/Documents/Githubs/campfire

rails g controller rooms show
rails g model message content:text
rails db:migrate
# routes.rb
root to: 'rooms#show'
#app/controllers/rooms_controller.rb
def show
  @messages = Message.all
end
#app/views/messages/_message.html.erb
<div class="message">
  <%= message.content %>
</div>

#app/views/rooms/show.html.erb
<h1>Rooms#show</h1>
<%= render @messages %>

然后添加 ActionCable 并配置

rails g channel room speak
# routes.rb
mount ActionCable.server => '/cable'
#app/assets/javascripts/cable.coffee
@App ||= {}
App.cable = ActionCable.createConsumer()

现在启动 rails server 并打开首页,可以看到出现了 <meta name="action-cable-url" content="/cable" /> 这一行:

利用 console 工具做点小测试:

App.cable
App.room.speak

继续搞起:

#app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    alert data['message']

  speak: (message) ->
    @perform 'speak', message: message
#app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    ActionCable.server.broadcast 'room_channel', message: data['message']
  end
end

利用 console 工具做点小测试 App.room.speak('hello world'),只要没敲错代码就能看到弹出的对话框了。

继续改进

#app/views/rooms/show.html.erb
<h1>Rooms#show</h1>

<div id="messages">
  <%= render @messages %>
</div>

<form>
  <label>Say something:</label><br>
  <input type="text" data-behavior="room_speaker">
</form>
#app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    $('#messages').append data['message']

  speak: (message) ->
    @perform 'speak', message: message

$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.room.speak event.target.value
    event.target.value = ''
    event.preeventDefault()

继续改进

rails g job MessageBroadcast
#app/channels/room_channel.rb
def speak(data)
  Message.create! content: data['message']
end

#app/models/message.rb
class Message < ApplicationRecord
  after_create_commit { MessageBroadcastJob.perform_later self }
end

#app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast 'room_channel', message: render_message(message)
  end

  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
    end
end

最后一点优化,增加缓存

#app/views/messages/_message.html.erb
<% cache message do %>
  <div class="message">
    <%= message.content %>
  </div>
<% end %>

Sprockets 4

Sprockets 4 最值得介绍的改变是关于 manifest.js

过去我们在 config/initializers/assets.rb 中指定哪些 assets 文件支持预编译。在 Sprockets 4 改用 app/assets/config/manifest.js 进行这项设置。事实上 Sprockets 3 就支持该功能,但是 sprockets-rails 里面进行了判断,只有当我们使用 Sprockets 4 的时候,才开启这项该功能。

# sprockets-rails/lib/sprockets/railtie.rb

if using_sprockets4?
  config.assets.precompile  = %w( manifest.js )
else
  config.assets.precompile  = [LOOSE_APP_ASSETS, /(?:\/|\\|\A)application\.(css|js)$/]
end

需要注意的一点是,在 Rails 5 的正式版本中,manifest 的文件后缀可能修改成 yml,关于这一点开发团队仍在讨论中。

下面来看一个 manifest.js 的例子,演示如何关联 JS, CSS, fonts, images:

// JS and CSS bundles
//
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css


// Images and fonts so that views can link to them
//
//= link_tree ../fonts
//= link_tree ../images

可以看到现在需要明确的指定:预编译字体和图片。

另外 link_directory 的意思是把这两个目录中的 .coffee.scss 文件分别编译成 .js.css。而 Images 和 fonts 则不需要编译。

现在我们可以从 config/initializers/assets.rb 中删除 config.assets.precompile 配置了:

config.assets.precompile += %w(
  all.css all.js
)

对于小型的应用,可以直接删除 config/initializers/assets.rb,而像 Basecamp 这样的大型应用,因为有一些额外配置,所以还是需要保留它。

相关链接

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

奉献爱心