Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

คุณเคยใช้ Facebook และได้รับการแจ้งเตือนโดยไม่ต้องรีเฟรชหน้าหรือไม่? การทำงานแบบเรียลไทม์ประเภทนี้ทำได้ในแอปพลิเคชันส่วนใหญ่โดยใช้เฟรมเวิร์ก JavaScript เช่น React ผ่านการจัดการสถานะ แอปพลิเคชันเหล่านี้ส่วนใหญ่ทำหน้าที่เป็นแอปพลิเคชันแบบหน้าเดียว เนื่องจากไม่จำเป็นต้องโหลดหน้าซ้ำระหว่างการใช้งานเพื่ออัปเดตข้อมูลแบบเรียลไทม์ แอปพลิเคชัน Rails นั้นไม่มีสถานะเป็นเวลานาน ในแง่ที่ว่ามักจะต้องโหลดหน้าซ้ำเพื่อให้ได้ สถานะปัจจุบันของแอปพลิเคชัน ตัวอย่างเช่น หากคุณใช้แอป Rails ที่แสดงรายการภาพยนตร์ในโรงภาพยนตร์และผู้ดูแลระบบเพิ่มภาพยนตร์ ภาพยนตร์ที่เพิ่มเข้ามาใหม่จะไม่แสดงบนแดชบอร์ดของคุณ เว้นแต่คุณจะรีเฟรชหน้า

ทำไมต้อง ActionCable

ActionCable เชื่อมช่องว่างนี้และทำให้มีการอัปเดตตามเวลาจริงและฟังก์ชันการทำงานแบบไดนามิกนี้ในแอปพลิเคชัน Rails มันใช้โปรโตคอลการสื่อสารที่เรียกว่า WebSocket เพื่อแนะนำสถานะในแอปพลิเคชันในขณะที่ยังคงมีประสิทธิภาพและสามารถปรับขนาดได้ ผู้ใช้สามารถรับเนื้อหาที่อัปเดตบนแดชบอร์ดโดยไม่ต้องรีเฟรชหน้า

ความมหัศจรรย์ของ Turbo-Rails

TurboRails ประกอบด้วยไดรฟ์เทอร์โบ เฟรมเทอร์โบ และสตรีมเทอร์โบ เมื่อมีการส่งคำขอจากส่วนหนึ่งของหน้าที่ห่อหุ้มด้วย turbo-frame การตอบสนอง HTML จะแทนที่เฟรมที่ส่งออกมาจากหากมีรหัสเดียวกัน ในทางกลับกัน สตรีม Turbo ให้เปิดใช้งานการอัปเดตหน้าบางส่วนเหล่านี้บนเว็บ การเชื่อมต่อซ็อกเก็ต ActionCable ออกอากาศการอัปเดตเหล่านี้จากช่อง และ Turbo stream จะสร้างสมาชิกให้กับช่องนี้และส่งการอัปเดต ด้วยเหตุนี้ คุณสามารถสร้างการอัปเดตแบบอะซิงโครนัสได้โดยตรงเพื่อตอบสนองต่อการเปลี่ยนแปลงโมเดล

สิ่งที่เราตั้งใจจะสร้าง

จุดประสงค์ของบทความนี้คือการแสดงให้เห็นว่า Turbo ทำงานร่วมกับ ActionCable เบื้องหลังอย่างไรเพื่อออกอากาศและแสดงการอัปเดตแบบเรียลไทม์ในแอป Rails 6 ดังนั้น เราจะสร้างแอปแชทที่ผู้ใช้ทุกคนสามารถสร้างห้องแชทได้ และผู้ใช้ทุกคนสามารถส่งข้อความไปยังห้องนั้นและรับการอัปเดตแบบเรียลไทม์ นอกจากนี้เรายังจะอนุญาตให้ผู้ใช้สนทนาแบบส่วนตัวกับอีกคนหนึ่ง เราจะไม่ใช้การเชิญแชทเป็นกลุ่ม สิ่งเหล่านี้อยู่นอกเหนือขอบเขตของโพสต์บล็อกนี้เนื่องจากเกี่ยวข้องกับการออกแบบฐานข้อมูลเพิ่มเติมเท่านั้นแทนที่จะเป็น Turbo และ ActionCable อย่างไรก็ตาม หลังจากบทเรียนนี้ คุณควรเลือกก้าวต่อไปก็คงจะดีไม่น้อย

สิ่งนี้ควรมีลักษณะอย่างไรหรือควรเกี่ยวข้องอย่างไร

  • หน้าดัชนีที่แสดงรายการห้องสนทนาและผู้ใช้ที่มีอยู่ทั้งหมด
  • หน้านี้ควรได้รับการอัปเดตแบบไดนามิกเมื่อมีผู้ใช้ใหม่ลงทะเบียนหรือสร้างห้องใหม่
  • แบบฟอร์มที่นำเสนอเพื่อสร้างห้องสนทนาใหม่
  • กล่องแชทข้อความเพื่อสร้างข้อความเมื่ออยู่ในห้องสนทนา

