The Flash
flash方法的来源
跟踪一下view中的flash方法如何运行
<% if binding.trace_tree(html: true, tmp: ['rails', 'view_flash.html']){flash[:alert]} %>
<%= flash[:alert] %>
<% end %>
调用栈如下
从源码来看,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