+3

Tạo chat room đơn giản sử dụng Rails Action Cable

Ở bài viết này chúng ta sẽ khám phá 2 tính năng mới của Ruby on Rails - Action CableActive Job. Với Action Cable ta không còn nghi ngờ gì nữa, nó cho phép tích hợp giao thức WebSocket communication mà từ đó có thể mở một tương tác communication session giữa user browser và server. Với những công nghệ mà việc xây dựng ứng dụng realtime trở nên dễ dàng hơn bao giờ hết 😄.

1. HTTP and Websockets

Đối với HTTP, kết nối giữa server và client là một kết nối ngắn hạn. Client gửi request tới server, khi đó một kết nối sẽ được thiết lập để yêu cầu resource (có thể là JSON, HTML, XML, ...) trả về cho client và ngay sau đó sẽ kết nối sẽ kết thúc. Câu hỏi được đặt ra ở đây là làm sao client biết được server có sử thay đổi data để gửi về client hay không?

Nào, đã đến lúc tìm hiểu về WebSocket rồi. Giao thức WebSocket nhằm duy trì kết nối giữa client và server, từ đây có thể stream data cho nhau. Các client đăng kí một kết nối tới server và khi có thông tin mới server sẽ broadcasts dữ liệu đến tất cả client đã đăng kí. Bằng cách này cả server và client đều biết được trạng thái của data để có thể dễ dàng đồng bộ khi xuất hiện sự thay đổi.

2. ActionCable function

Từ khi Rails Controller phát triển để xử lý nhiều requests, Rails đã đưa ra một ý tưởng để tích hợp WebSockets. Rails 5 có một thư mục trong app directory là channels. Channels hoạt động như là controllers để đóng gói các websocket request thành các unit work, ví dụ điển hình ở đây có thể là chat messages hay notifications. Các kênh có thể đăng ký client-side để có thể truyền dữ liệu tới một hay nhiều kênh.

3. Small chat app

Để hiểu rõ hơn về Rails Action Cable mình sẽ xây dựng một ứng dụng đơn giản step by step dưới @@

Requirement: Rails 5 (and higher) Ruby 2.2.4 (and higher)

Đối với action cable thì nó yêu cầu adapter PostgresSQL do đó bạn cần phải cài đặt nó trước nhé 😃) Tiếp theo bắt đầu init rails project với database postgres nào:

rails new chatapp --database=postgresql cd chatapp rake db:create

  1. Ý tưởng
  • Tạo một server-side websocket channel gọi là room_channel chứa một vài method khi mà user subcribes, unsubcribes và gửi data tới client.
  • Tạo một client-side sử dụng javascript (app này mình sẽ viết coffeescript) là RoomChannel. Nó sẽ đăng ký server-side channel và một số method connection/disconnecting với server để gửi nhận dữ liệu.
  • RailsJob đặt các messages vào hàng đợi để lưu vào database và broadcast trở lại qua websocket channel sau khi đã được tạo.

Đầu tiên chúng ta generate controller cho chatroom ha:

rails g controller rooms show

Tiếp đến là config routes theo chuẩn sách giáo khoa nha 😄

#config/routes.rb

Rails.application.routes.draw do
root to: 'rooms#show'

Tạo model message để lưu trữ nội dung tin nhắn:

rails g model message content:text rails db:migrate

Viết code để list all message nào: app/controllers/rooms_controller.rb

class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end

Tiếp đến view nè: app/views/rooms/show.html.erb

<h1>Chat room</h1>
<div id="messages"> <%= render @messages %> </div>
<form>
  <label>Say something:</label><br>
  <input type="text" data-behavior="room_speaker">
</form>

Tạo partial _message để render new message: app/views/messages/_message.html.erb

<div class="message">
  <p><%= message.content %></p>
</div>

render @messages sẽ tự động tìm _message.html.erb trong views/messages

Kiểm trả application js và chắc rằng nội dung như sau nhé: app/assets/javascripts/application.js

//= require jquery 
//= require jquery_ujs
  1. Creating a channel Đầu tiên để kích hoạt sử dụng action cable ta config như sau: config/routes.rb
mount ActionCable.server => '/cable'

init app/asset/javascipt/cable.js

//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

generate room channel

rails g channel room speak

Lệnh này sẽ generate 2 file: app/assets/javascripts/channels/room.coffee và app/channels/room_channel.rb.

class RoomChannel < ApplicationCable::Channel 
    def subscribed 
        # stream_from "some_channel" 
    end 
    
    def unsubscribed 
        # Any cleanup needed when channel is unsubscribed
    end 
    
    def speak 
    end 
end
App.room = App.cable.subscriptions.create "RoomChannel", 
    connected: -> 
        # Called when the subscription is ready for use on the server 
    disconnected: -> 
        # Called when the subscription has been terminated by the server 
    received: (data) -> 
        # Called when there's incoming data on the websocket for this channel 
    speak: -> 
        @perform 'speak'

Với javascipt file, client đăng ký server qua App.room = App.cable.subscriptions.create "RoomChannel". Có 3 method mặc định: connected, disconnected, received. Method speak gửi data tới server-side Chạy sever rails s và kiểm tra javascript console

  1. Trasmitting data Bây giờ chúng ta bắt đầu create và transmit data nhờ ActionCable. Chúng ta sửa lại speak function như sau: #app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel", 
    #rest of the code 
    speak: (message) -> 
        @perform 'speak', message: message

Function này sẽ gửi message object tới server-side speak method ở RoomChannel. Sửa lại một chút method để nó accept parameters nào =))

#app/channels/room_channel.rb 
def speak data 
    ActionCable.server.broadcast "room_channel", message: data['message'] 
end

Bây giờ speak method đã lấy được message và broadcast tới room_channel. Để get broadcast, chúng ta chỉ định subscribers lắng nghe sử dụng stream_from ở method subcribed

class RoomChannel < ApplicationCable::Channel 
    def subscribed 
        stream_from "room_channel" 
    end 
    
    def unsubscribed 
        # Any cleanup needed when channel is unsubscribed 
    end 
    
    def speak data 
        ActionCable.server.broadcast "room_channel", message: data['message'] 
    end 
end
  1. Lưu message vào database #app/channels/room_channel.rb
def speak data
    Message.create! content: data['message']
  end

Tiếp đến sử dụng RailsJob để đưa các messages vào queue từ đó server có thể thực thi nhiều tin nhắc trong cùng 1 thời điểm #app/models/message.rb

class Message < ApplicationRecord
  after_create_commit {MessageBroadcastJob.perform_later self}
end

after_create_commit cho phép job thực thi lưu message vào database. perform_later: job sẽ thực thi khi queue rỗng Generate MessageBroadCastJob:

rails g job MessageBroadcast

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform message
    ActionCable.server.broadcast "room_channel",
      message: render_message(message)
  end

  private

  def render_message message
    ApplicationController.renderer.render(
      partial: "messages/message",
      locals: {message: message}
    )
  end
end

Method perform sẽ nhận message và render nó vào _message.html.erb partial. Với rails 5 bạn có thể render template ngoài phạm vi controller, sử dụng renderer của ApplicationController. Bước cuối cùng ta append message vào chatroom #app/assets/javascripts/channels/room.coffee

  received: (data) ->
    $('#messages').append data['message']

4. Source code và tham khảo

Github: Chatapp Tham khảo: creating-a-chat-using-rails-action-cable


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí