使用 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
代码库中可能还有几十个这样的地方。祝您维护和测试代码顺利!
我们如何重构这段代码?也许我们需要另一层抽象?
这就是Active Delivery——我为解决这个难题而编写的一篇新文章。
Active Delivery 是一个为所有类型的通知提供入口点的框架:邮件程序、推送通知,任何您想要的。
它可以帮助您按以下方式重写上面的代码:
def notify_user(user)
MyDelivery.with(user: user).notify(:some_action)
end
甚至更多——您现在可以优雅地测试它:
# my_something_spec.rb
expect { subject }.to have_delivered_to(MyDelivery, :some_action).
with(user: user)
它是如何工作的?
在最简单的情况下,递送仅仅是邮件上的包装:
# 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
我们依靠约定优于配置来推断相应的邮件程序类。
好的。我们刚刚把邮件包好。怎么办?其他投递方式怎么办?
我们先来看一下框架的架构:
请注意,这里有一个内部层——线路。每条线路都是传递和实际通知渠道(例如邮件程序)之间的连接器。
Active Delivery 提供了一个 API 来添加自定义交付线路——这样您就可以实现几乎任何类型的通知!
为了使其变得更加简单,我们构建了另一个微框架——Abstract Notifier。
这是一个非常抽象的框架:它所做的就是提供一个类似 Action Mailer 的 API 来描述通知器类,纯 Ruby 抽象,对“如何发送通知”一无所知。
为什么要采用类似 Action Mailer 的界面?首先,它是一个熟悉且简洁的 API。而且我喜欢它的参数化类功能(我们在 Active Delivery 中大量使用了此功能)。
要“教”抽象通知器如何发送通知,您必须实现一个驱动程序(任何可调用对象)。
例如,我们使用Twilio Notify进行推送通知,我们的驱动程序ApplicationDelivery
和ApplicationNotifier
类如下所示:
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
现在让我们定义我们的传递、邮寄和通知类:
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
最后,这就是我们触发通知的方式:
PostsDelivery.with(user: user).notify(:published, post)
如果需要更多通知渠道怎么办?我们可以添加另一行通知程序到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
就是这样🐦!
查看Active Delivery和Abstract Notifier存储库以获取更多技术信息。
阅读更多开发文章,请访问https://evilmartians.com/chronicles!
鏂囩珷鏉yu簮锛�https://dev.to/evilmartians/crafting-user-notifications-in-rails-with-active-delivery-5cn6