Sử dụng Action cable với angularjs

Action Cable là một bước tiến mới của Rails, giúp chúng ta xây dựng các ứng dụng Real-time mà trước đây vốn chỉ có thể sử dụng thông qua Gem. Nó cho phép chúng ta có thể xây dựng các ứng dụng thời gian thực như: chat,...

Action Cable và Angularjs

Trước đây, khi xây dựng một ứng real-time giữa s rails- angularjs, mình thường dùng ajax hoặc các thư viện như PubNub,... Tuy nhiên thì mình vẫn không hài lòng lắm với cách này, vì viết ajax thì thường phải viết, xử lý nhiều, còn PubNub là bên thứ 3 và phải đăng ký tài khoản, nếu như bạn có nhiều tiền thì mới có nhiều tiện ích,... Gần đây, vì bị bắt tìm hiểu về action cable thì cá nhân mình thấy đây là một giải pháp rất hay. Thứ nhất, mình không phải dùng thư viện của bên thứ 3, có thể tự custom theo ý mình, và free. Tiếp đến, sử dụng action cable không phải xử lý quá nhiều, gọi lên server nhiều lần. Trong bài viết này mình sẽ viết ví dụ xây dựng một ứng dụng chat đơn giản, backen rails và frontend là angularjs.

Action cable trong rails

Khai báo gem

Để sử dụng actioncable trong rails, bạn phải khai báo gem redispuma

gem "redis", "~> 3.0"
gem "puma"

Sau đó bundle install

Khai báo config

Ở nhiều bài hướng dẫn, mọi người thưởng hướng dẫn config ở router, js, html. Tuy nhiên, ở bài viết này, rails chỉ đóng vai trò làm serve vì vậy chúng ta chỉ cần khai báo ở filr routes.rb

mount ActionCable.server => '/cable

Tạo kênh chat trên server

Sau khi bundle install chúng ta sẽ có thư mục channels có cấu trúc như sau

├── app
    ├── channels
        ├── application_cable
        ├── channel.rb
        └── connection.rb

Module ApplicationCable có class Channel và class Connection đã được định nghĩa Class Connection sẽ dùng để xác thực các kết nối. Ví dụ, khi thiết lập kênh kết nối tới một kênh chat, ứng dụng sẽ yêu cầu xác thực người dùng.

module ApplicationCable
  class Connection < ActionCable::Connection::Base
  end
end

Class Channel dùng để định nghĩa logic được chia sẻ giữa các kênh

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

Thực ra thì 2 file trên mình không dùng làm gì cả, chỉ giới thiệu cho mọi người thôi, với cả cho bài viết dài ra thôi. Chúng ta đã sử dụng Action Cable để tạo thành công một connection, đón nhận bất kì yêu cầu WebSocket nào tại địa chỉ ws://localhost:3000/cable1. Nhưng như vậy chưa đủ để tạo chức năng gửi tin nhắn real-time. Chúng ta cần định nghĩa cụ thể kênh gửi nhận tin nhắn và hướng dẫn các thành phần khác của ứng dụng truyền - nhận dữ liệu từ kênh này. Định nghĩa một kênh truyền với Action Cable rất dễ dàng. Chúng ta sẽ tạo ra một fileapp/channels/chat_channel` như sau:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "messages"
  end

  def receive(data)
    ActionCable.server.broadcast "messages", data.fetch('message')
  end
end

Trong file trên, hàm subcribed khai báo các kênh sẽ được sử dụng để lắng nghe các kết nối đến channel Chat, tức là channle Chat sẽ lắng nghe các message từ kênh messages. Hàm receive định nghĩa các hành động khi có thông điệp được gửi về channel Chat, ở đây mỗi khi channel Chat nhận được một thông điệp thì sẽ tiến hành broadcast thông điệp này đến kênh messages, điều này giúp cho tất cả các user đang trong ở trong một channel có thể nhận được.

Xây dựng kênh chat trên client

Về phía client mình chọn sử dụng Angularjs, do vậy mình sử dụng thư viện angular-actioncable. Để add angular-actioncable vào project của mình bạn có thể dụng một trong các cách sau:

angular.module('app', [
    'ngActionCable'
  ]) .run(function (ActionCableConfig){
  ActionCableConfig.wsUri= "wss://localhost:3000/cable";
});

