flash方法的来源

跟踪一下view中的flash方法如何运行

<% if binding.trace_tree(html: true, tmp: ['rails', 'view_flash.html']){flash[:alert]} %>
  

<%= flash[:alert] %>

<% end %>


调用栈如下

view_flash.html


从源码来看,view的flash方法确实是共用controller的,这定义在ActionView::Helpers::ControllerHelper

module ActionView
  module Helpers
    module ControllerHelper
      attr_internal :controller, :request

      delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers,
               :flash, :action_name, :controller_name, :controller_path, :to => :controller


再检查下ActionView::Helpers::ControllerHelper如何mixin

$ action_view git:(master) grep 'ControllerHelper' -rn *
helpers/controller_helper.rb:7:    module ControllerHelper #:nodoc:
helpers.rb:13:    autoload :ControllerHelper
helpers.rb:46:    include ControllerHelper


可见Helper会include ControllerHelper,然后ActionView::Base再include Helpers

module ActionView
  class Base
    include Helpers, ::ERB::Util, Context


顺便看看哪些module/class有定义flash方法(这个倒不算很重要)

irb(main):002:0> ObjectSpace.find_all{|m| m.is_a? Module and m.instance_methods(false).include? :flash and m.name !~ /Test/}
=> [ActionView::Helpers::ControllerHelper, ActionDispatch::Flash::FlashNow, ActionDispatch::Flash::RequestMethods]


而Request能调flash



是因为Request有mixin Flash::RequestMethods

class Request
  prepend Flash::RequestMethods
end


flash的运作

再跟踪一下如何在controller中设置flash

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(name: params[:name])
    if user.try(:authenticate, params[:password])
      session[:user_id] = user.id
    else
      binding.trace_tree(html: true, tmp: ['rails', 'controller_flash.html']) do
        flash[:alert] = "Invalid user/password combination"
        flash[:notice] = "Invalid user/password combination"
      end
    end
    redirect_to articles_url
  end


可见,在一次请求中FlashHash只会生成一次,并缓存于env中



源码如下

class Flash
  KEY = 'action_dispatch.request.flash_hash'.freeze

  module RequestMethods

    def flash
      flash = flash_hash
      return flash if flash
      self.flash = Flash::FlashHash.from_session_value(session["flash"])
    end

    def flash=(flash)
      set_header Flash::KEY, flash
    end

    def flash_hash # :nodoc:
      get_header Flash::KEY
    end


再看看FlashHash的方法都会在什么时候被调用



可见ActionDispatch::Flash::FlashHash#to_session_value会在controller的dispatch被request的commit_flash调用

而dispatch如下,似乎所有响应都会commit_flash

class Metal < AbstractController::Base
  def dispatch(name, request, response) #:nodoc:
    set_request!(request)
    set_response!(response)
    process(name)
    request.commit_flash
    to_a
  end


而所谓commit_flash就是将flash塞到session中传回给client(假设整个session保存在cookie中)

def commit_flash
  session    = self.session || {}
  flash_hash = self.flash_hash

  if flash_hash && (flash_hash.present? || session.key?('flash'))
    session["flash"] = flash_hash.to_session_value
    self.flash = flash_hash.dup
  end

  if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
      session.key?('flash') && session['flash'].nil?
    session.delete('flash')
  end
end


观察to_session_value以及再回头看看from_session_value

class FlashHash
  include Enumerable

  def self.from_session_value(value)
    case value
    when FlashHash # Rails 3.1, 3.2
      flashes = value.instance_variable_get(:@flashes)
      if discard = value.instance_variable_get(:@used)
        flashes.except!(*discard)
      end
      new(flashes, flashes.keys)
    when Hash # Rails 4.0
      flashes = value['flashes']
      if discard = value['discard']
        flashes.except!(*discard)
      end
      new(flashes, flashes.keys)
    else
      new
    end
  end

  # Builds a hash containing the flashes to keep for the next request.
  # If there are none to keep, returns nil.
  def to_session_value
    flashes_to_keep = @flashes.except(*@discard)
    return nil if flashes_to_keep.empty?
    { 'discard' => [], 'flashes' => flashes_to_keep }
  end

  def initialize(flashes = {}, discard = []) #:nodoc:
    @discard = Set.new(stringify_array(discard))
    @flashes = flashes.stringify_keys
    @now     = nil
  end


其运作过程是,每次请求的首次调用flash方法时,会从session中抓取:flashes,然后生成FlashHash对象。

生成的方法是,在self.from_session_value中new,这时会给第二个参数传当前session中flash的keys,以示这些keys要discard掉

discard的时机是在塞回session时,对flash中这些key进行except

当然,如果取得的keys与本次想使用的key有重复,是不会删掉的:

class FlashHash
  def []=(k, v)
    k = k.to_s
    @discard.delete k
    @flashes[k] = v
  end


关于keep

keep很简单,就是在@discard排除掉想保留的key,让它在下次请求仍然可用

def keep(k = nil)
  k = k.to_s if k
  @discard.subtract Array(k || keys)
  k ? self[k] : self
end


关于flash.now

根据刚才的分析,在同一次请求中多次调用flash,用的都是同一个FlashHash,它缓存在env中,那么rails guide中对flash.now的介绍似乎就没有必要了

class ClientsController < ApplicationController
  def create
    @client = Client.new(params[:client])
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end


但在源码的comment来看,其实是rails guide说得不够完整

# Sets a flash that will not be available to the next action, only to the current.
#
#     flash.now[:message] = "Hello current action"
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign ([]=).
# When you need to pass an object to the current action, you use now, and your object will
# vanish when the current action is done.


now的与非now的区别在于not be available to the next action

为了达到这个目的,now方法会用FlashNow来包装FlashHash,是任何[]=操作都会同时把key记录为discard,这样在FlashHash#to_session_value时这些key/value就不会在去到session中,并由下次请求的的from_session_value获取到

(当然,在view中还是直接用flash来获取)

class FlashHash
  def now
    @now ||= FlashNow.new(self)
  end

  def discard(k = nil)
    k = k.to_s if k
    @discard.merge Array(k || keys)
    k ? self[k] : self
  end
end

class FlashNow
  def []=(k, v)
    k = k.to_s
    @flash[k] = v
    @flash.discard(k)
    v
  end
end


所以render会搭配使用flash.now,而redirect搭配使用flash