[VR 前後端交响曲Day29] Rails专案开发 - Action Cable即时互动功能: 以edit和delete ticket为例

来到倒数第二天啦!感动流泪O_Q

前两天里用Vuex状态管理的方式实作编辑删除ticket

本日待实作的功能:利用Action Cable广播,在任何的浏览器登入,都会同步显示编辑和删除的结果~让使用者有即时互动的体验!

如下图所示,我开了两个浏览器(Safari和chrome),无论在哪个浏览器编辑删除ticket,另一个浏览器都会即时产生效果~

WebSocket: 让前端与後端没有距离

WebSocket是能让浏览器(Client端)与服务器(Server端)持续进行双向资料传递的通讯协定,Client端用Ajax非同步方式做请求, Server端也能主动发送Client所需要的资料。

相信大家都有听过HTTP吧~HTTP是基於TCP(Transmission Control Protocol, 传输控制协定)进行「三向交握(3-way handshake)建立连线(A方提出请求、B方确认A的请求并提出自己请求、A方再确认一次B的请求,总共三次。)

而WebSocket是进阶的网路应用层协议(在2011年由IETF标准化)。如果Client端初始化时HTTP标头中含有WebSocket的资讯,当连线Client端到Server端,Server端会将HTTP连线升级为WebSocket的连接,并返回同样包含websocket标头资讯的HTTP回应,就能立即实现持续的双向沟通,因此WebSocket常用来实作可以互相传讯息的聊天室。

ActionCable: 整合client端(JavaScript介面)以及server端

ActionCable是建构於WebSocket上、Pub(消息发布者)/Sub(消息订阅者)模型的Ruby on Rails框架,Publisher和Subscriber会以javascript透过非同步(async)的方式传递讯息,使讯息传递不需等待回应就可进行後续操作(想像聊天的时候,你不用等收到别人讯息的时候才能传出自己的讯息),非常便利!接下来今天的文章开始参考Rails Guides: Action Cable ,并使用Redis这款带有Pub/Sub功能的资料库实作。

Step1. 设定Server端连线

通常看到网路上的教学步骤,都会请你先new一个rails专案、建立User model以及必要的controller。
由於我们现在这个铁人赛专案已经有了很多个controller(kanban, column, ticket...XD),就直接挑比较符合业务逻辑的controller加上channel。

首先我来rails g channel建立一个channel,名叫column

rails g channel column

Running via Spring preloader in process 70377
      invoke  rspec
      create    spec/channels/column_channel_spec.rb
      create  app/channels/column_channel.rb

1-1 订阅频道

接着来修改channels/column_channel.rb这个档案,在里面指定订阅的频道是刚刚建立的column

class ColumnChannel < ApplicationCable::Channel
  def subscribed
    stream_from "column"
  end

  def unsubscribed
  end
end

1-2 建立连线前,进行身份验证

想像一个软件有群组聊天以及私人一对一聊天的功能,我们需要符合身份的人(也就是有订阅这个频道的人),有值的话才能回传给他特定的讯息,不然就拒绝连线。
那要怎麽实作这个功能呢?

最一开始使用Rails的devise gem制作注册登入系统,里已经提供给我们适当的环境变数env["warden"]
在这里,把它透过byebug印出来看看:

