Sidekiq、REDISとレール6で考案リアルタイム通知システム


このポストでは、我々は非同期機能について多くの話をしようとしています.
これは必ずしも新しいものだということを意味していませんが、今日のJS生態系のおかげで、世界は現実に起こります.
このポストで、私はこれの重要な概念を教えたいです.しかし、いつものように、我々は理論に滞在していない、我々は我々のアプリケーションからのリアルタイム通知などの実際の実装を見るつもりです.
私はできるだけ簡単でシンプルにしようとします.

定義と概念


非同期プログラミング:我々のプログラムで起こるイベントの発生を参照します.これらは、メインプログラムとは独立して実行され、実行を妨げることはありません.これに先立って、我々は、ユーザーの経験を真剣に実行を継続する応答を待つことを余儀なくされた.

Concurrency, Parallelism, Threads, Processes, Async and Sync — Related? 🤔


WebSockets : WebSocketsは、クライアント/サーバーのWeb技術の長い待望の進化を表します.彼らは、長い間保持された単一のTCPソケット接続をクライアントとサーバーの間で確立させます.そして、双方向の、完全な二重のメッセージが非常に低い待ち時間接続に結果としてほとんどオーバーヘッドですぐに分配されるメッセージを考慮に入れます.Keep reading
言い換えれば、クライアントとサーバとの間の接続ごとにピアツーピアを確立することができます.この前に、クライアントはサーバがどこにあるかを知っていたが、その逆ではなかった.
これのおかげで、我々はサーバーに要求を送ることができて、あなたの応答を待つことなく、我々のプログラムを実行し続けます.その後、サーバーはクライアントがどこにあるかを知っていて、あなたに応答を送ることができます.

しましょう👊


上記のすべては、すでに我々の通知システムのために意味をなします?
続行する前にREADISをインストールしてください.SideKiqはすべてのジョブと操作データを格納するREDISを使用します.
👋 Redisが何であるかを知らないならばofficial site
Sidekiq 私たちはスーパーシンプルで効率的な方法でバックグラウンドで動作するように役立ちます.また、私の好きな宝石の1つ♥️)
私の創造this project この記事のために直接興味を集中するには.プロジェクトは、私たちの通知を表示するには、ユーザー認証と必要なフロントと簡単なブログです.あなたはそれをダウンロードし、私との記事に従ってください.
注:「通知」ブランチで完全な実装を見ることができます

initの設定


インconfig/routes.rb 我々のルートをマウントしますActionCable ( WebSocketsのリアルタイム通信のためのフレームワーク)
Rails.application.routes.draw do
  # everything else...
  mount ActionCable.server => '/cable'
end
さて、WebSocketの仕組みを覚えていますか?Peer - to - peer接続は、他の語でよく、また、チャンネル(それは我々がRailsでそれを呼ぶように)で、そのチャンネルの中で、我々は常に各々のユーザーを特定しなければなりません.これは、サーバーが誰に返信するかを知ることができますし、誰がリクエストを知っている.この場合、ユーザーとそれを識別します.id ( deviseを使っています)
だからapp/channels/application_cable/connection.rb :
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_user
    end

    def find_user
      user_id = cookies.signed["user.id"]
      current_user = User.find_by(id: user_id)

      if current_user
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
私たちはクッキーにログインしたユーザを保存します(他の場所からそれを得るのを助けるでしょう、我々は見ます)、これのためのおもしろい解決(少なくとも工夫で)は使用ですWarden Hooks
そのために、アプリケーションで初期化子を作成できます.config/initializers/warden_hooks.rb
  Warden::Manager.after_set_user do |user, auth, opts|
    auth.cookies.signed["user.id"] = user.id
    auth.cookies.signed["user.expires_at"] = 30.minutes.from_now
  end

  Warden::Manager.before_logout do |user, auth, opts|
    auth.cookies.signed["user.id"] = nil
    auth.cookies.signed["user.expires_at"] = nil
  end
さあ、私たちが作成するすべての通知を保存するために、私たちのデータベースのテーブルを作りましょう.$ rails g model Notification user:references item:references viewed:boolean
注意:アイテムはpolymorphic association , 私は様々なタイプの通知を追加することができますので、私はこのようにそれを行う
これと他の詳細についてはdb/migrate/TIMESTAMP_create_notifications.rb ):
class CreateNotifications < ActiveRecord::Migration[6.0]
  def change
    create_table :notifications do |t|
      t.references :user, foreign_key: true
      t.references :item, polymorphic: true
      t.boolean :viewed, default: false

      t.timestamps
    end
  end
end
および$ rails db:migrateインapp/models/notification.rb 我々は、我々が行くところで見る2、3のものをするつもりです
class Notification < ApplicationRecord
  belongs_to :user
  belongs_to :item, polymorphic: true # Indicates a polymorphic reference

  after_create { NotificationBroadcastJob.perform_later(self) } # We make this later

  scope :leatest, ->{order("created_at DESC")}
  scope :unviewed, ->{where(viewed: false)} # This is like a shortcut

  # This returns the number of unviewed notifications
  def self.for_user(user_id)
    Notification.where(user_id: user_id).unviewed.count
  end
end
を作成しましょうconcern , 最も尊敬された柵の哲学の一つは、(自分自身を繰り返す)、乾いていることを忘れないでください、現在、各通知は、同じように動作するようにする必要があります(モデルで)(再び、このプロジェクトでは、我々は出版物を持っていますが、我々は我々の通知システムと統合する多くの他のものを持つことができるので、この形式では、それは超簡単です).
そのためにapp/models/concerns/notificable.rb
module Notificable
  extend ActiveSupport::Concern # module '::'

  included do # this appends in each place where we call this module
    has_many :notifications, as: :item
    after_commit :send_notifications_to_users
  end

  def send_notifications_to_users
    if self.respond_to? :user_ids # returns true if the model you are working with has a user_ids method
      NotificationSenderJob.perform_later(self)
    end
  end