7 ขั้นตอนง่ายๆ ในการเริ่มต้น

ในแอพนี้ เรากำหนดให้ผู้ใช้เข้าสู่ระบบโดยใช้ชื่อผู้ใช้ที่ไม่ซ้ำกันเท่านั้น ซึ่งทำได้โดยใช้เซสชัน

1. สร้างแอป Rails ใหม่

rails new chatapp
cd chatapp

2. สร้างโมเดลผู้ใช้และย้ายข้อมูล

rails g model User username
rails db:migrate

จากนั้น เราเพิ่มการตรวจสอบความถูกต้องเฉพาะสำหรับชื่อผู้ใช้ เนื่องจากเราต้องการให้ชื่อผู้ใช้ทั้งหมดไม่ซ้ำกันสำหรับเจ้าของ นอกจากนี้เรายังสร้างขอบเขตเพื่อดึงข้อมูลผู้ใช้ทั้งหมด ยกเว้นผู้ใช้ปัจจุบันสำหรับรายชื่อผู้ใช้ของเรา เนื่องจากเราไม่ต้องการให้ผู้ใช้สนทนากับตัวเอง :)

#app/models/user.rb
class User < ApplicationRecord
  validates_uniqueness_of :username
  scope :all_except, ->(user) { where.not(id: user) }
end

3. สร้างโมเดลห้องสนทนา

ห้องสนทนามีชื่อและสามารถเป็นห้องสนทนาส่วนตัว (สำหรับการแชทส่วนตัวระหว่างผู้ใช้สองคน) หรือสาธารณะ (สำหรับทุกคน) เพื่อระบุสิ่งนี้ เราได้เพิ่ม is_private ไปที่โต๊ะในห้องของเรา

 rails g model Room name:string is_private:boolean

ก่อนที่เราจะย้ายไฟล์นี้ เราจะเพิ่มค่าเริ่มต้นให้กับ is_private คอลัมน์เพื่อให้ห้องทั้งหมดที่สร้างเป็นสาธารณะโดยค่าเริ่มต้น เว้นแต่จะระบุไว้เป็นอย่างอื่น

class CreateRooms < ActiveRecord::Migration[6.1]
  def change
    create_table :rooms do |t|
    t.string :name
    t.boolean :is_private, :default => false

    t.timestamps

    end
  end
end

หลังจากขั้นตอนนี้ เราจะย้ายไฟล์โดยใช้คำสั่ง rails db:migrate . นอกจากนี้ยังจำเป็นต้องเพิ่มการตรวจสอบความถูกต้องเฉพาะสำหรับคุณสมบัติชื่อและขอบเขตในการดึงข้อมูลห้องสาธารณะทั้งหมดสำหรับรายการห้องของเรา

#app/models/room.rb
class Room < ApplicationRecord
  validates_uniqueness_of :name
  scope :public_rooms, -> { where(is_private: false) }
end

4. เพิ่มสไตล์

ในการเพิ่มสไตล์ขั้นต่ำให้กับแอปนี้ เราจะเพิ่ม bootstrap CDN ลงในไฟล์ application.html.erb

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

5. เพิ่มการตรวจสอบ

การเพิ่มการตรวจสอบสิทธิ์ในแอปจะต้องใช้ current_user แปรผันตลอดเวลา มาเพิ่มโค้ดต่อไปนี้ในไฟล์ที่ระบุในแอปของคุณเพื่อเปิดใช้งานการตรวจสอบความถูกต้อง

#app/controllers/application_controller.rb
helper_method :current_user

def current_user
  if session[:user_id]
    @current_user  = User.find(session[:user_id])
  end
end

def log_in(user)
  session[:user_id] = user.id
  @current_user = user
  redirect_to root_path
end

def logged_in?
  !current_user.nil?
end

def log_out
  session.delete(:user_id)
  @current_user = nil
end
#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def create
    user = User.find_by(username: params[:session][:username])
    if user
      log_in(user)
    else
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_path
  end

end
#app/views/sessions/new.html.erb
<%= form_for (:session) do |f| %>
  <%= f.label :username, 'Enter your username' %>
  <%= f.text_field :username, autocomplete: 'off' %>
  <%= f.submit 'Sign in' %>
<% end %>

เพิ่มเส้นทางต่อไปนี้ไปยังไฟล์ routes.rb

#routes.rb
Rails.application.routes.draw do
  get '/signin', to: 'sessions#new'
  post '/signin', to: 'sessions#create'
  delete '/signout', to: 'sessions#destroy'
end

6. สร้างตัวควบคุม

สร้าง RoomsController โดยใช้ rails g controller Rooms index และเพิ่มตัวแปรสำหรับรายการผู้ใช้และห้องในวิธีดัชนีของเรา

class RoomsController < ApplicationController

  def index
    @current_user = current_user
    redirect_to '/signin' unless @current_user
    @rooms = Room.public_rooms
    @users = User.all_except(@current_user)
  end
end

7. ตั้งค่าเส้นทาง

เพิ่มห้อง ผู้ใช้ และเส้นทางรูทไปที่ routes.rb เพื่อให้หน้า Landing Page ของเราเป็นหน้าดัชนีที่แสดงรายการห้องและผู้ใช้ทั้งหมด และเราสามารถไปยังห้องใดก็ได้ตามต้องการ

#routes.rb
  resources :rooms
  resources :users
  root 'rooms#index'

การตั้งค่ามุมมอง

การแนะนำครั้งแรกของเราเกี่ยวกับ 'ความมหัศจรรย์' ของ Turbo คือการรับการอัปเดตตามเวลาจริงบนแดชบอร์ดของเราในกรณีที่มีห้องที่เพิ่มเข้ามาใหม่หรือผู้ใช้ที่ลงทะเบียนใหม่ เพื่อให้บรรลุเป้าหมายนี้ ก่อนอื่น เราสร้างส่วนย่อยสองส่วน:_room.html.erb เพื่อแสดงแต่ละห้องและ _user.html.erb เพื่อแสดงผู้ใช้แต่ละคน เราจะแสดงรายการนี้ใน index.html.erb ไฟล์ที่สร้างขึ้นเมื่อสร้าง RoomsController เนื่องจากเป็นหน้า Landing Page ของเรา

# app/views/rooms/_room.html.erb
<div> <%= link_to room.name, room %> </div>
# app/views/users/_user.html.erb
<div> <%= link_to user.username, user %> </div>

เราดำเนินการแสดงไฟล์เหล่านี้ใน index.html.erb . ของเรา ไม่ใช่โดยการอ้างอิงโดยตรง แต่โดยการแสดงตัวแปรที่ดึงคอลเล็กชัน จำได้ว่าใน RoomsController ของเรา ตัวแปร @users และ @rooms ได้กำหนดไว้แล้ว

#app/views/rooms/index.html.erb
<div class="container">
  <h5> Hi <%= @current_user.username %> </h5>
  <h4> Users </h4>
  <%= render @users %>
  <h4> Rooms </h4>
  <%= render @rooms %>
</div>

ในคอนโซล ให้รันคำสั่งต่อไปนี้:

Room.create(name: 'music')
User.create(username: 'Drake')
User.create(username: 'Elon')

เปิดเซิร์ฟเวอร์ Rails ของคุณโดยใช้ rails s . คุณจะได้รับแจ้งให้ลงชื่อเข้าใช้ โดยใช้ชื่อผู้ใช้ของผู้ใช้ที่สร้างไว้ด้านบน และคุณควรจะมีห้องที่สร้างขึ้นใหม่และผู้ใช้ที่คุณไม่ได้ลงชื่อเข้าใช้ตามที่แสดงในภาพด้านล่าง

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

แนะนำเทอร์โบ

เพื่อให้ได้รับการอัปเดตแบบเรียลไทม์ เราจำเป็นต้องติดตั้ง Turbo

bundle add turbo-rails
rails turbo:install

เรียกใช้คำสั่งต่อไปนี้หากคุณไม่ได้ติดตั้ง Redis:

sudo apt install redis-server
#installs redis if you don't have it yet
redis-server
#starts the server

นำเข้า turbo-rails ลงใน application.js ไฟล์โดยใช้ import "@hotwired/turbo-rails"

ต่อไป เราจะเพิ่มคำแนะนำเฉพาะให้กับโมเดลของเราและขอให้พวกเขาเผยแพร่อินสแตนซ์ที่เพิ่มใหม่ไปยังช่องใดช่องหนึ่ง การออกอากาศนี้ดำเนินการโดย ActionCable ตามที่เราจะได้เห็นในไม่ช้า

#app/models/user.rb
class User < ApplicationRecord
  validates_uniqueness_of :username
  scope :all_except, ->(user) { where.not(id: user) }
  after_create_commit { broadcast_append_to "users" }
end

ในที่นี้ เราขอให้โมเดลผู้ใช้แพร่ภาพไปยังช่องที่เรียกว่า "users" หลังจากสร้างอินสแตนซ์ใหม่ทุกครั้งของผู้ใช้แล้ว

#app/models/room.rb
class Room < ApplicationRecord
  validates_uniqueness_of :name
  scope :public_rooms, -> { where(is_private: false) }
  after_create_commit {broadcast_append_to "rooms"}
end

ในที่นี้ เรายังขอให้โมเดลห้องออกอากาศไปยังช่องที่เรียกว่า "ห้อง" หลังจากสร้างห้องตัวอย่างใหม่แต่ละห้องแล้ว

