Flash,中文可翻译成闪现(LOL玩家应该很熟悉),在Ruby On Rails中我们对它最直观的体验就是在控制器的动作中操作 flash 这个变量(还是方法?)。官方对 flash 的描述如下:

The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions.

Just remember: They'll be gone by the time the next action has been performed.

简单概括一下它的功能,主要用于在控制器之间传输Ruby的“原生类型的数据”。在当前控制器的动作中存储一个值,这个值能够在下一次动作里面通过对应的方式来获取。然而这个值将会在下个动作完成的时候被销毁掉。考虑下面的示例代码:

class PostsController < ApplicationController
  def a
    flash.notice = 'From Action a'
    redirect_to action: :b
  end

  def b
    flash.notice #=> "From Action a"
    redirect_to action: :c
  end

  def c
    flash.notice #=> nil
  end
end

篇幅有限,对应的视图代码我就不写了。虽然大环境下“分离先行”的今天 flash 越发淡出众人的视野,不过坚持用ActiveAdmin写后台的我跟它打交道还是蛮多的,今天就想简单聊聊它的内部实现。

数据结构与初始化

抛开中间件的原理先不谈(我其实也不太懂),先把灯光聚焦在 flash

> flash.class
=> ActionDispatch::Flash::FlashHash

它是一个 ActionDispatch::Flash::FlashHash 的实例,初始化方法如下:

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

  def initialize_copy(other)
    if other.now_is_loaded?
      @now = other.now.dup
      @now.flash = self
    end
    super
  end
end

initialize 主要用于实例的初始化,这个大家都比较熟悉了,从这里面可以看出 FlashHashActionDispatch::Flash::FlashHash 从外部采集哈希表 flashes ,以及数组 discard ,并分别设置对应的实例变量 @flashes@discard 。还初始化了一个实例变量 @now

其实这个 @now 放不放这里都无所谓,它可以在其他方法里面再初始化,放在这个方法里面我觉得好处就在于: 代码阅览者可以一目了然,从初始化方法就可以了解当前的类的实例会囊括到哪些实例变量,是个人比较喜欢的编码风格。

也就是说构造一个 flash 就如此简单:

> ActionDispatch::Flash::FlashHash.new
=> #<ActionDispatch::Flash::FlashHash:0x00007fb109c6aab8 @discard=#<Set: {}>, @flashes={}, @now=nil>

> ActionDispatch::Flash::FlashHash.new({name: 'lanzhiheng', age: 28}, [:age])
=> #<ActionDispatch::Flash::FlashHash:0x00007fb109bbb3b0 @discard=#<Set: {"age"}>, @flashes={"name"=>"lanzhiheng", "age"=>28}, @now=nil>

initialize_copy 方法则会在对象拷贝的时候调用。根据一些网上的文章。这个方法会在对象拷贝的时候调用。比方说调用 Object#dup 的时候,伪代码大概如下

class Object
  def dup
    dup = self.class.allocate
    dup.copy_instance_variables(self)
    dup.initialize_dup(self)
    dup
  end

  def initialize_dup(other)
    initialize_copy(other)
  end
end

这时候 initialize_dup 接收的参数other就是 Object#dup 方法的接受者(调用它的对象本身)。拷贝的时候,它会通方法 ActionDispatch::Flash::FlashHash#now_is_loaded? 来检测 @now 对象是否已经加载,如果已经加载,则在拷贝出来的对象中也设置这个值,它是原来 now 的一个副本

@now = other.now.dup
@now.flash = self

OK,关于 flash 的数据结构还有初始化就介绍到这里,接下来看看它是怎么管理数据的。

管理

从一个动作设置的 flash 字段能够在下一个动作被读取到,并在下一个动作完成的时候销毁,这个销毁操作主要依靠 @discard 这个数组。回到最开始 abc 的例子,从 b 中查看 flash 会得到如下的值:

> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007fa73ba9b868 @discard=#<Set: {"notice"}>, @flashes={"notice"=>"From Action a"}, @now=nil>

