使用 Active Delivery 在 Rails 中制作用户通知

2025-06-08

使用 Active Delivery 在 Rails 中制作用户通知

Rails框架就像一把瑞士军刀,提供了很多开箱即用的有用功能(并且它变得越来越像瑞士)。

它建立在子框架之上,例如 ActiveRecord、ActiveJob、ActionCable (❤️)、ActionMailer ……好吧,我们就此打住。ActionMailer
的目的是什么?

ActionMailer 是发送电子邮件的抽象(尽管现在我们有ActionMailbox,但它也可以接收电子邮件)。

它抽象了传递机制并提供了Railsy API 来构建消息。

因此,向用户发送电子邮件通知对于 Rails 应用程序来说并不是什么大问题。

问题在于,在现代世界中,我们有许多不同的方式来发送通知,不仅仅是电子邮件:推送通知、聊天机器人、短信、鸽子

注意: DDH提到了一些“动作通知器”框架,“尚未从 Basecamp 中提取”,这听起来像是一个解决方案;但我们还没有到那一步。

像这样的代码很常见:

def notify_user(user)
  MyMailer.with(user: user).some_action.deliver_later if user.receive_emails?
  SmsSender.send_message(user, "Something happened") if user.receive_sms?
  NotifyService.send_notification(user, "action") if whatever_else?
end
Enter fullscreen mode Exit fullscreen mode

代码库中可能还有几十个这样的地方。祝您维护和测试代码顺利!

我们如何重构这段代码?也许我们需要另一层抽象

这就是Active Delivery——我为解决这个难题而编写的一篇新文章。

Active Delivery 是一个为所有类型的通知提供入口点的框架:邮件程序、推送通知,任何您想要的。

它可以帮助您按以下方式重写上面的代码:

def notify_user(user)
  MyDelivery.with(user: user).notify(:some_action)
end
Enter fullscreen mode Exit fullscreen mode

甚至更多——您现在可以优雅地测试它:

# my_something_spec.rb
expect { subject }.to have_delivered_to(MyDelivery, :some_action).
  with(user: user)
Enter fullscreen mode Exit fullscreen mode

它是如何工作的?

在最简单的情况下,递送仅仅是邮件上的包装:

# suppose that you have a mailer class
class MyMailer < ApplicationMailer
  def some_action
    # ...
  end
end

# the corresponding delivery could look like this
class MyDelivery < ActiveDelivery::Base
  # here we can also apply "delivery rules"
  before_notify :ensure_receive_emails, on: :mailer

  def ensure_receive_emails
    # returning `false` halts the execution
    params[:user].receive_emails?
  end
end

# when you call
MyDelivery.with(user: user).notify(:some_action)

# it invokes under the hood (only if user receives emails)
MyMailer.with(user: user).some_action.deliver_later
Enter fullscreen mode Exit fullscreen mode

我们依靠约定优于配置来推断相应的邮件程序类。

好的。我们刚刚把邮件包好。怎么办?其他投递方式怎么办?

我们先来看一下框架的架构:

主动交付架构

请注意,这里有一个内部层——线路。每条线路都是传递和实际通知渠道(例如邮件程序)之间的连接器。

Active Delivery 提供了一个 API 来添加自定义交付线路——这样您就可以实现几乎任何类型的通知!

为了使其变得更加简单,我们构建了另一个微框架——Abstract Notifier

这是一个非常抽象的框架:它所做的就是提供一个类似 Action Mailer 的 API 来描述通知器类,纯 Ruby 抽象,对“如何发送通知”一无所知。

为什么要采用类似 Action Mailer 的界面?首先,它是一个熟悉且简洁的 API。而且我喜欢它的参数化类功能(我们在 Active Delivery 中大量使用了此功能)。

要“教”抽象通知器如何发送通知,您必须实现一个驱动程序(任何可调用对象)。

例如,我们使用Twilio Notify进行推送通知,我们的驱动程序ApplicationDeliveryApplicationNotifier类如下所示:

class TwilioDriver
  attr_reader :service

  def initialize(service_id)
    client = build_twilio_api_client
    @service = client.notify.services(service_id)
  end

  def call(params)
    service.notifications.create(params)
  end
end

class ApplicationDelivery < ActiveDelivery::Base
  # NOTE: abstract_notifier automatically registers its default line,
  # you don't have to do that
  #
  # Default notifier infers notifier classes replacing "*Delivery* with
  # "*Notifier"
  register_line :notifier, ActiveDelivery::Lines::Notifier
end

class ApplicationNotifier < AbstractNotifier::Base
  self.driver = TwilioDriver.new(Rails.application.config.twilio_notify_id)
end
Enter fullscreen mode Exit fullscreen mode

现在让我们定义我们的传递、邮寄和通知类:

class PostsDelivery < ApplicationDelivery
  # here we can define callbacks, for example,
  # we want to enforce passing a target user as a param
  before_notify :ensure_user_provided  

  def ensure_user_provided
    raise ArgumentError, "User must be passed as a param" unless params[:user].is_a?(User)
  end

  # in our case we have a convenient params-reader method
  def user
    params[:user]
  end
end

class PostsMailer < ApplicationMailer
  def published(post)
    mail(
      to: user.email,
      subject: "Post #{post.title} has been published"
    )
  end
end

class PostsNotifier < ApplicationNotifier
  # Btw, we can specify default notification fields
  default action: "POSTS"

  def published(post)
    notification(
      body: "Post #{post.title} has been published",
      identity: user.twilio_notify_id
      # you can pass here any fields supported by your driver
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

最后,这就是我们触发通知的方式:

PostsDelivery.with(user: user).notify(:published, post)
Enter fullscreen mode Exit fullscreen mode

如果需要更多通知渠道怎么办?我们可以添加另一行通知程序到ApplicationDelivery

class ApplicationDelivery < ActiveDelivery::Base
  register_line :notifier, ActiveDelivery::Lines::Notifier

  register_line :pigeon,
                ActiveDelivery::Lines::Notifier,
                # resolver is responsible for inferring
                # the notifier class from
                # the delivery class name
                resolver: ->(name) { name.gsub(/Delivery$/, "Pigeon").safe_constantize }

end

class PigeonNotifier < AbstractNotifier::Base
  self.driver = PigeonDelivery.new
end

class PostsPigeon < PigeonNotifier
  def published(post)
    notification(
      to: user.pigeon_nest_id,
      message: "coo-chi coo-chi coo"
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

就是这样🐦!

查看Active DeliveryAbstract Notifier存储库以获取更多技术信息。


阅读更多开发文章,请访问https://evilmartians.com/chronicles

鏂囩珷鏉yu簮锛�https://dev.to/evilmartians/crafting-user-notifications-in-rails-with-active-delivery-5cn6
PREV
铁轨上的危险:让机器人为您做一些代码审查!
NEXT
我们最喜欢的 JavaScript 单行代码