เริ่มต้นคอนโซลของคุณหากยังไม่ได้เริ่ม หรือใช้ reload! คำสั่งถ้ามันทำงานอยู่แล้ว หลังจากสร้างอินสแตนซ์ใหม่ของสิ่งเหล่านี้ เราจะเห็นว่า ActionCable ออกอากาศอินสแตนซ์ที่เพิ่มไปยังช่องที่ระบุเป็นสตรีมเทอร์โบโดยใช้บางส่วนที่กำหนดให้เป็นเทมเพลต สำหรับห้องที่เพิ่มใหม่ จะออกอากาศ _room.html.erb . บางส่วน ด้วยค่าที่สอดคล้องกับอินสแตนซ์ที่เพิ่มใหม่ดังที่แสดงด้านล่าง

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

อย่างไรก็ตาม ปัญหาคือเทมเพลตที่ออกอากาศไม่แสดงบนแดชบอร์ด เนื่องจากเราต้องเพิ่มผู้รับการออกอากาศในมุมมองของเรา เพื่อให้สามารถรับและผนวกสิ่งที่ออกอากาศโดย ActionCable ได้ เราทำได้โดยเพิ่ม turbo_stream_from แท็กระบุช่องที่เราหวังว่าจะได้รับการออกอากาศจาก ดังที่เห็นในภาพด้านบน สตรีมที่ออกอากาศมีแอตทริบิวต์เป้าหมาย และระบุรหัสของคอนเทนเนอร์ที่จะต่อท้ายสตรีม ซึ่งหมายความว่าเทมเพลตที่ออกอากาศจะค้นหาคอนเทนเนอร์ที่มี id ของ "ห้อง" ที่จะต่อท้าย ดังนั้นเราจึงรวม div ที่มี id ดังกล่าวไว้ในไฟล์ดัชนีของเรา เพื่อให้บรรลุเป้าหมายนี้ ใน index.html.erb เราแทนที่ <%= render @users %> ด้วย:

<%= turbo_stream_from "users" %>
<div id="users">
  <%= render @users %>
</div>

และ <%= render @rooms %> กับ

<%= turbo_stream_from "rooms" %>
<div id="rooms">
  <%= render @rooms %>
</div>

ในขณะนี้เราสามารถสัมผัสความมหัศจรรย์ของเทอร์โบได้ เราสามารถรีเฟรชหน้าของเรา และเริ่มเพิ่มผู้ใช้และห้องใหม่จากคอนโซลของเรา และดูว่าพวกเขาถูกผนวกเข้ากับหน้าของเราแบบเรียลไทม์ ยิปปี้!!!

เบื่อกับการสร้างห้องใหม่จากคอนโซลหรือไม่? มาเพิ่มแบบฟอร์มที่ให้ผู้ใช้สร้างห้องใหม่กันเถอะ

#app/views/layouts/_new_room_form.html.erb
<%= form_with(model: @room, remote: true, class: "d-flex" ) do |f| %>
  <%= f.text_field :name, class: "form-control", autocomplete: 'off' %>
  <%= f.submit data: { disable_with: false } %>
<% end %>

ในรูปแบบข้างต้น @room ใช้แล้ว แต่ยังไม่ได้กำหนดไว้ในตัวควบคุมของเรา ดังนั้นเราจึงกำหนดและเพิ่มลงในวิธีดัชนีของ RoomsController ของเรา

@room = Room.new

เมื่อคลิกปุ่มสร้าง มันจะกำหนดเส้นทางไปยังวิธีการสร้างใน RoomsController ซึ่งไม่มีอยู่ในขณะนี้ จึงต้องเพิ่มเข้าไป

#app/controllers/rooms_controller.rb
def create
  @room = Room.create(name: params["room"]["name"])
end

เราสามารถเพิ่มแบบฟอร์มนี้ในไฟล์ดัชนีของเราโดยแสดงผลบางส่วนดังนี้:

<%= render partial: "layouts/new_room_form" %>

นอกจากนี้เรายังสามารถเพิ่มคลาสบูตสแตรปเพื่อแบ่งเพจออกเป็นส่วนๆ สำหรับรายชื่อห้องและผู้ใช้ และอีกส่วนหนึ่งสำหรับการแชท

<div class="row">
  <div class="col-md-2">
    <h5> Hi <%= @current_user.username %> </h5>
    <h4> Users </h4>
    <div>
      <%= turbo_stream_from "users" %>
      <div id="users">
        <%= render @users %>
      </div>
    </div>
    <h4> Rooms </h4>
    <%= render partial: "layouts/new_room_form" %>
    <div>
      <%= turbo_stream_from "rooms" %>
      <div id="rooms">
        <%= render @rooms %>
      </div>
    </div>
  </div>
  <div class="col-md-10 bg-dark">
    The chat box stays here
  </div>
</div>

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo รูปภาพของห้องที่เพิ่มอัปเดตแบบเรียลไทม์

ตอนนี้ เมื่อสร้างห้องใหม่ เราจะเห็นว่าห้องเหล่านี้ถูกสร้างขึ้น และหน้าได้รับการอัปเดตตามเวลาจริง คุณอาจสังเกตเห็นด้วยว่าแบบฟอร์มไม่เคลียร์หลังจากส่งแต่ละครั้ง เราจะจัดการกับปัญหานี้ในภายหลังโดยใช้สิ่งเร้า

