发布于 2026-01-06 2 阅读
0

使用 Rails、Noticed 和 Hotwire AWS AI 实时通知用户!

使用 Rails、Noticed 和 Hotwire 实现用户通知

AWS AI 直播!

用户通知几乎是所有 Web 应用程序的必备功能。当应用程序中发生用户关心的事件时,您需要通知用户该事件。一个常见的例子是带有评论系统的应用程序——当用户在评论中提及其他用户时,应用程序会通过电子邮件通知被提及的用户。

用于通知用户重要事件的渠道取决于应用程序,但通常情况下,通知路径会包含一个应用内通知小部件,用于向用户显示最新通知。其他常见选项包括电子邮件、短信、Slack 和 Discord。

需要为 Rails 应用程序添加通知系统的 Rails 开发者通常会选择Noticed。Noticed是一个 gem,它可以轻松地为 Rails 应用程序添加新的多渠道通知。

今天,我们将通过使用 Noticed 来实现应用内用户通知,来了解 Noticed 的工作原理。我们将使用Turbo Streams将这些通知实时发送给已登录用户,并且为了增加趣味性,我们还将把用户通知加载到Turbo Frame

完成后,我们的应用程序将按如下方式运行:

屏幕录像显示两个浏览器窗口并排打开。在一个浏览器中,用户填写并提交表单。在另一个浏览器中,表单提交后,用户填写的表单信息会显示在“通知”标题下。

在开始之前,本教程假设您已经能够独立构建简单的 Ruby on Rails 应用程序。无需事先了解 Turbo 或 Noticed。

让我们开始吧!

应用程序设置

要跟随本教程进行操作,首先从 Github 克隆此存储库,然后进行设置:

cd user-notices
bin/setup
Enter fullscreen mode Exit fullscreen mode

初始仓库包含一个 Rails 7 应用程序,其中已预装了 Turbo、Tailwind 和 Devise 框架。初始仓库使用 Ruby 3.0.2,但本教程中的所有内容也适用于 Ruby2.73.1.NET(如果您愿意)。

如果您想使用自己的应用程序而不是克隆起始应用程序,则需要一个安装了 Turbo 的 Rails 7 应用程序和一个围绕User模型构建的身份验证系统。

准备开始构建时,启动服务器并使用以下命令构建 Tailwind 的 CSS bin/dev

注意到设置

我们的初始应用程序包含一个基于 Devise 的用户模型,根路径已设置为操作,Dashboard#show目前该操作仅包含登录和注销链接。在深入代码之前,请先通过http://localhost:3000/users/sign_up上的表单创建一个用户,以便您稍后可以登录并测试本教程中的通知功能。

最终,我们的用户将能够Messages在应用程序中为其他用户创建消息。每次创建新消息时,系统Notification都会创建一个新通知,消息接收者会在其仪表盘上看到该通知。

在此之前,我们需要将 Noticed 添加到我们的应用程序中,并搭建一个 Message 资源。首先,在终端中添加 Noticed:

bundle add noticed
rails generate noticed:model
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

这些命令直接来自 Noticed 的安装文档。如果你的 Rails 应用正在运行,请确保在将 Noticed gem 添加到 Gemfile 后重启它bundle add

接下来,更新app/models/user.rb通知与用户关联功能:

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :notifications, as: :recipient
end
Enter fullscreen mode Exit fullscreen mode

在这里,我们notifications has_many按照 Noticed 设置脚本的指示添加了关联。

现在我们的用户可以接收通知,但我们没有任何有用的信息可以通知他们。我们将通过搭建Message资源框架来解决这个问题。请在终端中执行以下操作:

rails g scaffold message content:text user:references
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

多亏了 Rails 的强大功能,脚手架生成器几乎提供了我们创建消息并将其与用户关联所需的一切。由于我们通过tailwindcss-rails gem 使用了 Tailwind,脚手架生成器还包含了一些美观的基础样式。

生成器运行后,User再次更新模型app/models/user.rb

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :messages
  has_many :notifications, as: :recipient
end
Enter fullscreen mode Exit fullscreen mode

在这里,我们添加了has_many :messages设置消息关系的另一端。

