คุณเคยใช้ 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
. คุณจะได้รับแจ้งให้ลงชื่อเข้าใช้ โดยใช้ชื่อผู้ใช้ของผู้ใช้ที่สร้างไว้ด้านบน และคุณควรจะมีห้องที่สร้างขึ้นใหม่และผู้ใช้ที่คุณไม่ได้ลงชื่อเข้าใช้ตามที่แสดงในภาพด้านล่าง
แนะนำเทอร์โบ
เพื่อให้ได้รับการอัปเดตแบบเรียลไทม์ เราจำเป็นต้องติดตั้ง 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
. บางส่วน ด้วยค่าที่สอดคล้องกับอินสแตนซ์ที่เพิ่มใหม่ดังที่แสดงด้านล่าง
อย่างไรก็ตาม ปัญหาคือเทมเพลตที่ออกอากาศไม่แสดงบนแดชบอร์ด เนื่องจากเราต้องเพิ่มผู้รับการออกอากาศในมุมมองของเรา เพื่อให้สามารถรับและผนวกสิ่งที่ออกอากาศโดย 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>
รูปภาพของห้องที่เพิ่มอัปเดตแบบเรียลไทม์
ตอนนี้ เมื่อสร้างห้องใหม่ เราจะเห็นว่าห้องเหล่านี้ถูกสร้างขึ้น และหน้าได้รับการอัปเดตตามเวลาจริง คุณอาจสังเกตเห็นด้วยว่าแบบฟอร์มไม่เคลียร์หลังจากส่งแต่ละครั้ง เราจะจัดการกับปัญหานี้ในภายหลังโดยใช้สิ่งเร้า
แชทกลุ่ม
สำหรับการแชทเป็นกลุ่ม เราต้องสามารถกำหนดเส้นทางไปยังห้องแต่ละห้องได้ แต่จะต้องอยู่ในหน้าเดียวกัน เราทำเช่นนี้โดยการเพิ่มตัวแปรที่ต้องใช้หน้าดัชนีทั้งหมดลงในวิธีการแสดง 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>
รูปภาพแสดงการย้ายไปยังหลายห้อง
ตอนนี้เราจะไปที่ข้อความที่น่าสนใจมากขึ้น เราต้องให้ส่วนการแชทของเรามีความสูง 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>
จากคอนโซล ถ้าเราสร้างข้อความ เราจะเห็นว่ามีการแพร่ภาพไปยังห้องโดยใช้เทมเพลตที่กำหนด
หากต้องการเปิดใช้งานการสร้างข้อความจากแดชบอร์ด เราต้องเพิ่มวิธีการสร้างลงใน 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
นี่คือสิ่งที่เราได้รับ:
ดังที่เราเห็นในวิดีโอด้านบน ข้อความจะถูกต่อท้าย แต่ถ้าเราย้ายไปที่ห้องสนทนาอื่น ดูเหมือนว่าเราจะสูญเสียข้อความในก่อนหน้านี้เมื่อเรากลับมา เนื่องจากเราไม่ได้ดึงข้อความที่เป็นของห้องแสดง ในการทำเช่นนี้ ในวิธีการแสดง 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
เพราะเราต้องการให้สีพื้นหลังถูกแทนที่สำหรับผู้ใช้ปัจจุบัน
การแชทของเรามีลักษณะดังนี้:
แชทส่วนตัว
งานส่วนใหญ่ที่จำเป็นสำหรับการแชทส่วนตัวเสร็จสิ้นระหว่างการตั้งค่าการแชทเป็นกลุ่ม สิ่งที่เราต้องทำตอนนี้คือ:
- สร้างห้องส่วนตัวสำหรับการแชทส่วนตัวเมื่อกำหนดเส้นทางไปยังผู้ใช้เฉพาะในกรณีที่ไม่มีห้องดังกล่าว
- สร้างผู้เข้าร่วมสำหรับห้องดังกล่าวเพื่อให้ผู้บุกรุกไม่สามารถส่งข้อความไปยังห้องดังกล่าวได้ แม้แต่จากคอนโซล
- ป้องกันไม่ให้ห้องส่วนตัวที่สร้างขึ้นใหม่ถูกแพร่ภาพไปยังรายการห้อง
- แสดงชื่อผู้ใช้แทนชื่อห้องเมื่อเป็นแชทส่วนตัว
ในการกำหนดเส้นทางไปยังผู้ใช้รายใดรายหนึ่ง ผู้ใช้ปัจจุบันกำลังระบุว่าต้องการแชทแบบส่วนตัวกับผู้ใช้รายนั้น ดังนั้นใน 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 %>
ดังที่คุณเห็นด้านล่าง สำหรับการแชทส่วนตัว ชื่อผู้ใช้ของผู้ส่งจะไม่แสดง เราสามารถระบุข้อความของเราเองตามตำแหน่งได้
สิ่งกระตุ้น
เราจะใช้สิ่งกระตุ้นเพื่อล้างแบบฟอร์มเนื่องจากไม่มีการแสดงซ้ำแบบเต็มหน้า และแบบฟอร์มจะไม่ถูกล้างในการสร้างอินสแตนซ์โมเดลใหม่
bundle add stimulus-rails
rails stimulus:install
สิ่งนี้จะเพิ่มไฟล์ต่อไปนี้ตามที่เห็นในภาพด้านล่าง
เราสร้าง 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 ในแอปของคุณเมื่อมีความสำคัญอย่างยิ่งเท่านั้น