แชทกลุ่ม

สำหรับการแชทเป็นกลุ่ม เราต้องสามารถกำหนดเส้นทางไปยังห้องแต่ละห้องได้ แต่จะต้องอยู่ในหน้าเดียวกัน เราทำเช่นนี้โดยการเพิ่มตัวแปรที่ต้องใช้หน้าดัชนีทั้งหมดลงในวิธีการแสดง RoomsController และแสดงหน้าดัชนีให้นิ่ง

#app/controllers/rooms_controller.rb
def show
  @current_user = current_user
  @single_room = Room.find(params[:id])
  @rooms = Room.public_rooms
  @users = User.all_except(@current_user)
  @room = Room.new

  render "index"
end

ตัวแปรพิเศษชื่อ @single_room เพิ่มวิธีการแสดงแล้ว สิ่งนี้ทำให้เรามีห้องที่ถูกส่งไปยัง; ดังนั้นเราจึงสามารถเพิ่มข้อความแสดงเงื่อนไขในหน้าดัชนีของเราซึ่งแสดงชื่อห้องที่เรานำทางไปเมื่อคลิกชื่อห้อง สิ่งนี้ถูกเพิ่มภายใน div ด้วยชื่อคลาส col-md-10 ดังที่แสดงด้านล่าง

<div class="col-md-10 bg-dark text-light">
  <% if @single_room %>
    <h4 class="text-center"> <%= @single_room.name %> </h4>
  <% end %>
</div>

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo รูปภาพแสดงการย้ายไปยังหลายห้อง

ตอนนี้เราจะไปที่ข้อความที่น่าสนใจมากขึ้น เราต้องให้ส่วนการแชทของเรามีความสูง 100vh เพื่อให้เต็มหน้าและรวมกล่องแชทไว้เพื่อสร้างข้อความ กล่องแชทจะต้องมีรูปแบบข้อความ โมเดลนี้จะมีการอ้างอิงผู้ใช้และการอ้างอิงห้อง เนื่องจากข้อความไม่สามารถอยู่ได้หากไม่มีผู้สร้างและห้องที่มีความหมาย

rails g model Message user:references room:references content:text
rails db:migrate

เรายังจำเป็นต้องระบุการเชื่อมโยงนี้ในแบบจำลองผู้ใช้และห้องโดยเพิ่มบรรทัดต่อไปนี้เข้าไป

has_many :messages

มาเพิ่มแบบฟอร์มในหน้าของเราเพื่อสร้างข้อความและเพิ่มสไตล์ให้กับมัน:

#app/views/layouts/_new_message_form.html.erb
<div class="form-group msg-form">
  <%= form_with(model: [@single_room ,@message], remote: true, class: "d-flex" ) do |f| %>
    <%= f.text_field :content, id: 'chat-text', class: "form-control msg-content", autocomplete: 'off' %>
    <%= f.submit data: { disable_with: false }, class: "btn btn-primary" %>
  <% end %>
</div>
#app/assets/stylesheets/rooms.scss
  .msg-form {
    position: fixed;
    bottom: 0;
    width: 90%
  }

  .col-md-10 {
    height: 100vh;
    overflow: scroll;
  }

  .msg-content {
    width: 80%;
    margin-right: 5px;
  }

แบบฟอร์มนี้มี @message ตัวแปร; ดังนั้น เราต้องกำหนดมันในตัวควบคุมของเรา เราเพิ่มสิ่งนี้ลงในวิธีการแสดงของ RoomsController ของเรา

@message = Message.new

ใน routes.rb . ของเรา เราเพิ่มทรัพยากรข้อความภายในทรัพยากรห้อง เนื่องจากสิ่งนี้แนบกับพารามิเตอร์ รหัสของห้องที่สร้างข้อความ

resources :rooms do
  resources :messages
end

เมื่อใดก็ตามที่มีการสร้างข้อความใหม่ เราต้องการให้เผยแพร่ไปยังห้องที่สร้างข้อความนั้น ในการทำเช่นนี้ เราจำเป็นต้องมีข้อความบางส่วนที่แสดงข้อความ เนื่องจากนี่คือสิ่งที่จะออกอากาศ เราจึงต้องการ turbo_stream ที่ได้รับข้อความที่ออกอากาศสำหรับห้องนั้น ๆ และ div ที่จะทำหน้าที่เป็นที่เก็บสำหรับการต่อท้ายข้อความเหล่านี้ อย่าลืมว่า ID ของคอนเทนเนอร์นี้จะต้องเหมือนกับเป้าหมายของการออกอากาศ

เราเพิ่มสิ่งนี้ลงในโมเดลข้อความของเรา:

#app/models/message.rb
after_create_commit { broadcast_append_to self.room }