Ở đây, vì backend và frontend của mình chay ở 2 server riêng biết nên cần config thêm ActionCableConfig.wsUri= "wss://localhost:3000/cable" để actioncable biết địa chỉ websocket Tiếp đến, chúng ta sẽ viết một controller để kết nối, gửi nhận dữ liệu như sau:

  angular.module('app')
  .controller('ChatController', function ($scope, ActionCableChannel){
    $scope.inputText = "";
    $scope.myData = [];
    // connect to ActionCable
    var consumer = new ActionCableChannel("ChatChannel");
    var callback = function(message) {
      $scope.myData.push(message);
    };
    consumer.subscribe(callback).then(function(){
      $scope.sendToMyChannel = function(message){
        consumer.send(message);
      };
      $scope.$on("$destroy", function(){
        consumer.unsubscribe().then(function(){ $scope.sendToMyChannel = undefined; });
      });
    });
  });

Trong đó, ta tạo biến consumer là một action cable mới: var consumer = new ActionCableChannel("ChatChannel");. consumer có 2 function chính là subscribe()unsubscribe(), giống như tên gọi chính là để subscribe là unsubscribe đến channel Chat, functionsend() dùng để gửi dữ liệu lên server Đến đây thì ứng dụng đã gần như hoàn thiện, sau đây là file html đơn giản dùng cho việc gửi và hiển thị dữ liệu

<body ng-app="app">
    <section ng-controller="ChatController">
      <ul>
        <li ng-repeat="datum in myData">
          {{ datum }}
        </li>
      </ul>
      <input ng-model="inputText" /><button ng-click="sendToMyChannel(inputText)">Send</button>
    </section>
    <br />
    <section>
          You can
          &quot;<kbd>Ctrl+c</kbd>&quot; + &quot;<kbd>rails s</kbd>&quot; and &quot;<kbd>Ctrl+z</kbd>&quot; + &quot;<kbd>fg 1</kbd>&quot;
          the server to see how it affects client states over time.
          Each client will maintain one and only one open connection and multiple clients will reconnect at slightly different times on server restart.
        </div>
      </div>
    </section>
  </body>

Trên đây là bài hướng dẫn tạo một ứng dụng chat đơn giản giữa rails-angularjs sử dụng Action cable, các bạn có thể xem một ví dụ source code ở đây https://github.com/Neil-Ni/rails5-actioncable-angular-demo

Angular-actioncable

Sau đây mình xin giới thiệu một số hàm, config của thư viện action-cable

Factory: ActionCableChannel

name arguments description
new channelName:String channel, Params:Hash:optiona, l returns instance Tạo mới và mở ActionCableChannel instance. function này bắt buộc phải có và phải viết đầu tiênvar consumer = new ActionCableChannel("ChatChannel", {chat_id: 1, user_id: 1});
subscribe callback:Function, returns promise subcribe đến một channel, callback thường là phục vụ cho việc nhận dữ liệu consumer.subscribe(function(message){ $scope.thing = message });
unsubscribe callback:Function, returns promise unscrible một channel, consumer.unsubscribe().then(function(){ $scope.sendToMyChannel = undefined; })
send message: String, action: String:optional, returns promise gửi một message lên serve consumer.send('message');
onConfirmSubscription callback:Function function được gọi mỗi khi server chấp nhận subscribe consumer.onConfirmSubscription(function(){ console.log('subscribed'); });

Factory: ActionCableSocketWrangler

Method

name arguments description
start Khởi động services ngActionCable, việc này là default của service trừ khi bạn disable ActionCableSocketWrangler.start();
stop tắt services ngActionCable. ActionCableSocketWrangler.stop();

Properties

name type description
connected Property:Boolean ngActionCable đã start và đã connect. ActionCableSocketWrangler.connected;
connecting Property:Boolean ngActionCable đã started and đang kết nối. ActionCableSocketWrangler.connecting;
disconnected Property:Boolean ngActionCable đã dừng và ngưng kết nối. ActionCableSocketWrangler.disconnected;

Configuration: ActionCableConfig

Properties

name type description
wsUri String khai báo URI để connect hay chính là địa chỉ websocket của server
autoStart Boolean định nghĩa ngActionCable có tự động start hay không, default is true. Nếu như bạn config ActionCableConfig.autoStart= false; thì mỗi khi cần tạo một action cable mới bạn phải start service ngActionCable như đã nói ở trên
debug Boolean hiển thị log thên console của trình duyệt, default là false. Nếu muốn set là true thì bạn cần thêm ActionCableConfig.debug= true;