rails5 异步
Rails 5 has introduced many new cool features and ActionCable is probably the most anticipated one. To put it simply, ActionCable is a framework for real-time configuration over web sockets. It provides both client-side (JavaScript) and server-side (Ruby) code and so you can craft sockets-related functionality like any other part of your Rails application. I really like this new addition and recommend giving it a shot.
Rails 5 引入了许多新的很酷的功能,而ActionCable可能是最令人期待的功能。 简而言之,ActionCable是用于通过Web套接字进行实时配置的框架。 它提供了客户端(JavaScript)和服务器端(Ruby)代码,因此您可以像Rails应用程序的任何其他部分一样设计与套接字相关的功能。 我真的很喜欢这个新功能,建议您试一试。

There are a handful of introductory tutorials on the Internet explaining how to get stated with ActionCable, however students often ask me how to introduce file uploading functionality over web sockets. This topic is not really covered anywhere, so I decided to research it myself.
互联网上有一些入门教程,说明如何使用ActionCable进行陈述,但是学生经常问我如何通过Web套接字引入文件上传功能。 这个主题在任何地方都没有涉及,因此我决定自己研究。
In this two-parted tutorial we will create a basic chat application powered by Rails 5.1 and ActionCable with the ability to upload files. We will utilize Clearance for authentication as well as Shrine and FileReader API for file uploading.
在这个分为两部分的教程中,我们将创建一个由Rails 5.1和ActionCable驱动并具有上传文件功能的基本聊天应用程序。 我们将使用Clearance进行身份验证,并使用Shrine和FileReader API进行文件上传。
The source code for this article is available at GitHub. The final application will look like this:

GitHub上提供了本文的源代码。 最终的应用程序将如下所示:
In the first part of the tutorial we are going to create a new application, introduce basic authentication, integrate ActionCable and utilize ActiveJob for the broadcasting. Shall we proceed?
在本教程的第一部分中,我们将创建一个新应用程序,介绍基本身份验证,集成ActionCable并利用ActiveJob进行广播。 我们可以继续吗?
奠定基础 ( Laying Foundations )
Start off by creating a new Rails application:
首先创建一个新的Rails应用程序:
rails new ActionCableUploader
At the time of writing the newest version of Rails was 5.1.1, so I am going to use it for this demo. Please note that ActionCable is not included in Rails 4 and older.
在撰写本文时,Rails的最新版本是5.1.1,因此我将在本演示中使用它。 请注意,ActionCable不包含在Rails 4和更早的版本中。
We will require a basic authentication system. To speed things up, we are not going to write it from scratch but rather use some third-party solution. The most obvious choice that comes to mind is probably Devise but let's make things a bit more interesting and use another solution called Clearance. This gem is similar to Devise, but is intended to be smaller and simpler. After all, we really do need something simple, as this article is not about authentication solutions. Clearance was created by Thoughtbot, the guys who brought us Paperclip, FactoryGirl and other great solutions.
我们将需要一个基本的身份验证系统。 为了加快速度,我们不会从头开始编写它,而是使用一些第三方解决方案。 想到的最明显的选择可能是Devise,但让我们做些有趣的事情,并使用另一个名为Clearance的解决方案。 该宝石类似于Devise,但旨在使其更小,更简单。 毕竟,我们确实确实需要一些简单的操作,因为本文与认证解决方案无关。 Clearance是由Thoughtbot创建的,他们为我们带来了Paperclip , FactoryGirl和其他出色的解决方案。
So, drop a new gem into the Gemfile:
因此,将一个新的gem放入Gemfile中 :
gem'clearance', '~> 1.16'
and then run:
然后运行:
bundleinstall
rails generate clearance:install
The latter command is going to equip your application with the Clearance's code. It is going to perform the following operations:
后面的命令将为您的应用程序配备Clearance的代码。 它将执行以下操作:
- Create a
User
model and the corresponding migration. If you already have a model with such name, it will be tweaked properly 创建一个User
模型和相应的迁移。 如果您已经有一个具有这种名称的模型,则将对其进行适当的调整 - Create an initializer file for Clearance. You are welcome to check it out and modify as needed 为清除创建一个初始化文件。 欢迎您检出并根据需要进行修改
- Insert a
Clearance::Controller
module into theApplicationController
将一个Clearance::Controller
模块插入ApplicationController
- Make you a coffee (well, actually it won't) 给你煮咖啡(嗯,实际上不会)
When you are ready, apply the migration:
准备就绪后,请应用迁移:
rails db:migrate
That's it, the preparations are done and we can move to the next section!
就是这样,准备工作已经完成,我们可以进入下一部分!
添加聊天页面 ( Adding Chat Page )
What I want to do now is create the chat page and restrict access to it. The corresponding controller will be called ChatsController
. Add a root route now:
我现在要做的是创建聊天页面并限制对其的访问。 相应的控制器将称为ChatsController
。 立即添加根路由:
# config/routes.rb
root 'chats#index'
Don't forget to create the controller itself:
不要忘记创建控制器本身:
# controllers/chats_controller.rb
class ChatsController < ApplicationController
before_action :require_login
def index
end
end
The before_action :require_login
line, as you've probably guessed, restricts access to all actions of the current controller. This action does pretty much the same as the authenticate_user!
method in Devise.
您可能已经猜到, before_action :require_login
行限制了对当前控制器的所有操作的访问。 该操作与authenticate_user!
几乎一样authenticate_user!
Devise中的方法。
Now create a views/chats/index.html.erb view that will have only a header for now:
现在创建一个views / chats / index.html.erb视图,该视图现在仅具有标题:
<h1>Demo Chat</h1>
Lastly, populate your application layout with the following contents to display a sign out link and flash messages (if any):
最后,使用以下内容填充您的应用程序布局,以显示退出链接和Flash消息(如果有):
<!-- views/layouts/application.html.erb -->
<% if signed_in? %>
Signed in as: <%= current_user.email %>
<%= button_to 'Sign out', sign_out_path, method: :delete %>
<% else %>
<%= link_to 'Sign in', sign_in_path %>
<% end %>
<div id="flash">
<% flash.each do |key, value| %>
<%= tag.div value, class: "flash #{key}" %>
<% end %>
</div>
Now start the server and navigate to http://localhost:3000
. You should see a page similar to this one.

现在启动服务器并导航到http://localhost:3000
。 您应该看到类似于此页面的页面。
Register with some sample credentials—after that you should be able to see the chat page which means everything is working just fine.
使用一些示例凭证注册-之后,您应该可以看到聊天页面,这意味着一切正常。
留言内容 ( Messages )
Now let's create a new model and the corresponding table. I'll call the model Message
which is quite an unsuprising name. It will have a body and a foreign key to establish an association to the users
table:
现在让我们创建一个新模型和相应的表。 我将其称为Message
模型,这是一个令人惊讶的名称。 它将具有一个主体和一个外键来建立与users
表的关联:
rails g model Message user:belongs_to body:text
rails db:migrate
Make sure that your models have the proper associations set up, as we want each message to have an author (that is, a user):
确保您的模型已设置正确的关联,因为我们希望每个消息都具有一个作者(即用户):
# models/user.rb
has_many :messages, dependent: :destroy
# models/message.rb
belongs_to :user
Brilliant. Next, of course, we'll need a form to actually send a message. To render it, I am going to use a new helper method called form_with introduced in Rails 5 which is meant to replace form_for
and form_tag
(though the latter methods are still supported). The form will be processed by JavaScript, so I'll use #
for the URL. Place the following code into your views/chats/index.html.erb view:
辉煌。 接下来,当然,我们需要一个表格来实际发送消息。 为了渲染它,我将使用Rails 5中引入的名为form_with的新辅助方法,该方法旨在替换form_for
和form_tag
(尽管仍支持后者)。 表单将由JavaScript处理,因此我将使用#
作为URL。 将以下代码放入您的views / chats / index.html.erb视图中:
<div id="messages">
<%= render @messages %>
</div>
<%= form_with url: '#', html: {id: 'new-message'} do |f| %>
<%= f.label :body %>
<%= f.text_area :body, id: 'message-body' %>
<br>
<%= f.submit %>
<% end %>
Note that the form_with
does not generate any ids for the tags so I am adding them manually to further select these elements using JS.
请注意, form_with
不会为标签生成任何id,因此我手动添加它们以使用JS进一步选择这些元素。
I've also provided the #messages
block to render all the messages. This requires the views/messages/_message.html.erb partial to be present, so add it now:
我还提供了#messages
块来呈现所有消息。 这要求显示views / messages / _message.html.erb部分,所以现在添加它:
<div class="message">
<strong><%= message.user.email %></strong> says:
<%= message.body %>
<br>
<small>at <%= l message.created_at, format: :short %></small>
<hr>
</div>
Lastly, load the messages inside the index
action:
最后,在index
动作中加载消息:
# chats_controller.rb
def index
@messages = Message.order(created_at: :asc)
end
This will sort them by creation date, ascending. Your chat page will look something like this:

这将按创建日期对其进行升序排列。 您的聊天页面将如下所示:
ActionCable:采取行动了! ( ActionCable: Time for Action! )
Our next task is to enable real-time conversation between the client and the server powered by the ActionCable's magic. Let's start with adding some configuration:
我们的下一个任务是在ActionCable的魔力的支持下,在客户端和服务器之间实现实时对话。 让我们从添加一些配置开始:
# config/environments/development.rb
config.action_cable.url = 'ws://localhost:3000/cable'
config.action_cable.allowed_request_origins = [ 'http://localhost:3000', 'http://127.0.0.1:3000' ]
Next, a route:
接下来,一条路线:
# routes.rb
# ...
mount ActionCable.server => '/cable'
Lastly, meta tags:
最后,元标记:
<!-- views/layouts/application.html.erb -->
<!-- ... -->
<%= action_cable_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<!-- ... -->
Now let's take care of the client and write some CoffeeScript code. Create a new file app/assets/javascripts/channels/chat.coffee with the following contents:
现在,让我们照顾客户端并编写一些CoffeeScript代码。 使用以下内容创建一个新文件app / assets / javascripts / channels / chat.coffee :
jQuery(document).on 'turbolinks:load', ->
$messages = $('#messages')
$new_message_form = $('#new-message')
$new_message_body = $new_message_form.find('#message-body')
if $messages.length > 0
App.chat = App.cable.subscriptions.create {
channel: "ChatChannel"
},
connected: ->
disconnected: ->
received: (data) ->
send_message: (message) ->
Here we are checking if the #messages
block is present on the page and, if yes, set up a new subscription to the ChatChannel
. This channel will be used to communicate with the server in real time. Note that there are a bunch of callbacks that you can use: connected
, disconnected
and received
. send_message
will be used to actually forward the messages to the server. This new file will be loaded automatically as javascripts/cable.coffee requires the channels folder by default.
在这里,我们正在检查页面上是否存在#messages
块,如果是,则设置对ChatChannel
的新订阅。 该通道将用于与服务器实时通信。 请注意,您可以使用许多回调: connected
, disconnected
和received
。 send_message
将实际用于将消息转发到服务器。 默认情况下,此新文件将以javascripts / cable.coffee的要求自动保存为channels文件夹。
One thing to note, however, is that Rails 5.1 apps do not include jQuery as a dependency anymore, so you'll need to add it yourself:
但是要注意的一件事是,Rails 5.1应用程序不再包含jQuery作为依赖项,因此您需要自己添加它:
# Gemfile
gem 'jquery-rails
Run:
跑:
bundleinstall
and include jQuery to the javascripts/application.js file:
并将jQuery包含到javascripts / application.js文件中:
//= require jquery3
I am including the latest version of jQuery to support only the modern browsers, but you can also choose versions 1 or 2.
我包括最新版本的jQuery,仅支持现代浏览器,但您也可以选择版本1或2 。
Now we need to listen for the form submit event, prevent the default action and call the send_message
method defined for the channel instead:
现在,我们需要监听表单提交事件,阻止默认操作,而是调用为该通道定义的send_message
方法:
jQuery(document).on 'turbolinks:load', ->
# ...
if $messages.length > 0
# ...
$new_message_form.submit (e) ->
$this = $(this)
message_body = $new_message_body.val()
if $.trim(message_body).length > 0
App.chat.send_message message_body
e.preventDefault()
return false
Here I am checking if the body has at least one character and call the send_message
method if it is true. Nothing complex going on here.
在这里,我正在检查主体是否具有至少一个字符,如果为true,则调用send_message
方法。 这里没有什么复杂的事情。
Next, flesh out the send_message
method. What it needs to do is receive the body of the message and forward if to the server where it will be stored to the database. Note that this method will not output anything to the page—it should happen inside the received
callback.
接下来, send_message
方法。 它需要做的就是接收消息的主体,然后转发给服务器,消息将存储在数据库中。 请注意,此方法不会在页面上输出任何内容,它应该在received
回调中发生。
# ...
send_message: (message) ->
@perform 'send_message', message: message
@
means this
in CoffeeScript. 'send_message'
argument is the name of the method to call on the server-side which we will create in a minute.
@
意味着this
在CoffeeScript的。 'send_message'
参数是我们将在一分钟内创建的在服务器端调用的方法的名称。
Lastly, code the received
callback to clear the textarea and render a new message:
最后, received
回调进行编码以清除文本区域并呈现新消息:
# ...
received: (data) ->
if data['message']
$new_message_body.val('')
$messages.append data['message']
That's it, we have finished coding the client-side! The server-side awaits, so proceed to the next section.
就是这样,我们已经完成了对客户端的编码! 服务器端正在等待,因此请继续下一节。
ActionCable:服务器端 ( ActionCable: Server-Side )
If you have played The Witcher series, you know that the sword of destiny has two edges. So as ActionCable. Therefore, let's take of the server-side now.
如果您玩过《巫师》系列,您就会知道命运之剑有两个优势 。 如ActionCable。 因此,让我们现在来看服务器端。
Create a new app/channels/chat_channel.rb file that will process the messages sent from the client-side:
创建一个新的app / channels / chat_channel.rb文件,该文件将处理从客户端发送的消息:
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_channel"
end
def unsubscribed
end
def send_message(data)
end
end
There are two callbacks here that are run automatically: subscribed
(that runs as soon as the new client subscribes to the channel using App.cable.subscriptions.create
code we've written a moment ago) and unsubscribed
. send_message
is the method that is called by the following line of code in our chat.coffee file:
这里有两个回调是自动运行的: subscribed
(新客户端使用我们刚才编写的App.cable.subscriptions.create
代码订阅频道后立即运行)和unsubscribed
。 send_message
是由我们的chat.coffee文件中的以下代码行调用的方法:
@perform 'send_message', message: message
Note, by the way, that the files inside the app/channels directory are not auto-reloaded (even in development environment), so you must restart the server after modifying them.
请注意,顺便说一句, app / channels目录中的文件不会自动重新加载(即使在开发环境中),因此您必须在修改它们后重新启动服务器。
The data
local variable contains a hash so we can access the message's body quite easily to save it to the database:
data
局部变量包含一个哈希,因此我们可以很容易地访问消息的正文以将其保存到数据库:
# ...
def send_message(data)
Message.create(body: data['message'])
end
There is a problem, however: we don't have access to the Clearance's current_user
method from inside the channel's code, therefore it is not possible to enforce authentication and associate the created message to a user.
但是,存在一个问题:我们无法从通道的代码内部访问Clearance的current_user
方法,因此无法执行身份验证并将创建的消息与用户相关联。
To fix this problem, the current_user
should be defined manually. We are going to employ the methods similar to the ones provided in the Clearance's session.rb file:
若要解决此问题,应手动定义current_user
。 我们将采用与Clearance的session.rb文件中提供的方法类似的方法:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_current_user
reject_unauthorized_connection unless self.current_user
end
private
def find_current_user
if remember_token.present?
@current_user ||= user_from_remember_token(remember_token)
end
@current_user
end
def cookies
@cookies ||= ActionDispatch::Request.new(@env).cookie_jar
end
def remember_token
cookies[Clearance.configuration.cookie_name]
end
def user_from_remember_token(token)
Clearance.configuration.user_model.find_by(remember_token: token)
end
end
end
The following code simply tries to find a currently logged in user by a remember token stored in the cookie (the cookie's name is taken from the Clearance configuration). The user is then assiged to the self.current_user
. If, however, the user cannot be found, we reject connection effectively disallowing to communicate using the channel. The connect
method is called automatically each time someone tries to subscribe to a channel, so there nothing else we need to do here.
下面的代码只是尝试通过存储在cookie中的“记住”令牌来查找当前登录的用户(cookie的名称来自Clearance配置)。 然后,将用户辅助到self.current_user
。 但是,如果找不到用户,我们将拒绝有效的连接,不允许使用该通道进行通信。 每当有人尝试订阅频道时,都会自动调用connect
方法,因此我们在此无需执行其他任何操作。
Now return to the ChatChannel
and tweak the send_message
method a bit:
现在返回到ChatChannel
并ChatChannel
调整send_message
方法:
# ...
def send_message(data)
current_user.messages.create(body: data['message'])
end
At this point our ActionCable setup is finished. Later you can add other channels using the same principle. There is, however, one last thing to do (yeah, there is always "one last thing", isn't it?). After the message is stored in the database, it should be broadcasted to all users who are subscribed to the channel. The client code will then run the received
callback and render the new message. So, let's do it now!
至此,我们的ActionCable安装完成。 稍后,您可以使用相同的原理添加其他渠道。 但是,还有最后一件事情要做(是的,总会有“最后一件事情”,不是吗?)。 消息存储在数据库中之后,应该将其广播给所有订阅该频道的用户。 然后,客户端代码将运行received
回调并呈现新消息。 所以,现在就开始吧!
回调和(非常)ActiveJob ( Callback and (Very) ActiveJob )
We are going to employ a model callback to broadcast a newly created message. However, I'd like to perform this task in background, therefore using the ActiveJob for this task seems like a good idea as well.
我们将使用模型回调来广播新创建的消息。 但是,我想在后台执行此任务,因此将ActiveJob用于此任务似乎也是一个好主意。
Here is the code for the Message
model:
这是Message
模型的代码:
# models/message.rb
class Message < ApplicationRecord
belongs_to :user
validates :body, presence: true
after_create_commit :broadcast_message
private
def broadcast_message
MessageBroadcastJob.perform_later(self)
end
end
First of all, I've added a very basic validation rule to ensure the body is present. Next, there is a new after_create_commit
callback that runs only after the commit was performed. Inside the corresponding method we are queueing the broadcasting job while passing self
as an argument. self
in this case points to the created message. Using the background job here is convenient because later you may extend it and, for example, send notification emails to the users saying that there is a new message waiting for them.
首先,我添加了一个非常基本的验证规则,以确保该主体存在。 接下来,有一个新的after_create_commit
回调仅在执行提交后运行。 在相应的方法内部,我们将self
作为参数传递时正在排队广播工作。 在这种情况下, self
指向创建的消息。 在这里使用后台作业很方便,因为稍后您可以扩展它,例如,向用户发送通知电子邮件,说有新消息在等待他们。
The background job itself is quite simple:
后台作业本身非常简单:
# app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast 'chat_channel', message: render_message(message)
end
private
def render_message(message)
MessagesController.render partial: 'messages/message', locals: {message: message}
end
end
We queue the job with a default priority. Inside the perform
action we broadcast the message rendered by the MessagesController
. Note that the same partial created earlier is utilized here.
我们以默认优先级对作业进行排队。 在perform
动作内部,我们广播了MessagesController
呈现的MessagesController
。 请注意,此处使用了先前创建的相同部分。
The MessagesController
does not exist, so create it now:
MessagesController
不存在,所以现在创建它:
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
end
And that's it! Our messages are now saved and broadcasted properly, so you can boot the server, navigate to the main page of the site and try to chat with yourself. Note that after you load the page, a request to the /cable will be performed (you may observe it using Firebug or a similar tool):

就是这样! 现在,我们的消息已保存并正确广播,因此您可以启动服务器,导航到站点的主页并尝试与自己聊天。 请注意,加载页面后,将对/ cable进行请求(您可以使用Firebug或类似工具来观察它):
To make process a bit more interesting you may open two separate browser windows and note that messages appear in both of them nearly instantly:

为了使过程更有趣,您可以打开两个单独的浏览器窗口,并注意消息几乎同时出现在两个窗口中:
Inside the terminal you should see an output like this:

在终端内部,您应该看到如下输出:
结论 ( Conclusion )
This ends the first part of the tutorial. We have crafted the real-time chatting application that can now be extended quite easily. Throughout the article you have learned how to:
教程的第一部分到此结束。 我们精心设计了实时聊天应用程序,现在可以很容易地对其进行扩展。 在整篇文章中,您都学习了如何:
- Integrate Clearance gem 整合间隙宝石
- Code the client-side for ActionCable 为客户端编码ActionCable
- Code the server-side for ActionCable 在服务器端为ActionCable编码
- Enforce server-side authentication 实施服务器端身份验证
- Use ActiveJob to broadcast messages 使用ActiveJob广播消息
In the second part we will finalize this application and allow the users to upload files via ActionCable with the help of the Shrine gem and FileReader Web API.
在第二部分中,我们将最终确定该应用程序,并允许用户在Shrine gem和FileReader Web API的帮助下通过ActionCable上传文件。
So, stay tuned and see you soon!
所以,请继续关注,很快再见!
翻译自: https://scotch.io/tutorials/asynchronous-chat-with-rails-and-actioncable
rails5 异步