ด้วยวิธีนี้ จะออกอากาศไปยังห้องใดห้องหนึ่งที่สร้างขึ้น

นอกจากนี้เรายังเพิ่มสตรีม ที่เก็บข้อความ และแบบฟอร์มข้อความลงในไฟล์ดัชนีของเรา:

#within the @single_room condition in app/views/rooms/index.html.erb
<%= turbo_stream_from @single_room %>
<div id="messages">
</div>
<%= render partial: 'layouts/new_message_form' >

เราสร้างข้อความบางส่วนที่จะออกอากาศ และในนั้น เราแสดงชื่อผู้ใช้ของผู้ส่งเฉพาะในกรณีที่ห้องนั้นเป็นห้องสาธารณะ

#app/views/messages/_message.html.erb
<div>
  <% unless message.room.is_private %>
    <h6 class="name"> <%= message.user.username %> </h6>
  <% end %>
  <%= message.content %>
</div>

จากคอนโซล ถ้าเราสร้างข้อความ เราจะเห็นว่ามีการแพร่ภาพไปยังห้องโดยใช้เทมเพลตที่กำหนด

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

หากต้องการเปิดใช้งานการสร้างข้อความจากแดชบอร์ด เราต้องเพิ่มวิธีการสร้างลงใน MessagesController

#app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def create
    @current_user = current_user
    @message = @current_user.messages.create(content: msg_params[:content], room_id: params[:room_id])
  end

  private

  def msg_params
    params.require(:message).permit(:content)
  end
end

นี่คือสิ่งที่เราได้รับ:สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

ดังที่เราเห็นในวิดีโอด้านบน ข้อความจะถูกต่อท้าย แต่ถ้าเราย้ายไปที่ห้องสนทนาอื่น ดูเหมือนว่าเราจะสูญเสียข้อความในก่อนหน้านี้เมื่อเรากลับมา เนื่องจากเราไม่ได้ดึงข้อความที่เป็นของห้องแสดง ในการทำเช่นนี้ ในวิธีการแสดง RoomsController เราเพิ่มตัวแปรที่ดึงข้อความทั้งหมดที่เป็นของห้องแชท และบนหน้าดัชนี เราแสดงข้อความที่ดึงมา นอกจากนี้เรายังสามารถเห็นได้ว่าแบบฟอร์มข้อความไม่ได้รับการล้างหลังจากส่งข้อความ สิ่งนี้จะได้รับการจัดการด้วยสิ่งกระตุ้นที่ตามมา

#in the show method of app/controllers/rooms_controller.rb
@messages = @single_room.messages
#within the div with id of 'messages'
  <%= render @messages %>

ตอนนี้แต่ละห้องจะโหลดข้อความไว้ที่ทางเข้า

เราจำเป็นต้องทำให้สิ่งนี้ดูเรียบร้อยมากขึ้นโดยจัดข้อความของผู้ใช้ปัจจุบันให้ชิดขวาและข้อความอื่นๆ ทางด้านซ้าย วิธีที่ง่ายที่สุดในการบรรลุสิ่งนี้คือการกำหนดคลาสตามเงื่อนไข message.user == current_user แต่ตัวแปรในเครื่องไม่สามารถใช้ได้กับสตรีม ดังนั้นสำหรับข้อความที่ออกอากาศจะไม่มี current_user .พวกเราทำอะไรได้บ้าง? เราสามารถกำหนดคลาสให้กับที่เก็บข้อความตาม ID ผู้ส่งข้อความแล้วใช้ประโยชน์จาก current_user วิธีตัวช่วยเพื่อเพิ่มสไตล์ให้กับ application.html.erb . ของเรา ไฟล์. ด้วยวิธีนี้ หาก ID ของผู้ใช้ปัจจุบันคือ 2 คลาสในแท็กสไตล์ใน application.html.erb จะเป็น .msg-2 ซึ่งจะสอดคล้องกับคลาสในข้อความของเราบางส่วนเมื่อผู้ส่งข้อความเป็นผู้ใช้ปัจจุบัน

#app/views/messages/_message.html.erb
<div class="cont-<%= message.user.id %>">
  <div class="message-box msg-<%= message.user.id %> " >
    <% unless message.room.is_private %>
      <h6 class="name"> <%= message.user.username %> </h6>
    <% end %>
  <%= message.content %>
  </div>
</div>

เราเพิ่ม message-box สไตล์:

#app/assets/stylesheets/rooms.scss
.message-box {
  width: fit-content;
  max-width: 40%;
  padding: 5px;
  border-radius: 10px;
  margin-bottom: 10px;
  background-color: #555555 ;
  padding: 10px
}

ในแท็กส่วนหัวของ application.html.erb . ของเรา ไฟล์.

#app/views/layouts/application.html.erb
<style>
  <%= ".msg-#{current_user&.id}" %> {
  background-color: #007bff !important;
  padding: 10px;
  }
  <%= ".cont-#{current_user&.id}" %> {
  display: flex;
  justify-content: flex-end
  }
