Gem Devise async send email

bajiudongfeng · April 09, 2017 · Last by bajiudongfeng replied at April 12, 2017 · 2029 hits

1. 解决方案

  • 在 user 这个 model 下添加如下代码
# Override Devise to send mails with async
def send_devise_notification(notification, *args)
  devise_mailer.send(notification, self, *args).deliver_later
end

默认的话使用 devise 内部内置的发送邮件的方法,也可以进行覆盖,自定义邮件发送。但是要注意修改devise.rb文件:

# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'

  • 后台进行邮件处理的可以选择 Resque,sidekiq 等,此处使用sidekiq 在 application.rb 文件中加入如下代码:

config.active_job.queue_adapter = :sidekiq

  • devise 默认的邮件发送使用的 redis 队列名称是 mailers,因此要在sidekiq.yml文件中加入此队列。

- [mailers, 3]

2. 运行结果

development.log 文件:

[ActiveJob] Enqueued ActionMailer::DeliveryJob (Job ID: 6a5e3f3f-bccf-4fc7-89e0-f16f63918aa0) to Sidekiq(mailers) with arguments: "Devise::Mailer", "reset_password_instructions", "deliver_now", #>, "CpfoqSDN3z6zvA9yiUsL", {}

sidekiq.log 文件:

2017-04-09T08:53:57.511Z 24351 TID-y44z8 ActionMailer::DeliveryJob JID-fede8f1837ccca19b5dbf262 INFO: start
2017-04-09T08:53:58.493Z 24351 TID-y44z8 ActionMailer::DeliveryJob JID-fede8f1837ccca19b5dbf262 INFO: done: 0.983 sec

3. 源码探究

  • 打印 send_devise_notification(notification, *args) 参数信息:

I, [2017-04-09T17:00:56.308408 #21573] INFO -- : notification: reset_password_instructions
I, [2017-04-09T17:00:56.308512 #21573] INFO -- : args: ["hmNJj3Gszwvswd2VjxQL", {}]
I, [2017-04-09T17:00:56.498526 #21573] INFO -- : devise_mailer: Devise::Mailer Class

因此可知 类: Devise::Mailer 方法:reset_password_instructions

  • 源码文件路径:devise/app/mailers/devise/mailer.rb
if defined?(ActionMailer)
  class Devise::Mailer < Devise.parent_mailer.constantize
    include Devise::Mailers::Helpers

    def confirmation_instructions(record, token, opts={})
      @token = token
      devise_mail(record, :confirmation_instructions, opts)
    end

    def reset_password_instructions(record, token, opts={})
      @token = token
      devise_mail(record, :reset_password_instructions, opts)
    end

    def unlock_instructions(record, token, opts={})
      @token = token
      devise_mail(record, :unlock_instructions, opts)
    end

    def email_changed(record, opts={})
      devise_mail(record, :email_changed, opts)
    end

    def password_change(record, opts={})
      devise_mail(record, :password_change, opts)
    end
  end
end

因此:devise_mailer会根据不同的参数调用不同的方法。

  • Devise.parent_mailer.constantize 解析:
# devise/lib/devise.rb
module Devise
  # The parent mailer all Devise mailers inherit from.
  # Defaults to ActionMailer::Base. This should be set early
  # in the initialization process and should be set to a string.
  mattr_accessor :parent_mailer
  @@parent_mailer = "ActionMailer::Base"
end

  • 发送邮件操作进一步解析

发送邮件的方法 devise_mail 定义在 module Devise::Mailers::Helpers中,这是发送邮件的具体实现。

# 发送邮件 record 就是User记录, action 是 :reset_password_instructions
def devise_mail(record, action, opts = {}, &block)
  initialize_from_record(record)
  mail headers_for(action, opts), &block
end

 # 一些初始化的工作
def initialize_from_record(record)
  @scope_name = Devise::Mapping.find_scope!(record)
  @resource   = instance_variable_set("@#{devise_mapping.name}", record)
end

# 设置一些头信息
def headers_for(action, opts)
  headers = {
    subject: subject_for(action),
    to: resource.email,
    from: mailer_sender(devise_mapping),
    reply_to: mailer_reply_to(devise_mapping),
    template_path: template_paths,
    template_name: action
  }.merge(opts)

  @email = headers[:to]
  headers
end

headers 的结果类似:

headers: {:subject=>"重置密码信息", :to=>"[email protected]", :from=>"[email protected]", :reply_to=>"[email protected]", :template_path=>["devise/mailer"], :template_name=>:reset_password_instructions}

其实关键就是

def devise_mail(record, action, opts = {}, &block)
  initialize_from_record(record)
  mail headers_for(action, opts), &block
end

最后调用的是 mail 方法,是 ActionMailer 的东西(Devise 啰嗦那么多就是不想和项目的 Mailer 代码绑定),然后提出了 send_devise_notification(notification, *args) 接口,所以核心逻辑就是帮 Devise 去用 ActiveJob 对 Mailer 的强化为发送邮件异步化...

@jasl 是啊,devise 把发送邮件相关的方法都实现好了,而要异步发送就需要加入后台队列。因此这个方法就出现了。

# Override Devise to send mails with async
def send_devise_notification(notification, *args)
  devise_mailer.send(notification, self, *args).deliver_later
end
You need to Sign in before reply, if you don't have an account, please Sign up first.