为方便起见,我们还可以添加指向消息索引页面的链接app/views/dashboard/show.html.erb

<div>
  <% if user_signed_in? %>
    Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
    <%= link_to "All messages", messages_path, class: "text-blue-500" %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

然后对消息表单进行一些小调整,这样我们就不用记住用户 ID 了:

<%= form_with(model: message, class: "contents") do |form| %>
  <% if message.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>

      <ul>
        <% message.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :content %>
    <%= form.text_area :content, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="my-5">
    <%= form.label :user_id %>
    <%= form.select :user_id, options_for_select(User.all.pluck(:email, :id)), class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

完成这些更改后,请访问http://localhost:3000/messages并创建几条消息,以确保创建消息的功能符合预期。

现在 Noticed 已经安装完毕,消息功能也已准备就绪,接下来我们将会在创建新消息时向用户发送通知。

消息通知

本节的目标是在Dashboard#show页面上创建并显示向已登录用户的通知。

第一步是使用 Noticed 内置的通知生成器添加新通知。在终端中:

rails generate noticed:notification MessageNotification
Enter fullscreen mode Exit fullscreen mode

此生成器会MessageNotification在指定位置创建一个新类app/notifications/message_notification.rb。接下来,请前往该位置并进行一些小的更新:

class MessageNotification < Noticed::Base
  deliver_by :database

  param :message

  def message
    params[:message].content
  end

  def url
    message_path(params[:message])
  end
end
Enter fullscreen mode Exit fullscreen mode

此处deliver_by :database将新创建的通知存储在数据库中,这对于后续的 Turbo Stream 广播非常重要。如果我们也想通过电子邮件发送通知,可以deliver_by :email, mailer: SomeMailer按照 Noticed文档中的说明进行添加。

param :message将对象序列化message并将其与通知记录一起存储在数据库中。我们message在 ` messageand`url方法中使用该序列化对象。以这种方式将对象序列化为参数,可以轻松访问与通知相关的记录,而无需管理复杂的引用——只需将所需的记录转储到params参数中即可。

现在我们可以使用我们的功能MessageNotification在新消息创建时向用户发送通知。接下来,我们需要将该MessageNotification类投入使用。请前往app/models/message.rb并更新它:

class Message < ApplicationRecord
  has_noticed_notifications

  belongs_to :user

  after_create_commit :notify_user

  def notify_user
    MessageNotification.with(message: self).deliver_later(user)
  end
end
Enter fullscreen mode Exit fullscreen mode

每次创建消息时after_create_commitnotify_user都会运行并创建一个新的对象MessageNotification,序列化该message对象并将消息传递给消息的用户。我们还添加了has_noticed_notifications 函数,以确保当消息被销毁时,所有相关的通知也会被销毁。

经过这些小小的改动,我们现在就拥有了一个基于数据库的通知系统,并且运行正常。真棒!

我们目前已在数据库中存储了通知,但用户无法在任何地方查看这些通知,这不太方便。接下来,我们将创建一个Notifications控制器来向用户显示通知。

在终端中,生成控制器和用于渲染每个通知的局部视图:

rails g controller Notifications index
touch app/views/notifications/_notification.html.erb
Enter fullscreen mode Exit fullscreen mode

前往config/routes.rb并添加通知路径助手:

Rails.application.routes.draw do
  resources :notifications, only: [:index]
  resources :messages
  devise_for :users
  get 'dashboard/show'
  root "dashboard#show"
end
Enter fullscreen mode Exit fullscreen mode

更新NotificationsControllerat app/controllers/notifications_controller.rb

class NotificationsController < ApplicationController
  def index
    @notifications = Notification.where(recipient: current_user)
  end
end
Enter fullscreen mode Exit fullscreen mode

在这里,我们将范围限定notifications在应用程序内current_user,因此用户只有在登录应用程序后才能看到自己的通知。

更新新的通知索引视图app/views/notifications/index.html.erb

<%= turbo_frame_tag "notifications" do %>
  <h1 class="font-bold text-4xl">Notifications</h1>
  <ul>
    <%= render @notifications %>
  </ul>
<% end %>
Enter fullscreen mode Exit fullscreen mode