</style>

เราเพิ่ม !important แท็กไปที่ background-color เพราะเราต้องการให้สีพื้นหลังถูกแทนที่สำหรับผู้ใช้ปัจจุบัน

การแชทของเรามีลักษณะดังนี้:สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

แชทส่วนตัว

งานส่วนใหญ่ที่จำเป็นสำหรับการแชทส่วนตัวเสร็จสิ้นระหว่างการตั้งค่าการแชทเป็นกลุ่ม สิ่งที่เราต้องทำตอนนี้คือ:

  • สร้างห้องส่วนตัวสำหรับการแชทส่วนตัวเมื่อกำหนดเส้นทางไปยังผู้ใช้เฉพาะในกรณีที่ไม่มีห้องดังกล่าว
  • สร้างผู้เข้าร่วมสำหรับห้องดังกล่าวเพื่อให้ผู้บุกรุกไม่สามารถส่งข้อความไปยังห้องดังกล่าวได้ แม้แต่จากคอนโซล
  • ป้องกันไม่ให้ห้องส่วนตัวที่สร้างขึ้นใหม่ถูกแพร่ภาพไปยังรายการห้อง
  • แสดงชื่อผู้ใช้แทนชื่อห้องเมื่อเป็นแชทส่วนตัว

ในการกำหนดเส้นทางไปยังผู้ใช้รายใดรายหนึ่ง ผู้ใช้ปัจจุบันกำลังระบุว่าต้องการแชทแบบส่วนตัวกับผู้ใช้รายนั้น ดังนั้นใน UsersController . ของเรา เราตรวจสอบว่ามีห้องส่วนตัวระหว่างสองคนนี้หรือไม่ ถ้าใช่ ก็จะกลายเป็น @single_room . ของเรา ตัวแปร; มิฉะนั้นเราจะสร้างมันขึ้นมา เราจะสร้างชื่อห้องพิเศษสำหรับห้องสนทนาส่วนตัวแต่ละห้อง เพื่อให้เราสามารถอ้างอิงได้เมื่อจำเป็น นอกจากนี้เรายังต้องรวมตัวแปรทั้งหมดที่จำเป็นสำหรับหน้าดัชนีในวิธีการแสดงเพื่อให้อยู่ในหน้าเดียวกัน

#app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    @current_user = current_user
    @rooms = Room.public_rooms
    @users = User.all_except(@current_user)
    @room = Room.new
    @message = Message.new
    @room_name = get_name(@user, @current_user)
    @single_room = Room.where(name: @room_name).first || Room.create_private_room([@user, @current_user], @room_name)
    @messages = @single_room.messages

    render "rooms/index"
  end

  private
  def get_name(user1, user2)
    users = [user1, user2].sort
    "private_#{users[0].id}_#{users[1].id}"
  end
end

ดังที่เราเห็นข้างต้น เราต้องเพิ่ม create_private_room วิธีการไปยังโมเดลห้องของเรา ซึ่งจะสร้างห้องส่วนตัวและผู้เข้าร่วม สิ่งนี้ทำให้เราสร้างโมเดลใหม่ที่เรียกว่า Participant ซึ่งจะระบุผู้ใช้และห้องส่วนตัวที่เขาหรือเธออยู่

rails g model Participant user:references room:references
rails db:migrate

ในรูปแบบห้องของเรา เราเพิ่ม create_private_room วิธีการและเปลี่ยน after_create . ของเรา โทรแจ้งชื่อห้องเท่านั้นถ้าไม่ใช่ห้องส่วนตัว

#app/models/room.rb
has_many :participants, dependent: :destroy
after_create_commit { broadcast_if_public }

def broadcast_if_public
  broadcast_append_to "rooms" unless self.is_private
end

def self.create_private_room(users, room_name)
  single_room = Room.create(name: room_name, is_private: true)
  users.each do |user|
    Participant.create(user_id: user.id, room_id: single_room.id )
  end
  single_room
end

เพื่อป้องกันไม่ให้ผู้ใช้รายอื่นส่งข้อความไปยังห้องส่วนตัวเมื่อไม่ได้เข้าร่วม เราได้เพิ่ม before_create ตรวจสอบรูปแบบข้อความ ดังนั้น สำหรับห้องส่วนตัว จะมีการยืนยันว่าผู้ส่งข้อความเป็นผู้มีส่วนร่วมในการสนทนาส่วนตัวนั้นจริง ๆ ก่อนที่จะมีการสร้างข้อความดังกล่าว เราควรสังเกตว่าจากแดชบอร์ด เป็นไปไม่ได้ที่จะส่งข้อความไปยังห้องส่วนตัว ถ้าคุณไม่ใช่ผู้เข้าร่วม เนื่องจากคุณจะต้องคลิกที่ชื่อผู้ใช้เท่านั้น จากนั้นห้องจะถูกสร้างขึ้นสำหรับผู้ใช้ทั้งสอง การตรวจสอบนี้มีไว้เพื่อความปลอดภัยเป็นพิเศษ เนื่องจากผู้ที่ไม่ใช่ผู้เข้าร่วมสามารถสร้างข้อความจากคอนโซลได้

