使用 Rails 6 中的 Sidekiq、Redis 和 Devise 构建实时通知系统
免责声明:
刚刚发布了一篇关于如何在 Rails 7 中使用 Hotwire 实现此功能的新文章。
你也可以看看这篇,我在这里解释一下新篇里没提到的内容 ;)
在这篇文章中,我们将深入探讨异步函数,这是如今非常流行的技术,但在几年前却并不流行。
我的意思并不是说这一定是什么新鲜事,但我相信,得益于如今的 JS 生态系统,世界正在实时运转。
在这篇文章中,我想讲解其中的关键概念。但和往常一样,我们不会停留在理论层面,而是会看到一个实际的应用实例,例如我们应用程序的实时通知功能。
我会尽量言简意赅。
定义和概念 -
异步编程:指的是程序中发生的事件独立于主程序执行,并且互不干扰。在此之前,我们不得不等待响应才能继续执行,这严重影响了用户体验。
WebSocket: WebSocket 代表了客户端/服务器 Web 技术期待已久的一次革新。它允许在客户端和服务器之间建立一个长期保持的单个 TCP 套接字连接,从而实现双向、全双工消息的即时分发,开销极低,最终形成极低延迟的连接。继续阅读
换句话说,它允许我们在客户端和服务器之间建立点对点连接。在此之前,只有客户端知道服务器在哪里,反之则不然。
这样一来,我们就可以向服务器发送请求,并继续执行程序,而无需等待您的响应。然后服务器就能知道客户端的位置,并向您发送响应。
开始吧👊
以上内容对于我们的通知系统来说都很有意义,对吧?
在继续之前,请确保您已安装 Redis。Sidekiq 使用 Redis 来存储所有任务和操作数据。
👋如果您还不了解 Redis,可以在其官方网站上了解一下。
Sidekiq帮助我们以超级简单高效的方式在后台工作。(也是我最喜欢的工具之一♥️)
我创建这个项目是为了配合这篇文章,以便更专注于我们感兴趣的内容。这个项目是一个简单的博客,具备用户身份验证功能以及显示通知所需的前端界面。您可以下载它,并和我一起阅读这篇文章。
注意:您可以在“notifications”分支中查看完整的实现。
初始化配置...
我们将挂载ActionCable(基于 WebSocket 的实时通信框架)config/routes.rb的路由。
Rails.application.routes.draw do
# everything else...
mount ActionCable.server => '/cable'
end
现在,你还记得 WebSocket 的工作原理吗?点对点连接,换句话说,也是一个通道(就像我们在 Rails 中称呼的那样),在这个通道中,我们必须始终标识每个用户。这样服务器才能知道要回复谁,以及是谁发出的请求。在这个例子中,我们将使用 user.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
我们将保存已登录用户的 cookie(这有助于我们从其他地方获取该用户信息,稍后会详细介绍),一个有趣的解决方案(至少在 Devise 中)是使用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
注意:: item是一个多态关联,我这样做是为了方便他们添加各种类型的通知。
让我们在迁移过程中详细说明这一点以及其他细节(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我们将做一些事情,到时候再看情况。
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
让我们创建一个关注点,记住 Rails 最受推崇的理念之一是 DRY(不要重复自己),目前,每个通知都需要相同的内容才能正常工作(在模型中)(再次强调,在这个项目中我们只有发布,但我们可能还有许多其他东西想要集成到我们的通知系统中,所以使用这种形式非常简单)。
为此,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返回相应的修复结果。让我们来做一下(app/models/post.rb):
class Post < ApplicationRecord
include Notificable
belongs_to :user
def user_ids
User.all.ids # send the notification to that users
end
end
我们将创建一个负责发送通知的任务,这些通知将在后台发送,并由 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 install
我们将告诉 Rails,我们将使用 Sidekiq 来处理队列适配器上的任务config/application.rb:
# everything else...
module Blog
class Application < Rails::Application
# everything else...
config.active_job.queue_adapter = :sidekiq
end
end
我们还将设置 Sidekiq 提供的路由,其中包括一个后台管理系统(稍后您可以通过 localhost:3000/sidekiq 访问),这非常有趣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 库可以轻松实现这一点(例如这个),但为了避免帖子过大,我们将在控制台打印一条简单的消息。所以:
// 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
我将创建一个 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
在导航栏中添加链接,例如我的示例中的链接是(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 个命令。
就这些了,希望对你有用👋
文章来源:https://dev.to/matiascarpintini/real-time-notification-system-with-sidekiq-redis-and-devise-in-rails-6-33l9