10:49:52 web.1       | (byebug) env["warden"]
10:49:52 web.1       | Warden::Proxy:70111027317460 @config={:default_scope=>:user, :scope_defaults=>{}, :default_strategies=>{:user=>[:rememberable, :database_authenticatable]}, :intercept_401=>false, :failure_app=>#<Devise::Delegator:0x00007f87fa2cc638>}

10:49:52 web.1       | (byebug) env["warden"].user
10:50:30 web.1       | #<User id: 1, email: "tingtinghsu[at]秘密", created_at: "2020-10-01 01:43:03", updated_at: "2020-10-01 07:12:27", name: "Ting">

我们在channels/application_cable/connection.rb进行连线前的身份验证,如果env["warden"].user有值,指定给current_user

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    #这里引入current_user
    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        if current_user = env["warden"].user
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
  end
end

Step 2. 设定client端连线

接下来是浏览器端了!

2-1 Consumer.js (Rails的产生预设档案)

虽然这一份js档案不是我们自己写的,不过还是可以来研究一下大神们开发的架构,这个档案会让consumer预设与server的cable做连线。

// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"
export default createConsumer()

2-2 Consumer连线後,会成为Subscriber订阅者:建立 column_channel.js

Consumer连线後成为Subscriber後就会进来这里。有三个部分:connected()received(data)disconnected()
我们可以在浏览器的dev tool里console分别印出连线状况,帮助debug。

app/frontend/channel/column.js

import consumer from "./consumer"
console.log("loading")

consumer.subscriptions.create("ColumnChannel", {
  connected() {
    console.log("connected")
  },

  received(data) {
    console.log("Welcome to ColumnChannel")    
    console.log(data)

    if(data.commit){
      //等等会用到这个区块 
    }
  },

  disconnected() {
    console.log("disconnected")
  }
});

Step 3. Rails MVC: 透过 controller 广播

3-1 统一让後端资料变更成功时广播送出commit

把专案的编辑删除ticket的controller动作结合action cable,资料更动成功的话,透过ActionCable.server.broadcastcolumn channel做广播,要commit的那一包payload为转为string的:show.json

  def update
    respond_to do |format|
      if @ticket.update(ticket_params)     
      ActionCable.server.broadcast("column", { commit: 'UPDATE_TICKET', payload: render_to_string(:show, format: :json)})        
        format.json { render :show, status: :ok}
      else
        format.json { render json: @ticket.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @ticket.destroy
    respond_to do |format|
      puts "destroy success"
      ActionCable.server.broadcast("column", { commit: 'DELETE_TICKET', payload: render_to_string(:show, format: :json)})       
      format.json { head :no_content }      
    end
  end

ps.这边的点我卡了几个小时,因为一直没有render出自己要的json档,後来发现要把scaffold长出来没用到的html.erb先删掉,Rails才不会找错档案。

3-2 修改Vuex action,前端打资料去後端时不需重复commit

记得把原本Vuex action内的commit注解掉,因为现在我们已经透过controller进行全域的广播了(不然会操作的那一方浏览器会commit两次)

  actions: {
    updateTicket({ commit }, {id, name}){
      // 略
      Rails.ajax({
        url: `/kanbans/${el.dataset.kanbanid}/tickets/${id}`,
        type: 'PUT',
        data,
        dataType: 'json',
        success: result => {
          // commit("UPDATE_TICKET", result);
          console.log(result);
        },
        error: error => {
          console.log(error);            
        }
      });      
    },
    deleteTicket({ commit }, {ticket_id, column_id}){
      //略
      Rails.ajax({
        url: `/kanbans/${el.dataset.kanbanid}/tickets/${ticket_id}`,
        type: 'DELETE',
        dataType: 'json',
        success: result => {
          // commit("DELETE_TICKET", {ticket_id, column_id});
        },
        error: error => {
          console.log(error)
        }
      });
    },

Step 4. Subscriber订阅者处理需要commit的data

4-1. 让Vuex的$store变成全域变数

之前做专案时,如果是透过webpacker引入jQuery套件,有一个偷吃步的方法可以把$字符号设为全域都可使用
就是在application.js加上window.$ = $

import $ from 'jquery'
window.$ = $

那麽为了让我们可以在Action Cable的client端也可以对store做事情,
我们如法炮制,在application.js这个Vue的挂载点加上这句:

window.$store = store;

4-2. Subscriber订阅者收到资料received(data)

vuex限制commit只能传2个参数,第一个是function名称,第二个用object包起来的参数(称为payload)。
我们把收到的data,利用window.$store.commit(data.commit, JSON.parse(data.payload));重新渲染自己的页面

app/frontend/channel/column.js

import consumer from "./consumer"

consumer.subscriptions.create("ColumnChannel", {
  connected() {
  },

  received(data) {
    if(data.commit){
      console.log("data commit!")
      window.$store.commit(data.commit, JSON.parse(data.payload));      
    }
  },

  disconnected() {
  }
});

完成搂!在哪个浏览器编辑删除ticket,另一个浏览器都会即时产生效果~

实作完今天的单元後,又对於WebSocket这个浏览器(Client端)与服务器(Server端)进行双向资料传递的通讯协定更加清晰了!

心得:

  1. 这并不是我第一次实做action cable,但是第一次透过结合Vuex状态管理来做ActionCable.server.broadcast,算是一个自己的大突破~~本来预计想做的是拖拉的action cable即时效果(但还没克服bug)紧急换成实作删除ticket修改ticket的即时互动效果~终於在自己规定的时限内研究出来+把文章写好。
  2. 後来发现Vue自己也有actioncable-vue 的套件,铁人赛结束後我也会来玩玩看!
  3. 做完action cable功能终於能够呼应本次铁人赛的名称「前端後端交响曲」了~~放烟火!

Ref:


<<:  动员大外宣: 让资安进步成为公司的光环(向外)

>>:  LeetCode 1. Two Sum

截取Video画面,存成一张张图片Python cv2

找到一个有趣的程序码,改了一下,可截取Video画面,存成一张张图片。 进行中想要中断执行,可按 E...

DAY14 呼叫功能列表样板

@csrf_exempt def callback(request): if request.met...

[想试试看JavaScript ] for回圈

回圈 for 回圈 for 回圈,很适合用来处理数值会依照次数,有「递增」或「递减」的变化 范例如下...

Django template - javascript变数含safe filter

这边有一个javascript变数: var subtitles = {{ json_dual }}...

第 22 集:Bootstrap 客制化 utilities(下)

此篇延续 Bootstrap 客制化 Sass utilities(上)最後尚未介绍的 gener...