#app/models/message.rb
before_create :confirm_participant

def confirm_participant
  if self.room.is_private
    is_participant = Participant.where(user_id: self.user.id, room_id: self.room.id).first
    throw :abort unless is_participant
  end
end

ในการแสดงชื่อผู้ใช้แทนชื่อห้องในระหว่างการแชทส่วนตัว เราระบุในหน้าดัชนีของเราว่าหาก @user มีตัวแปรอยู่ ควรแสดงชื่อผู้ใช้ของผู้ใช้ โปรดทราบว่าตัวแปรนี้มีอยู่ในวิธีการแสดง UsersController เท่านั้น สิ่งนี้นำไปสู่แท็ก h4 ที่แสดงชื่อห้องที่เปลี่ยนเป็นสิ่งนี้:

<h4 class="text-center"> <%= @user&.username || @single_room.name %> </h4>

ตอนนี้ เมื่อเรานำทางไปยังผู้ใช้ เราจะเห็นชื่อผู้ใช้ของผู้ใช้แทนที่จะเป็นชื่อห้อง และเราสามารถส่งและรับข้อความได้ อย่าลืมเพิ่มลิงก์ออกจากระบบไปยังหน้าแรกของเรา

<%= link_to 'Sign Out', signout_path,  :method => :delete %>

ดังที่คุณเห็นด้านล่าง สำหรับการแชทส่วนตัว ชื่อผู้ใช้ของผู้ส่งจะไม่แสดง เราสามารถระบุข้อความของเราเองตามตำแหน่งได้

สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

สิ่งกระตุ้น

เราจะใช้สิ่งกระตุ้นเพื่อล้างแบบฟอร์มเนื่องจากไม่มีการแสดงซ้ำแบบเต็มหน้า และแบบฟอร์มจะไม่ถูกล้างในการสร้างอินสแตนซ์โมเดลใหม่

bundle add stimulus-rails
rails stimulus:install

สิ่งนี้จะเพิ่มไฟล์ต่อไปนี้ตามที่เห็นในภาพด้านล่าง สร้างแอปแชทแบบเรียลไทม์ใน Rails โดยใช้ ActionCable และ Turbo

เราสร้าง reset_form_controller.js เพื่อรีเซ็ตแบบฟอร์มของเราและเพิ่มฟังก์ชันต่อไปนี้เข้าไป

//app/javascript/controllers/reset_form_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  reset() {
    this.element.reset()
  }
}

จากนั้น เราเพิ่มแอตทริบิวต์ข้อมูลลงในแบบฟอร์ม โดยระบุผู้ควบคุมและการดำเนินการ

data: { controller: "reset-form", action: "turbo:submit-end->reset-form#reset" }

ตัวอย่างเช่น form_with แท็กของแบบฟอร์มข้อความของเรามีการเปลี่ยนแปลงดังต่อไปนี้:

<%= form_with(model: [@single_room ,@message], remote: true, class: "d-flex",
     data: { controller: "reset-form", action: "turbo:submit-end->reset-form#reset" }) do |f| %>

ในที่สุด นี่คือทั้งหมดที่จำเป็น แบบฟอร์มของเราชัดเจนหลังจากสร้างข้อความหรือห้องใหม่ เราควรสังเกตด้วยว่าการกระทำกระตุ้น "ajax:success->reset-form#reset" ยังสามารถล้างแบบฟอร์มเมื่อ ajax:success เหตุการณ์เกิดขึ้น

บทสรุป

ในแอพนี้ เราได้เน้นไปที่การดำเนินการต่อท้ายของ Turbo Streams แต่นี่ไม่ใช่ทั้งหมดที่เกี่ยวข้องกับ Turbo Stream อันที่จริง Turbo Streams ประกอบด้วยการดำเนินการห้าอย่าง:ผนวก เพิ่ม แทนที่ อัปเดต และลบ ในการดำเนินการลบและอัปเดตข้อความแชทแบบเรียลไทม์ การดำเนินการเหล่านี้จะมีประโยชน์ และอาจจำเป็นต้องมีความรู้เกี่ยวกับเฟรมเทอร์โบและวิธีทำงาน นอกจากนี้ สิ่งสำคัญที่ควรทราบคือสำหรับแอปพลิเคชันที่ขึ้นอยู่กับการอัปเดต WebSocket บางอย่าง คุณลักษณะ ในการเชื่อมต่อที่ไม่ดี หรือหากมีปัญหาเซิร์ฟเวอร์ WebSocket ของคุณอาจถูกตัดการเชื่อมต่อ ดังนั้นจึงขอแนะนำให้ใช้ Turbo Stream ในแอปของคุณเมื่อมีความสำคัญอย่างยิ่งเท่านั้น