end
今、我々はそれを含めることができますapp/models/post.rb . 私たちのsend_notifications_to_users 方法を予想するuser_ids それぞれの修正をあなたに返信する.そのようにしましょう.
class Post < ApplicationRecord
  include Notificable
  belongs_to :user

  def user_ids
    User.all.ids # send the notification to that users
  end
end
我々は、作成するつもりですjob 通知を送信するの担当では、これは我々がバックグラウンドに送信され、我々はSideKiqで処理されます.そのために$ rails g job NotificationSender仕事の中app/jobs/notification_sender_job.rb ):
class NotificationSenderJob < ApplicationJob
  queue_as :default

  def perform(item) # this method dispatch when job is called
    item.user_ids.each do |user_id|
      Notification.create(item: item, user_id: user_id)
    end
  end
end
最終的に、SideKIQをインストールする必要があります(そして、Sinatraは少し簡単にするためにいくつかのことを作る)ので、アウトでGemfile :
# everything else...
gem 'sinatra', '~> 2.0', '>= 2.0.8.1'
gem 'sidekiq', '~> 6.0', '>= 6.0.7'
忘れないで.$ bundle installSideKIQをジョブのキューアダプタで使用するようにRailsに指示しますconfig/application.rb ):
# everything else...
module Blog
  class Application < Rails::Application
    # everything else...
    config.active_job.queue_adapter = :sidekiq
  end
end

また、SideKiqが私たちに提供するルートを設定するつもりです.その中で、バックグラウンドのためのバックオフィスの一種(後に、localhostからのaccesを持つことができます.インconfig/routes.rb :
require 'sidekiq/web'
Rails.application.routes.draw do
  # everything else...
  mount Sidekiq::Web => '/sidekiq'
end
今、我々は我々が我々の通知を送るチャンネルをつくるつもりです.$ rails g channel Notificationこのチャンネルのバックエンドでapp/channels/notification_channel.rb ), ユーザーを購読します.
class NotificationChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications.#{current_user.id}" # in this way we identify to the user inside the channel later
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
そして、チャンネルのフロントエンドapp/javascript/channels/notification_channel.js ) これは、ブラウザにプッシュ通知を送信するには興味深いですが、多くのJSライブラリが非常に簡単にthis ), しかし、ポストを非常に重くしないために、我々はコンソールに単純なメッセージを印刷するつもりです.それで
// everything else...
consumer.subscriptions.create("NotificationChannel", {
  // everything else...
  received(data) {
    if(data.action == "new_notification"){
      cosole.log(`New notification! Now you have ${data.message} unread notifications`) 
    } // we will define action & message in the next step
  }
});
この時点で、我々はすでに多くの実行している、その通知をユーザーに送信しましょう!このために、我々はちょうどこれをするもう一つの仕事をつくるつもりです、覚えていてください、前の仕事は通知をつくることを担当しています、これは放送をします.それで$ rails g job NotificationBroadcast内部app/jobs/notification_broadcast_job.rb :
class NotificationBroadcastJob < ApplicationJob
  queue_as :default

  def perform(notification)
    notification_count = Notification.for_user(notification.user_id)

    ActionCable.server.broadcast "notifications.#{ notification.user_id }", { action: "new_notification", message: notification_count }
  end
end
ファンタスティック、我々はすでにすべての作業を持っている!🎉
私は例を終了するためにバックエンドにいくつかのものを追加するつもりです.
まず第一に、私は私が私のユーザーモデルにメソッドを加えるつもりです.そして、モデルはこの質問をする良い場所です.インapp/models/user.rb :
class User < ApplicationRecord
  # everything else...
  def unviewed_notifications_count
    Notification.for_user(self.id)
  end
end
私もコントローラを作るつもりです.$ rails g controller Notifications index . コントローラ内部app/controllers/notifications_controller.rb ) いくつかのメソッドを追加します.
class NotificationsController < ApplicationController
  def index
    @notifications = Notification.where(user: current_user).unviewed.leatest

    respond_to do |format|
      format.html
      format.js
    end
  end

  def update
    @notification = Notification.find(params[:id])

    message = @notification.update(notification_params) ? "Viewed notification" : "There was an error"

    redirect_to :back, notice: message
  end

  private
  def notification_params
    params.require(:notification).permit(:viewed)
  end
end
私は、リモート対応して、NAVで私のドロップダウンで最新の通知を表示することができるJSビューを作成します.インapp/helpers/notifications_helper.rb :
module NotificationsHelper
  def render_notifications(notifications)
    notifications.map do |notification|
      render partial: "notifications/#{notification.item_type.downcase}", locals:{notification: notification}
    end.join("").html_safe
  end
end
あなたのNavで私のケースで関連を加えてくださいapp/views/partials/notifications.html.erb ):
<%= link_to notifications_path, remote: true, data:{ type:"script" } %>
忘れずにパスを追加しましょうapp/config/routes.rb ) この新しいコントローラのために.
# everything else...
Rails.application.routes.draw do
  # everything else...
  resources :notifications, only: [:index, :update]
end
この項目の一部を作成するapp/views/notifications/_post.rb ). このようにして「マークされたマーク」へのリンクを含めることができます.
<%= link_to notification_path(id: notification, notification:{viewed: true}), method: :put %>
ローカルで実行するにはREDISを実行しなければなりません.$ redis-server ) とsidekiq$ bundle exec sidekiq ) + $ rails s , これらの3つのコマンドを並列に実行すると、3ターミナルウィンドウが開きます.
それはすべて、私はあなたに役立つことを望む👋