请注意turbo_frame_tag通知列表的包装方式。我们的计划是在仪表盘显示页面上渲染通知列表——我们将使用 Turbo Frame 的预加载功能,将通知索引页面的内容加载到仪表盘显示页面上。

render @notifications它依赖 Rails 的集合渲染功能来渲染每个通知。在此之前,我们需要填写以下内容app/views/notifications/_notification.html.erb

<li>
  <div>
    <p class="text-gray-700">
      <%= notification.to_notification.message %>
    </p>
    <div class="flex justify-between mt-1 text-gray-500 text-sm space-x-4">
      <p>
        Received on <%= notification.created_at.to_date %>
      </p>
      <p>
        Status: <%= notification.read? ? "Read" : "Unread" %>
      </p>
    </div>
  </div>
</li>
Enter fullscreen mode Exit fullscreen mode

在这里,我们使用to_notificationNoticed 来访问message我们之前添加的方法MessageNotification,并使用 Noticed 的内置read?方法来检查用户是否已阅读通知。

用户在仪表盘上看到通知之前,还需要完成最后一步。请前往app/views/dashboard/show.html.erb并更新设置,为已登录用户添加预加载的 Turbo Frame:

<div class="flex justify-between">
  <% if user_signed_in? %>
    <div>
      Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
      <%= link_to "All messages", messages_path, class: "text-blue-500" %>
    </div>
    <%= turbo_frame_tag "notifications", src: notifications_path %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

turbo_frame_tag元素的 id 为notifications,与由 渲染的 Turbo Frame 相匹配Notifications#index。当 Turbo 为 Turbo Frame 预加载内容时,它期望传递给 的 url 返回src一个包含具有匹配 id 的 Turbo Frame 的响应。

我们的预加载通知索引页面的事件顺序如下:

  • 已登录用户访问Dashboard#show
  • Dashboard#show已加载
  • Turbo 检测turbo_frame_tag到该src属性后,会发起一个新的请求。/notifications
  • Notifications#index返回包含与turbo_frame_tag发起请求的标签匹配的 HTML 代码
  • Turbo 会提取内容turbo_frame_tag并使用该内容替换现有 Turbo Frame 的内容。

此时,已登录用户可以在控制面板上看到他们的通知。您可以登录该用户并创建一条新消息进行测试。然后以该用户身份刷新控制面板,即可看到您的通知已列在控制面板上:

网页打开后,在“通知”标题下显示通知列表的屏幕截图。

虽然我们的用户可以看到通知,但他们无法实时看到这些通知——他们必须手动刷新仪表盘才能看到新通知。让我们通过 Turbo Stream 广播实现通知实时显示,来结束本教程。

Turbo Streams 的实时通知

基于Turbo Rails 的Turbo 模型广播功能,可轻松通过 ActionCable 向用户发送实时更新。本节结束后,每当创建新通知时,都会通过 ActionCable 通道发送 Turbo Stream 广播,自动将新通知添加到用户的仪表盘通知列表中。

首先,更新配置app/models/notification.rb,以便在创建新通知时触发 Turbo Stream 广播:

class Notification < ApplicationRecord
  include Noticed::Model
  belongs_to :recipient, polymorphic: true

  after_create_commit :broadcast_to_recipient

  def broadcast_to_recipient
    broadcast_append_later_to(
      recipient,
      :notifications,
      target: 'notifications-list',
      partial: 'notifications/notification',
      locals: {
        notification: self
      }
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

这里,该broadcast_to_recipient方法appends会将一条新通知添加到通知列表中。为了确保只有通知的目标用户才能收到广播,我们将广播通道设置为recipient, :notifications,如turbo-rails 源代码中所述。

模型更新会处理 Turbo Stream 广播的发送,但在前端接收到广播之前,我们需要将用户订阅到 Turbo Stream 频道。此外,我们还必须确保标记中包含一个notifications-listID(target与传递给`TurboStream` 的 ID 匹配broadcast_append_later_to),以便 Turbo Stream 能够进行更新。

从 [日期] 开始app/views/notifications/index.html.erb,将notifications-listid 添加到ul包含通知列表的 [电子邮件地址] 中:

<%= turbo_frame_tag "notifications" do %>
  <h1 class="font-bold text-4xl">Notifications</h1>
  <ul id="notifications-list">
    <%= render @notifications %>
  </ul>
<% end %>
Enter fullscreen mode Exit fullscreen mode

然后是app/views/dashboard/show.html.erb

<div class="flex justify-between">
  <% if user_signed_in? %>
    <div>
      Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
      <%= link_to "All messages", messages_path, class: "text-blue-500" %>
    </div>
    <%= turbo_stream_from current_user, :notifications %>
    <%= turbo_frame_tag "notifications", src: notifications_path %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

这里,turbo_stream_fromturbo-rails 的辅助函数会将用户订阅到一个与我们的模型正在广播的频道相匹配的频道(current_user, :notifications在视图中 ==recipient, :notifications在模型中)。

当用户访问控制面板时,turbo_stream_from辅助程序会为匹配的频道打开一个 ActionCable 订阅。Turbo 会接收发送到该频道的广播,并在收到新广播时更新页面。频道范围的限定current_user确保用户不会收到原本发送给其他用户的广播,从而使我们的新消息通知仅发送给消息的目标用户。

完成这些更改后,请在一个浏览器中以用户身份登录并前往控制面板。通过检查服务器日志中类似如下的行来确认 ActionCable 频道订阅已创建:

Turbo::StreamsChannel is streaming from Z2lkOi8vdXNlci1ub3RpY2VzL1VzZXIvMQ:notifications
Enter fullscreen mode Exit fullscreen mode

在另一个浏览器中,打开新消息表单并创建一条消息,将表单中的用户设置为您在第一个浏览器中登录的用户。如果一切顺利,新通知将立即添加到列表中,无需更新页面。

屏幕录像显示两个浏览器窗口并排打开。在一个浏览器中,用户填写并提交表单。在另一个浏览器中,表单提交后,用户填写的表单信息会显示在“通知”标题下。

非常感谢你跟着教程一步一步学习,今天的代码就到这里啦!

总结与延伸阅读

今天,我们使用 Rails 和 Noticed 构建了一个简单的通知系统,并使用 Turbo Streams 为用户实时显示新通知。Noticed 是一个功能极其强大的 gem,它大大简化了在 Rails 应用中构建和扩展多渠道通知系统的工作,并且与 Turbo Streams 的轻松集成使其成为任何现代 Rails 应用的理想之选。

在我们的教程应用中,我们将通知显示为一个静态列表,列出用户收到的所有通知,用户无法与通知进行交互或将其从列表中删除。此外,我们还要求用户必须登录到控制面板才能看到新通知。

在生产应用中,我们可能会扩展通知控制器,使其包含一个update允许用户将通知标记为已读并从列表中清除这些通知的方法。

我们还可能会在应用程序的每个页面上呈现的主导航中构建一个通知指示器,其中包含一个指示未读通知的图标(想想在成千上万个 Web 应用程序中随处可见的铃铛图标)。

本教程的巧妙之处在于,即使是更复杂的实现,基本方法也保持不变。使用 Turbo Streams 向用户广播新通知并实时更新 UI。使用内置的 Noticed 方法来处理通知(例如mark_as_read!读取通知)。使用 Turbo Frame 获取用户的通知,并将这些通知加载到页面的一部分中。

有关 Noticed、Turbo Streams 和 Turbo Frames 的更多学习资料:

最后,如果您想深入了解如何在 Rails 应用中实现通知,我的书中有一章专门介绍如何从零开始构建一个非常轻量级的通知系统,其灵感来源于 Noticed。在这一章中,我们以更贴近实际的方式添加了实时更新,包括标准的铃铛图标、弹出式通知列表以及点击即可将通知标记为已读的功能。本书采用同样的循序渐进的教程风格,涵盖了如何使用 StimulusReflex、CableReady、Hotwire 等工具从头开始构建一个现代化的 Rails 应用。

如果您以后想在实际生产应用程序中使用 Noticed,那么构建自己的通知系统是一个很好的练习,因为您将对 Noticed 提供的功能有更深入的了解和体会。

今天就到这里。一如既往,感谢阅读!

文章来源:https://dev.to/davidcolbyatx/user-notifications-with-rails-noticed-and-hotwire-39ga