Ứng dụng chat đơn giản với Rails5, ActionCable, Redux, và React

1. Giới thiệu

Một tính năng đáng chú ý của phiên bản Rails 5 đó là tích hợp ActionCable. Nhiệm vụ của ActionCable là cho phép chúng ta có thể tạo chức năng real-time trong các ứng dụng Rails trở nên đơn giản hơn rất nhiều.ActionCable sử dụng giao thức Websocket để hỗ trợ giao tiếp 2 chiều giữa client và server.

Redux js là một thư viện Javascript giúp tạo ra thành một lớp quản lý trạng thái của ứng dụng. Được dựa trên nền tảng tư tưởng của kiến trúc Flux do Facebook giới thiệu, do vậy Redux thường là bộ đôi kết hợp hoàn hảo với React (React Js và React Native)

2. Redux và React kết hợp với nhau như thế nào

Mình sẽ không đi quá sâu vào các kĩ thuật của Flux và React ở đây, vì vậy nếu bạn không quen với điều đó, các bạn có thể xem lại các kiến thức nền tảng của Flux và React.

Giả sử bạn hiểu được ý tưởng căn bản, đây là một sơ đồ về cách thức Redux đơn giản hoá các kiến trúc:

react.png

Nói cách khác, React là ý tưởng rất hay cho việc giữ UIS được gọn gàng và có tổ chức, các nhà phát triển web còn dùng để theo dõi các dữ liệu và trạng thái của ứng dụng riêng của họ.

Đây là chính nơi Redux được sử dụng: một hành động từ giao diện người dùng, được kết hợp với một đối tượng JavaScript đơn giản(cục bộ), được tạo ra thông qua một trạng thái mới.Sau đó trạng thái mới được xử lý bằng cách React-Redux và gửi đến React .Đó là nó chính là cách thức của vấn đề.

Bây giờ, vấn đề là trong ứng dụng sử dụng Rails của tôi, tôi liên kết chúng lại với nhau kết hợp với việc tham khảo ứng dụng của Kenta Suzuki's: “React-rails-redux-sample”, React-rails gem giúp chúng ta setup một cấu trúc thư mục trong asset pipeline có thể được tinh chỉnh một chút cho Redux như sau:

foldersreact.png

Actions trong Redux là functions có thể pass qua nhiều action type và bất cứ điều gì khác bạn muốn gửi tới reducer của bạn.Trong ví dụ sau đây, chat.js file trong actions folder sẽ được biết như sau:

export const ADD_MESSAGE = 'ADD_MESSAGE';
export const SET_MESSAGES = 'SET_MESSAGES';

export function setMessages(messages) {
  return {
    type: SET_MESSAGES,
    messages: messages
  };
}

export function addMessage(message) {
  return {
    type: ADD_MESSAGE,
    message: message
  };
}

Như trong ví dụ chúng ta có một action để sets messages trong store (dòng 4-9), ngoài ra chúng ta cũng add một message vào store(dòng 11-16).

Khi đó reducer sẽ như hình bên dưới: import { SET_MESSAGES, ADD_MESSAGE } from '../actions/chat';

export default function chat(state = {}, action) {
  const { type, messages } = action;

  switch (type) {
  case ADD_MESSAGE:
    return [
      ...state,
      action.message
    ]
  case SET_MESSAGES:
    return messages
  default:
    return state;
  }
}

3. Component và container

Trong đó có bao gồm một bước chuyển đổi trạng thái đơn giản to gửi một action và một trạng thái đến đúng địa chỉ yêu cầu để trả về trạng thái mới thích hợp hơn(một object mà app chúng ta đang chờ đợi, điều đó là rất quan trọng..bạn không muốn thay đổi trạng thái cũ).

Tại thời điểm này, nếu bạn thực sự muốn sử dụng nó luôn, bạn nên xem lại một chút.Bắt buộc phải mất một thời gian, tuy nhiên chúng ta vẫn phải mất một chút thời gian, đó không chỉ là việc Redux đã hoạt động, mà còn là nó đã hoạt động như thế nào? h1.

Ta phân chia các thành phần của React thành 2 phần, component và container. Lý do để làm điều này, là để làm cho nó dễ dàng hơn để map các trạng thái đến với các property (và phục vụ cho data-binding) trong container, và nhiệm vụ của component chỉ là view-layer.

Như vậy, file container sẽ có dạng:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Chat from '../components/Chat';
import * as ChatActions from '../actions/chat';

function mapStateToProps(state) {
  return {
    messages: state.chat
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(ChatActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Chat);

Sẽ có nhiều thứ cần làm hơn nếu bạn đọc tài liệu của React và Redux, nhưng trong vic sụ demo này, chỉ cần vậy là bạn có thể gửi các action được rồi.

Như vậy, file component sẽ có dạng:

import React, { Component, PropTypes } from 'react';

class Chat extends Component {

  render() {

    const { messages, addMessage } = this.props;

    const handleSubmit = (e) => {
      e.preventDefault();
    };

    const handleKeyUp = (e) => {
      if(e.keyCode == 13){
        App.room.speak(e.target.value);
        e.target.value = "";
      };
    };

    return (
      <div>
        <ul>
          {messages.map((msg) => {
              return <li key={`chat.msg.${msg.id}`}>{msg.content}</li>;
            })
          }
        </ul>
        <form onSubmit={handleSubmit}>
          <input type="text" onKeyUp={handleKeyUp}/>
        </form>
      </div>
    );
  }
}

Chat.propTypes = {
  messages: PropTypes.any
};

export default Chat;

4. Jbuilder

Tôi có một tinh chỉnh đó là việc dịch từ DHH's tutorial sang Redux/React để sử dụng được Jbuilder files:

jbuilderfile.png

Khi đó file index.json.jbuilder như sau:

json.messages(Message.limit(10).order("created_at DESC").load.reverse) do |message|
    json.partial! 'messages/message', message: message
end

Do đó chúng ta load 10 messages gần nhất, và message có cấu trúc đơn giản như sau:

    json.extract! message, :id, :content

Cách viết trong file index.html.erb có thể đơn giản như sau:

<%= react_component(Root',
    render(template: “messages/index.json.jbuilder”) %>

Do đó khi ứng dụng Rails được load, nó gửi một trạng thái khởi tạo(một đối tượng message ở dạng JSON format) tới ứng dụng Redux/React.

Cuối cùng trong file MessageBroadCastJob thì DHH được nói tới, tôi sẽ đơn giản hóa nó và trả về JSON:

class MessageBroadcastJob < ApplicationJob
    queue_as :default

    def perform(message)
        ActionCable.server.broadcast 'room_channel',
        message:ActiveSupport::JSON.decode(render_message(message))
    end

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

Trên đây là một đoạn tut nhỏ để demo ứng dụng live chat, và các bạn có thể demo trực tiếp ứng dụng tại địa chỉ: https://rails5reduxchat.herokuapp.com/

Chúng ta cũng phải mất một chút thời gian để set up ứng dụng trên production eviroment cho Redis/ActionCable, hãy để ý tôi viết trong file cable.coffee và update file production.rb config để include action_cable configs cho production. Và cuối cùng là source để các bạn tham khảo: https://github.com/wclittle/Rails5-ActionCable-Redux-React-ChatAppExample