ba 的下一个动作,所以 b 能够读取到 flash.noticea 中所设置的值,并且会在 @discard 中记录即将要销毁的键 "notice" , ActionDispatch::Flash::FlashHash 里面提供了一个名为 sweep 的方法:

class FlashHash
  def sweep #:nodoc:
    @discard.each { |k| @flashes.delete k }
    @discard.replace @flashes.keys
  end
end

如果有需要其实我们也可以手动调用这个方法来抹除那些在 @discard 数组中记录过的数据,就像

> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007fa73ba9b868 @discard=#<Set: {"notice"}>, @flashes={"notice"=>"From Action a"}, @now=nil>
> flash.sweep
=> #<Set: {}>
> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007fa73ba9b868 @discard=#<Set: {}>, @flashes={}, @now=nil>

另外如果想要维护 @discard 这个数组中的数据,其实可以通过 ActionDispatch::Flash::FlashHash::discard 以及 ActionDispatch::Flash::FlashHash::keep 这一组方法,源码如下

class FlashHash
  def discard(k = nil)
    k = k.to_s if k
    @discard.merge Array(k || keys) # 这里有个奇怪的keys
    k ? self[k] : self
  end

  def keep(k = nil)
    k = k.to_s if k
    @discard.subtract Array(k || keys) # 这里有个奇怪的keys
    k ? self[k] : self
  end
end

其中的 keys 则是 @flashes 这个哈希表里面的键

class FlashHash
  def keys
    @flashes.keys
  end
end

如果给 FlashHash#discard 方法提供特定键作为参数的时候,它就会把这个键合并进数组当中,如果没有提供任何参数,那么它就会把 @flashes 哈希表的所有键弄进数组中,在销毁操作的时候一并处理掉。 FlashHash#keep 的能力也差不多,只不过干的是相反的事情,把指定的键从数组中删除,或者把哈希表的所有键都从数组中删除。

那么稍微调整一下 abc 的例子其实完全可以做到把 flash 里的数据,维持到再下一个动作 c

class PostsController < ApplicationController
  def a
    flash.notice = 'From Action a'
    redirect_to action: :b
  end

  def b
    flash.notice #=> "From Action a"
    flash.keep(:notice)
    redirect_to action: :c
  end

  def c
    flash.notice #=> "From Action a"
  end
end

哈哈,不过一般不会这样做吧。

真闪现

其实严格来说 flash 还不算是真的闪现,因为毕竟它所保存的东西的生命周期跨越了两个控制器动作。有没有可能设置一组数据,他们的生命周期只延续到当前控制器动作的完结?我们往往会通过在动作中设置实例变量来达到这个目的。

class PostsController < ApplicationController
  def d
    @hello = "Hello World"
  end
end
<!-- posts/d.html.erb -->

<div>
  <%= @hello %>
</div>

@hello 实例变量并不会在控制器动作之间共享,只有当前控制器动作以及对应的视图能够访问。这其实用 flash 也能实现。

class PostsController < ApplicationController
  def d
    flash.now.notice = 'Hello World'
  end
end
<!-- posts/d.html.erb -->

<div>
  <%= flash.now.notice %>
</div>

这里的 now 是什么玩意?

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

调用 now 方法会设置 flash 的实例变量 @now ,它是 ActionDispatch::Flash::FlashNow 的一个实例。代码也比较简单:

class FlashNow
  attr_accessor :flash
  def initialize(flash)
    @flash = flash
  end

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

  #...
end

初始化的时候就是设置了实例变量 @flash ,方法 FlashNow#[]= 会调用 @flash 本身的 []= 方法,来设置对应的值,只是在这之后它还会吧对应的键给 discard 掉,于是当前请求结束之后,这个键值对就会销毁掉。

再者,又因为访问器的存在,所以能够直接获取对应的实例变量 @flash 。于是乎在动作 d 里面我们其实可以

> flash.now.flash.now.flash.now.flash == flash
=> true

持久化

flash 的数据一般都需要在两个控制器之间共享(从一个控制器设置的值能够在下一个控制器中被获取到)。这种时候就需要一个中转站作为两个控制器动作之间数据共享的桥梁,从代码上看 flash 是通过 session 来做的中转的。

工具方法

module RequestMethods
  # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
  # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
  # to put a new one.
  def flash
    flash = flash_hash
    return flash if flash
    self.flash = Flash::FlashHash.from_session_value(session["flash"])
  end

  # ...
end

首先从 RequestMethods#flash_hash 中去获取 flash ,如果不存在则从 session 中去获取,并调用内部的 RequestMethods#flash= 方法,把对应的值设置进来。它们的源码范例如下

module RequestMethods
  KEY = "action_dispatch.request.flash_hash"

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

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

作用一目了然,一个是根据 KEY 从头部获取 flash 。另一个则是把 flash 的值设置到头部,而从最开始的 RequestMethods#flash 方法可以看出,这个值是从 session 中获取的。我理解就是两个控制器动作之间的信息传递就是通过 session 来做中转,而有些时候我们需要从视图中去读取 flash 的相关数据,这种时候如果已经把相关的值设置到头部了,就可以从头部去获取。这个模块会被导入到 request 这个对象上,所以我们还可以这样去获取 flash

> request.flash
=> #<ActionDispatch::Flash::FlashHash:0x00007f9c05eebe68> ....
> flash == request.flash
=> true

存储

最后看看它的存储方式,如果 flash_hash 获取不到对应的值,那么它就会尝试从 session 中去获取。这种情况应该会发生在一个新的动作开始的时候,它需要从 session 中去读取 flash 相关的信息,以知道上一个动作有什么要传递过来。而 flash 的存储模式如下

class FlashHash
  # ...
  def to_session_value #:nodoc:
    flashes_to_keep = @flashes.except(*@discard)
    return nil if flashes_to_keep.empty?
    { "discard" => [], "flashes" => flashes_to_keep }
  end
end

可见,在存入 session 之前它会先根据 @discard 中的内容对 @flashes 进行一次清理。把剩余的值存到 session 中,如果这个时候哈希表已经空了,那么就返回 nil 值。而读取的方式也很简单

class FlashHash
  def self.from_session_value(value) #:nodoc:
    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
end

老版本的Rails我们应该都不用管(第一次接触Rails已经是5.x+了),我们常用的版本应该就是走 Hash 那条分支,这里就有点怪了,数据保存到 session 的时候已经经历过一次 flashes 的清理,这里是否有必要再清理一次?接下来就是利用得到的 flashes 哈希表,以及 discard 数组来初始化一个 ActionDispatch::Flash::FlashHash 对象。另外,如果 session 中获取的值为 nil 的时候,就直接不带参数地去调用 new 方法。

读到这里,也很好理解了为什么 ActionDispatch::Flash::FlashNow 的实例只在当前动作范围内有用,无法传到到下一个动作。因为在初始化的时候,它除了设置 @flashes 中的键值对,还把对应的键追加到了 @discard 数组中,这样在持久化之前调用的 FlashHash#to_session_value 就会把对应的东西给清理掉。下一个动作就获取不到了。而常规情况去设置 flash 中的数据,并不会不会把对应的键追加到 @discard 数组中,如下

> flash.clear
=> {}
> flash.now.notice = 'I am notice'
=> "I am notice"
> flash[:error] = 'I am error'
=> "I am error"
> flash
=> #<ActionDispatch::Flash::FlashHash:0x00007ff4f7c5d638
 @discard=#<Set: {"notice"}>,
 @flashes={"notice"=>"I am notice", "error"=>"I am error"},
 @now=
  #<ActionDispatch::Flash::FlashNow:0x00007ff4f7c5cf30
   @flash=#<ActionDispatch::Flash::FlashHash:0x00007ff4f7c5d638 ...>>>

这个时候 @discard 数组里面就只有 "notice" 而没有 "error" 。

尾声

以上就是对Rails中经常会用到的那个 flash 所做的源码分析。笔者平时都用ActiveAdmin来做后台管理的开发,所以跟它打交道会稍微多一些,就找空闲时间稍微窥探了一下源码。并不能算特别全面,有错误之处,还望指正。