使用 Rails 和 WebSockets 从头创建聊天应用程序
介绍
在本教程中,我们将使用 Rails 和 WebSockets 从头开始创建一个聊天网络应用程序。
您可以在以下位置找到本教程的代码GitHub 上。
什么是 WebSocket
WebSocket 实际上是一种协议,它通过单个长寿命 TCP 连接实现 Web 应用程序的客户端和服务器之间的双向通信。
WebSocket 协议支持以较低的开销在 Web 浏览器(或其他客户端应用程序)和 Web 服务器之间进行交互,从而促进与服务器之间的实时数据传输。
这是通过为服务器提供一种标准化的方式来实现的,即服务器无需先由客户端请求即可向客户端发送内容,并允许在保持连接打开的同时来回传递消息。这样,客户端和服务器之间就可以进行双向的持续对话。
通信通过 TCP 端口号 80(如果是 TLS 加密连接则为 443)进行,这对于那些使用防火墙阻止非 Web 互联网连接的环境非常有用。类似的双向浏览器-服务器通信已经通过非标准化的方式使用诸如 Comet 之类的权宜之计技术实现了。--
WebSocket @ Wikipedia
为什么使用 WebSockets
假设您需要创建一个网页来显示正在运行的进程的状态。
如果没有 WebSocket,您需要执行以下操作之一:
- 使用 AJAX 和 Javascript 间隔来请求和呈现流程的最新状态或
- 每 x 秒自动重新加载页面(
<meta http-equiv="refresh" content="x">
)或 - 在页面上添加一条消息“状态未自动更新¯\_(ツ)_/¯按此处重新加载页面。”
即使没有任何变化,所有这些方法都会从服务器请求进程状态。
WebSocket 的存在是为了允许这种通信按需进行。其代价是必须在服务器和所有客户端(每个打开的浏览器标签页)之间保持 TCP 连接。
构建应用程序
我们将使用以下内容构建 Web 应用程序:
- Ruby:版本2.6.2
- Rails:版本5.2.3
设置环境
我们将安装适当的 ruby 和 rails 版本。
安装 ruby
我使用rvm管理系统上安装的Ruby版本。要安装所需的 Ruby 版本,请使用以下命令:
rvm install ruby 2.6.2
安装导轨
在您的系统中创建一个名为 的目录rails-chat-tutorial
。
导航到该目录并创建以下两个文件:
.ruby 版本
ruby-2.6.2
.ruby-gemset
rails-chat-tutorial
通过这些文件,我们让rvm知道,当处理这个目录时,我们想要使用特定的 ruby 版本(.ruby-version
)和特定 gemset 中的 gem(.ruby-gemset
)
现在,重新进入目录,您应该会看到如下内容:
$ cd .
ruby-2.6.2 - #gemset created /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial
ruby-2.6.2 - #generating rails-chat-tutorial wrappers...........
使用以下命令安装所需的 Rails 版本:
gem install rails -v 5.2.3
创建 Rails 应用程序
我们已准备好创建我们的新Rails应用程序:
rails new .
注意:我们没有为应用程序定义名称,rails 将使用目录名称来解析它:rails-chat-tutorial
。
Rails 将创建所有应用程序的文件并安装所需的 gem。
让我们启动应用程序以确保一切正常。
rails server
您应该会看到类似这样的内容:
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.2-p47), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop
打开浏览器并访问http://localhost:3000
,如果您看到这个,我们就可以开始了。
用户和设计
我们将使用出色的设计解决方案进行身份验证。
Gemfile
将以下 gem 要求附加到位于应用程序目录根目录的文件底部。
gem 'devise'
在您的终端上,通过执行以下命令安装新的 gem:
bundle
使用以下方法完成与设备的集成:
rails generate devise:install
我们将使用设备生成器创建代表我们用途的模型。
在您的终端上执行:
rails generate devise User username:string
注意:我们在模型中添加了一个额外的属性username
(除了 devise 生成的默认值之外),以便我们在显示用户时可以呈现更友好的内容而不是他们的电子邮件。
打开生成的迁移,您将在下面找到db/migrate/<datetime>_devise_create_users.rb
并将用户名的唯一索引定义附加到[4]:
add_index :users, :username, unique: true
在文件中找到定义username
列的行并将其更改为:
t.string :username, null: false
使该属性成为必需的。
然后在位于的用户模型中app/models/user.rb
添加唯一性和存在性的验证规则:
validates :username, uniqueness: true, presence: true
最后,使用以下命令应用数据库迁移:
rails db:migrate
房间和消息
每个聊天消息都会在一个房间内发生。
让我们把它们全部建造起来。
使用以下命令创建Room
:
rails generate resource Room name:string:uniq
并使用以下命令创建RoomMessage
:
rails generate resource RoomMessage room:references user:references message:text
我们现在要定义适当的关系[7]。
打开app/models/room.rb
并添加类内的关系:
has_many :room_messages, dependent: :destroy,
inverse_of: :room
打开app/models/room_message.rb
并添加类内部的关系:
belongs_to :user
belongs_to :room, inverse_of: :room_messages
使用以下命令迁移数据库:
rails db:migrate
我们现在可以设置我们的路线,以便根请求由RoomsController#index
操作提供服务。
打开config/routes.rb
文件并将其内容更改为:
Rails.application.routes.draw do
devise_for :users
root controller: :rooms, action: :index
resources :room_messages
resources :rooms
end
重新启动服务器并尝试导航到应用程序的根 URL。
您应该会看到一条错误消息,不用担心:
我们必须index
在中创建动作RoomsController
。打开控制器app/controllers/rooms_controller.rb
并将其内容更改为以下内容:
class RoomsController < ApplicationController
def index
end
end
然后创建文件app/views/rooms/index.html.erb
并添加以下内容:
<h1>Rooms index</h1>
重新加载,瞧!
添加身份验证
ApplicationController
我们希望所有用户在开始聊天之前都经过身份验证,因此我们将在位于app/controllers/application_controller.rb中添加以下行:
before_action :authenticate_user!
如果我们现在导航到http://localhost:3000
现在,我们应该被重定向到登录页面[10]。
在继续介绍好东西之前,让我们先用一些好的功能来充实应用程序。
添加引导程序
我们将使用Bootstrap ,并使用bootstrap-rubygem gem将其集成到应用程序中。
按照 gem 的说明,将依赖项附加到您的Gemfile
.
gem 'bootstrap', '~> 4.3.1'
gem 'jquery-rails'
并执行bundle
以获取并安装它。
将文件扩展名更改app/assets/stylesheets/application.css
为scss
并将其内容替换为:
@import "bootstrap";
在[9]行app/assets/javascript/application.js
之前添加以下几行://= require_tree .
//= require jquery3
//= require popper
//= require bootstrap-sprockets
添加 simple_form
我们将使用这个伟大的宝石来轻松生成表格。
在您的和包中附加 gem 依赖项Gemfile
以安装它。
gem 'simple_form'
然后使用以下命令完成集成:
rails generate simple_form:install --bootstrap
注意:我们使用了--bootstrap指令,因为这是我们正在使用的框架。
使用引导程序和简单表单设计视图
Devise 使用自己的视图进行登录、注册等。但我们确实有办法自定义这些视图,现在我们最终使用了引导程序和简单表单,我们可以按照尊重我们选择的方式生成这些视图。
在你的终端中:
rails generate devise:views
登录视图位于app/views/devise/sessions/new.html.erb
,注册视图位于app/views/devise/registrations/new.html.erb
。打开这两个文件,并通过替换以下行[6]来更改提交按钮的类:
<%= f.button :submit, "Sign up" %>
和
<%= f.button :submit, "Sign up", class: 'btn btn-success' %>
呈现按钮引导样式。
在查看我们的更改之前,让我们在默认布局中做最后一件事。
打开app/views/layouts/application.html.erb
并将其内容替换为:
<!DOCTYPE html>
<html>
<head>
<title>RailsChatTutorial</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<%= yield %>
</div>
</div>
</div>
</body>
</html>
最后一个是在我们的视图中使用Bootstrap 的网格。
导航至http://localhost:3000
并查看我们所创建的内容。
Sign up
让我们尝试按照表单的链接进行注册:
如您所见,没有字段可以填写用户名。为此,我们必须:
- 在注册表单中添加字段
- 配置设备以接受新属性(
username
),否则从表单提交后将ApplicationController
忽略它。
要在注册表单中添加字段,请打开app/views/devise/registrations/new.html.erb
并在电子邮件和密码字段之间添加这些行。
<%= f.input :username,
required: true %>
然后打开app/controllers/application_controller.rb
文件配置新属性,将内容更改为:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :username])
end
end
完成,重新加载并注册[5]。
清理未使用的组件
我们不会使用coffee script
,turbolinks
所以让我们删除所有相关的东西。
打开Gemfile
并删除以下行:
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
打开app/assets/javascripts/application.js
并删除以下行:
//= require turbolinks
打开app/views/layouts/application.html.erb
并更改以下行[3]:
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
到
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
检查您的app/assets/javascripts
文件夹中没有任何带扩展名的文件.coffee
,如果发现,请将其删除。2
在终端中,还执行以下命令:
rails tmp:cache:clear
清除所有缓存的已编译咖啡脚本。
完成。重启服务器。
添加导航栏
为了提高网页的可用性,将添加顶部导航栏。
创建目录app/views/shared
并在其中创建一个名为 的文件_navigation_bar.html.erb
。这将是负责渲染导航栏的部分,稍后我们会将其添加到应用程序的默认布局中,以便在所有网页上渲染。添加以下内容:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between">
<a class="navbar-brand" href="#">Rails chat tutorial</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#nav-bar-collapse" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<% if current_user %>
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="avatar" src="<%= gravatar_url(current_user) %>">
<%= current_user.username %>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<%= link_to 'Logout', destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
</div>
</div>
<% end %>
</nav>
注意这gravatar_url(current_user)
行。这是一个辅助方法,我们将使用它来解析已登录用户的 Gravatar URL。这不是内置方法,我们必须定义它,但它非常简单。
编辑app/helpers/application_helper.rb
并添加以下方法:
def gravatar_url(user)
gravatar_id = Digest::MD5::hexdigest(user.email).downcase
url = "https://gravatar.com/avatar/#{gravatar_id}.png"
end
笔记:
- 如您所见,只有用户登录后才会呈现用户的用户名、头像和退出链接。
头像图片有一个 CSS 类avatar
。我们必须在应用程序的样式表中定义这个类。创建一个 CSS 文件,将应用程序中使用的所有 CSS 类都集中到这个名为 的 CSS 文件中app/assets/stylesheets/rails-chat-tutorial.scss
。
现在添加头像的规则:
.avatar {
max-height:30px;
border-radius: 15px;
width:auto;
vertical-align:middle;
}
并打开application.scss
导入新创建的样式表。添加以下行:
@import "rails-chat-tutorial"
我们必须在应用程序布局中添加此部分。编辑app/views/layouts/application.html.erb
并将其内容更改为:
<!DOCTYPE html>
<html>
<head>
<title>RailsChatTutorial</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<%= render partial: 'shared/navigation_bar' %>
<div class="my-3">
<%= yield %>
</div>
</div>
</div>
</div>
</body>
</html>
重新加载以查看栏。
太棒了!填写您想要的凭证并提交表单。
客房管理
我们将为群组创建一个简单的布局。
- 一个窄列垂直显示所有可用房间
- 一个宽列,用于存放聊天消息和表单。
房间索引页的第二列为空,因为只有当用户在特定房间内时才会出现此列。
在索引页中我们将提供创建房间的选项。
房间索引
首先,我们必须加载 中的所有房间RoomsController
。打开app/controllers/rooms_controller.rb
并将索引操作更改为:
def index
@rooms = Room.all
end
打开app/views/rooms/index.html.erb
并将其内容更改为[8]:
<div class="row">
<div class="col-12 col-md-3">
<div class="mb-3">
<%= link_to new_room_path, class: "btn btn-primary" do %>
Create a room
<% end %>
</div>
<% if @rooms.present? %>
<nav class="nav flex-column">
<% @rooms.each do |room| %>
<%= link_to room.name, room_path(room), class: "nav-link room-nav-link" %>
<% end %>
</nav>
<% else %>
<div class="text-muted">
The are no rooms
</div>
<% end %>
</div>
<div class="col">
<div class="alert alert-primary">
<h4 class="alert-heading">
Welcome to the RailsChatTutorial!
</h4>
<p>
We need to talk.
</p>
<hr />
<p>
You can create or join a room from the sidebar.
</p>
</div>
</div>
如果有房间,页面左栏会呈现垂直导航,其中包含指向每个房间页面的链接。右栏会显示一条简单的欢迎信息。
按下Create a room
按钮,我们会得到不存在操作的预期错误。
房间新建/编辑
我们必须定义创建和更新房间的操作。
打开app/controllers/rooms_controller.rb
并将其内容更改为:
class RoomsController < ApplicationController
# Loads:
# @rooms = all rooms
# @room = current room when applicable
before_action :load_entities
def index
@rooms = Room.all
end
def new
@room = Room.new
end
def create
@room = Room.new permitted_parameters
if @room.save
flash[:success] = "Room #{@room.name} was created successfully"
redirect_to rooms_path
else
render :new
end
end
def edit
end
def update
if @room.update_attributes(permitted_parameters)
flash[:success] = "Room #{@room.name} was updated successfully"
redirect_to rooms_path
else
render :new
end
end
protected
def load_entities
@rooms = Room.all
@room = Room.find(params[:id]) if params[:id]
end
def permitted_parameters
params.require(:room).permit(:name)
end
end
注意:我们预加载@rooms
和@room
变量,使它们可用于带有before_action :load_entities
钩子的所有操作。
我们将为该Room
对象创建一个简单的表单,并在创建和编辑房间时使用它。创建app/views/rooms/_form.html.erb
并添加:
<%= simple_form_for @room do |form| %>
<%= form.input :name %>
<%= form.submit "Save", class: 'btn btn-success' %>
<% end %>
new
然后,相应地创建/操作的视图edit
:
应用程序/视图/房间/new.html.erb
<h1>
Creating a room
</h1>
<%= render partial: 'form' %>
应用程序/视图/房间/edit.html.erb
<h1>
Editing room <%= @room.name %>
</h1>
<%= render partial: 'form' %>
是时候创建第一个房间了。在房间的索引页上,按下Create a room
保存,就在这里。
添加此类app/assets/stylesheets/rails-chat-tutorial.scss
可以改善房间的显示效果。
.room-nav-link {
border: 1px solid lighten($primary, 40%);
background: lighten($primary, 45%);
& + .room-nav-link {
border-top: 0 none;
}
}
注意:我们将edit
在房间的页面(又称show
操作)中添加链接。
在进入房间页面之前,我们将重构索引页面,以便能够在房间页面内使用左列的内容。
创建部分app/views/rooms/_rooms.html.erb
内容:
<div class="mb-3">
<%= link_to new_room_path, class: 'btn btn-primary' do %>
Create a room
<% end %>
</div>
<% if @rooms.present? %>
<nav class="nav flex-column">
<% @rooms.each do |room| %>
<%= link_to room.name, room_path(room), class: 'nav-link room-nav-link' %>
<% end %>
</nav>
<% else %>
<div class="text-muted">
The are no rooms
</div>
<% end %>
并改变app/views/rooms/index.html.erb
以使用它:
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="alert alert-primary">
<h4 class="alert-heading">
Welcome to the RailsChatTutorial!
</h4>
<p>
We need to talk.
</p>
<hr />
<p>
You can create or join a room from the sidebar.
</p>
</div>
</div>
</div>
房间页面
show
在中添加操作app/controllers/rooms_controller.rb
:
def show
@room_message = RoomMessage.new room: @room
@room_messages = @room.room_messages.includes(:user)
end
笔记:
- 我们构建一个新的房间消息,我们将在视图中使用它来构建用于创建聊天消息的表单。
- 在显示房间消息时,我们访问用户的电子邮件属性来解析 Gravatar 哈希值。我们
.includes(:user)
在查询中使用了@room_messages
它来获取用户及其用户,避免了N+1 次查询[1]。
创建视图app/views/rooms/show.html.erb
:
<h1>
<%= @room.name %>
</h1>
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="chat">
<% @room_messages.each do |room_message| %>
<%= room_message %>
<% end %>
</div>
<%= simple_form_for @room_message, remote: true do |form| %>
<div class="input-group mb-3">
<%= form.input :message, as: :string,
wrapper: false,
label: false,
input_html: {
class: 'chat-input'
} %>
<div class="input-group-append">
<%= form.submit "Send", class: 'btn btn-primary chat-input' %>
</div>
</div>
<%= form.input :room_id, as: :hidden %>
<% end %>
</div>
</div>
笔记:
- 我们重用了
app/views/rooms/_rooms.html.erb
上一步创建的部分 - 我们添加了一个
div
类.chat
,这是房间消息的渲染器。 @room_message
我们为在控制器中实例化的添加了表单。remote: true
实例化表单时,我们还使用了 指令,因此该表单将通过Ajax提交。- 我们为该属性添加了一个隐藏字段,
:room_id
以便RoomMessagesController
一旦我们提交表单,该值就会到达。
通过添加以下行来设置聊天组件的样式app/assets/stylesheets/rails-chat-tutorial.scss
:
.chat {
border: 1px solid lighten($secondary, 40%);
background: lighten($secondary, 50%);
height: 50vh;
border-radius: 5px 5px 0 0;
overflow-y: auto;
}
.chat-input {
border-top: 0 none;
border-radius: 0 0 5px 5px;
}
导航到一个房间来查看已经完成的事情。
按下Send
按钮时页面上不会发生任何事情,但如果您检查服务器的控制台,您会注意到:
AbstractController::ActionNotFound (The action 'create' could not be found for RoomMessagesController):
让我们修复它。
创建房间消息
这很简单。我们要做的就是create
在 中实现操作RoomMessagesController
。
应用程序/控制器/room_messages_controller.rb
class RoomMessagesController < ApplicationController
before_action :load_entities
def create
@room_message = RoomMessage.create user: current_user,
room: @room,
message: params.dig(:room_message, :message)
end
protected
def load_entities
@room = Room.find params.dig(:room_message, :room_id)
end
end
笔记:
room_id
我们使用上一步中在表单中添加的隐藏字段参数来预加载房间- 我们为房间创建一条新消息,将其用户设置为当前登录用户
如果您现在尝试提交消息,您将再次看到任何内容,但在服务器控制台中,您可以从日志中看到房间消息已创建。
Started POST "/room_messages" for ::1 at 2019-04-04 19:24:33 +0300
Processing by RoomMessagesController#create as JS
Parameters: {"utf8"=>"✓", "room_message"=>{"message"=>"My first message", "room_id"=>"8"}, "commit"=>"Send"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98
Room Load (0.2ms) SELECT "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ? [["id", 8], ["LIMIT", 1]]
↳ app/controllers/room_messages_controller.rb:13
(0.1ms) begin transaction
↳ app/controllers/room_messages_controller.rb:5
RoomMessage Create (0.7ms) INSERT INTO "room_messages" ("room_id", "user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["room_id", 8], ["user_id", 1], ["message", "My first message"], ["created_at", "2019-04-04 16:24:33.456641"], ["updated_at", "2019-04-04 16:24:33.456641"]]
↳ app/controllers/room_messages_controller.rb:5
(4.0ms) commit transaction
↳ app/controllers/room_messages_controller.rb:5
No template found for RoomMessagesController#create, rendering head :no_content
Completed 204 No Content in 88ms (ActiveRecord: 5.1ms)
用户通常希望在发送新消息后清空消息字段。我们不会令用户失望。
创建一个文件app/assets/javascripts/room.js
并添加以下内容:
$(function() {
$('#new_room_message').on('ajax:success', function(a, b,c ) {
$(this).find('input[type="text"]').val('');
});
});
我们绑定到Rails在成功提交表单时ajax:success
触发的事件,我们要做的是清除文本字段的值。
重新加载页面并尝试再次提交并检查。发送消息后,字段值应该被清空。
显示房间消息
如果您重新加载页面,您将看到如下内容:
让我们美化这些信息。
将内容替换app/views/rooms/show.html.erb
为:
<h1>
<%= @room.name %>
</h1>
<div class="row">
<div class="col-12 col-md-3">
<%= render partial: 'rooms' %>
</div>
<div class="col">
<div class="chat">
<% @room_messages.each do |room_message| %>
<div class="chat-message-container">
<div class="row no-gutters">
<div class="col-auto text-center">
<img src="<%= gravatar_url(room_message.user) %>" class="avatar" alt="">
</div>
<div class="col">
<div class="message-content">
<p class="mb-1">
<%= room_message.message %>
</p>
<div class="text-right">
<small>
<%= room_message.created_at %>
</small>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
<%= simple_form_for @room_message, remote: true do |form| %>
<div class="input-group mb-3">
<%= form.input :message, as: :string,
wrapper: false,
label: false,
input_html: {
class: 'chat-input'
} %>
<div class="input-group-append">
<%= form.submit "Send", class: 'btn btn-primary chat-input' %>
</div>
</div>
<%= form.input :room_id, as: :hidden %>
<% end %>
</div>
</div>
并在 .chat 类中添加以下 css 类:
.chat-message-container {
padding: 5px;
.avatar {
margin: 5px;
}
.message-content {
padding: 5px;
border: 1px solid $primary;
border-radius: 5px;
background: lighten($primary, 10%);
color: $white;
}
& + .chat-message-container {
margin-top: 10px;
}
}
刷新页面。神奇!
WebSockets 简介 - ActionCable
是时候开始使用带有 ActionCable 的 WebSockets 了。
Action Cable 将 WebSockets 与 Rails 应用程序的其余部分无缝集成。它允许使用 Ruby 编写实时功能,其风格和形式与 Rails 应用程序的其余部分相同,同时保持高性能和可扩展性。它是一个全栈产品,提供客户端 JavaScript 框架和服务器端 Ruby 框架。您可以访问使用 Active Record 或您选择的 ORM 编写的完整域模型。—— Action Cable
概述@ Ruby on Rails 指南 (v5.2.3)
安装 redis
我们将使用适配器,它与生产环境不同,redis
是一个安全的选择。async
您首先必须在系统上安装redis 。
要在 Ubuntu 上安装它,您只需在终端中执行以下命令:
sudo apt update
sudo apt install redis-server
要检查安装是否成功,请确保在终端中收到 PONG:
$ redis-cli
127.0.0.1:6379> ping
PONG
配置 ActionCable
由于我们正在开发环境中工作,请打开您的config/cable.yml
并将其内容替换为:
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: rails-chat-tutorial_development
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: rails-chat-tutorial_production
注意:我们添加了一个选项channel_prefix
,因为:
此外,可以提供 channel_prefix 以避免在多个应用程序使用同一 Redis 服务器时发生通道名称冲突
- Action Cable 概述 # Redis 适配器 @ Ruby on Rails 指南(v5.2.3)
最后,我们将在中添加依赖项Gemfile
:
gem 'redis'
修改后不要忘记捆绑Gemfile
。
配置 Devise 以验证 websocket 连接
建立 websocket 连接时,我们无法访问用户会话,但可以访问 cookie。因此,为了能够验证用户身份,我们需要先做一些与设备相关的工作(感谢 Greg Molnar)。
为名称下的warden hooks创建一个初始化程序config/initializers/warden_hooks.rb
,并添加以下几行:
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
end
解释:我们在成功登录时添加一个带有用户 ID 的 cookie,并在用户注销后将其删除。
配置 websocket 连接
打开app/channels/application_cable/connection.rb
并将其内容更改为以下内容:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.signed['user.id'])
verified_user
else
reject_unauthorized_connection
end
end
end
end
解释:
此处的identified_by是一个连接标识符,稍后可用于查找特定连接。请注意,任何标记为标识符的内容都会自动在该连接创建的任何通道实例上创建同名委托。--
Action Cable 概述 # 连接设置 @ Ruby on Rails 指南 (v5.2.3)
在该find_verified_user
方法中,我们访问之前在 Warden 钩子中设置的 cookie。
创建房间频道
通道封装了逻辑工作单元,类似于控制器在常规 MVC 设置中的作用。-- Action Cable 概述
# Channels @ Ruby on Rails 指南(v5.2.3)
我们将创建RoomChannel
所有房间页面都将订阅的。
app/channels/room_channel.rb
使用以下内容创建:
class RoomChannel < ApplicationCable::Channel
def subscribed
room = Room.find params[:room]
stream_for room
# or
# stream_from "room_#{params[:room]}"
end
end
解释:
- 一旦建立了对频道的订阅,就会调用该
subscribed
方法,它负责设置来回发送数据的流。
我们稍后要配置房间页面代码,通过room
参数来请求订阅该频道。
我们有两个选择:
-
使用
stream_for
:这样,Rails 会自动为给定对象(room
在本例中为“room:asdfwer234”)生成一个流名称。之后,当我们想要将数据广播到该流时,只需调用 即可。RailsRoomChannel.broadcast_to(room_object, data)
会从 中解析流名称room_object
。换句话说,我们无需手动解析要发送数据的流名称(参见下一条)。- 当频道处理与模型(例如本例中的特定房间)绑定的订阅时,此选项可用
-
使用
stream_from
:我们手动定义流的名称,稍后,当我们想要广播到流时,我们必须使用:ActionCable.server.broadcast("room_#{a_room_id_here}", data)
。
点击此处了解更多信息。
广播房间消息
每次创建房间消息时,我们只需要将其广播到消息的房间流即可。
为此,create
请将 的操作更改为app/controllers/room_messages_controller.rb
:
def create
@room_message = RoomMessage.create user: current_user,
room: @room,
message: params.dig(:room_message, :message)
RoomChannel.broadcast_to @room, @room_message
end
解释:我们添加了RoomChannel.broadcast_to @room, @room_message
将广播到房间特定流的行(如上所述),@room_message
并通过该方法转换为 json to_json
。
所以,在另一端,也就是客户端,我们将接收RoomMessage
模型的 JSON 表示。让我们看看它是什么:
{
"id":29,
"room_id":8,
"user_id":1,
"message":"My first message",
"created_at":"2019-04-04T17:09:00.637Z",
"updated_at":"2019-04-04T17:09:00.637Z"
}
我们计划通过 JavaScript 在聊天室页面添加新消息,但这些信息不够完善。我们唯一缺少的是用户头像。是时候重构了。
打开app/models/user.rb
并添加以下方法:
def gravatar_url
gravatar_id = Digest::MD5::hexdigest(email).downcase
"https://gravatar.com/avatar/#{gravatar_id}.png"
end
我们已经在app/helpers/application_helper.rb
文件中实现了这一点,并且我们不会再使用它,因此将其删除。
更新app/views/shared/_navigation_bar.html.erb
并将之前的 Gravatar 分辨率更改为:
<img class="avatar" src="<%= current_user.gravatar_url %>">
在那里也进行更新app/views/rooms/show.html.erb
和更改,使用:
<img src="<%= room_message.user.gravatar_url %>" class="avatar" alt="">
最后,我们将改变的JSON
表示形式RoomMessage
以包含用户的 url:
应用程序/模型/room_message.rb
def as_json(options)
super(options).merge(user_avatar_url: user.gravatar_url)
end
让我们确认新的 JSON 转换是否有效:
{
"id":29,
"room_id":8,
"user_id":1,
"message":"My first message",
"created_at":"2019-04-04T17:09:00.637Z",
"updated_at":"2019-04-04T17:09:00.637Z",
"user_avatar_url":"https://gravatar.com/avatar/02a28db6886d578f75a820b50f2dd334.png"
}
太好了,继续。
订阅房间信息流
我们将在房间页面中添加一些数据,以便每次访问房间时通过 Javascript 使用它们来订阅适当的流。
打开文件app/views/rooms/show.html.erb
并将定义聊天 div 的行更改为:
<div class="chat" data-channel-subscribe="room" data-room-id="<%= @room.id %>">
解释:我们添加了两个数据属性,一个定义我们要订阅哪个频道,一个定义我们所在的房间。
在文件末尾添加以下代码片段:
<div class="d-none" data-role="message-template">
<div class="chat-message-container">
<div class="row no-gutters">
<div class="col-auto text-center">
<img src="" class="avatar" alt="" data-role="user-avatar">
</div>
<div class="col">
<div class="message-content">
<p class="mb-1" data-role="message-text"></p>
<div class="text-right">
<small data-role="message-date"></small>
</div>
</div>
</div>
</div>
</div>
</div>
此代码片段将用作每条传入消息的模板。每次收到消息时,我们将
- 克隆这个 html
- 改变相应元素的值并
- 将生成的 html 附加到聊天 div 的末尾。
现在我们将创建 Javascript 来完成订阅和处理传入频道数据的工作。
创建文件app/assets/javascripts/channels/room_channel.js
并添加以下代码:
$(function() {
$('[data-channel-subscribe="room"]').each(function(index, element) {
var $element = $(element),
room_id = $element.data('room-id')
messageTemplate = $('[data-role="message-template"]');
$element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000)
App.cable.subscriptions.create(
{
channel: "RoomChannel",
room: room_id
},
{
received: function(data) {
var content = messageTemplate.children().clone(true, true);
content.find('[data-role="user-avatar"]').attr('src', data.user_avatar_url);
content.find('[data-role="message-text"]').text(data.message);
content.find('[data-role="message-date"]').text(data.updated_at);
$element.append(content);
$element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000);
}
}
);
});
});
解释:
- 对于每个具有数据属性
channel-subscribe
的元素room
- 创建对“RoomChannel”的订阅,将元素的
room-id
数据属性作为具有名称的参数传递room
(还记得这行RoomChannel
:Room.find params[:room]
吗?) - 收到数据后,克隆模板片段并根据传入的对象属性更改其内容。
- 将新生成的内容附加到聊天 div 中,最后
- 通过平滑滚动到 div 底部来制作一些动画以给人留下深刻印象。
- 创建对“RoomChannel”的订阅,将元素的
致谢
本教程最初发表在我的个人博客上。
非常感谢您的反馈。
- [1] Armando Andini - N+1 查询
- [2] Rodolfo Ruiz - Coffeescript 剩余内容
- [3] Felix Wolfsteller - Turbolinks 剩菜
- [4] Maria Kravtsova -迁移错字
- [5][7][8] Tony Dehnke -注册步骤、缺少添加模型关系的步骤、HTML 代码块中缺少一行
- [6] keytonw - Devise 视图缺少按钮类
- [9] Martin -在 application.js 中提及需求的顺序
- [10] Sumak -删除重复的身份验证相关代码
就这些!好长的帖子,猫咪照片也太累了吧。
鏂囩珷鏉ユ簮锛�https://dev.to/iridakos/creating-a-chat-application-from-scratch-using-rails-and-websockets-4a6