使用 Rails 和 WebSockets 从头创建聊天应用程序

2025-06-09

使用 Rails 和 WebSockets 从头创建聊天应用程序

介绍

在本教程中,我们将使用 Rails 和 WebSockets 从头开始​​创建一个聊天网络应用程序。

Rails 聊天教程 gif

您可以在以下位置找到本教程的代码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,如果您看到这个,我们就可以开始了。

Rails 新应用程序

用户和设计

我们将使用出色的设计解决方案进行身份验证。

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。

您应该会看到一条错误消息,不用担心:

RoomsController 没有操作索引

我们必须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.cssscss并将其内容替换为:

@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并查看我们所创建的内容。

使用 Devise 和简单表单登录

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 scriptturbolinks所以让我们删除所有相关的东西。

打开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(还记得这行RoomChannelRoom.find params[:room]吗?)
    • 收到数据后,克隆模板片段并根据传入的对象属性更改其内容。
    • 将新生成的内容附加到聊天 div 中,最后
    • 通过平滑滚动到 div 底部来制作一些动画以给人留下深刻印象。

致谢

本教程最初发表在我的个人博客上。

非常感谢您的反馈。

就这些!好长的帖子,猫咪照片也太累了吧。

疲惫的猫照片

鏂囩珷鏉ユ簮锛�https://dev.to/iridakos/creating-a-chat-application-from-scratch-using-rails-and-websockets-4a6
PREV
stup - 用于日常笔记的 shell 工具
NEXT
VS Code 快捷键,适用于代码新手 [mac/windows][GIF]