Rails 如何插入一个 Middleware 在指定位置?

ibachue · 2013年06月24日 · 最后由 zgm 回复于 2013年06月25日 · 3483 次阅读

在写一个 Gem,想把 Gem 中的 middleware 设置在::ActionDispatch::Static之后,本来觉得这样很容易做到,直到我发现 Rails 使用::ActionDispatch::Static这个 Middleware 是有条件的,那就是config.serve_static_assets要打开。如果config.serve_static_assets为 false 的话,那么毫无疑问就会出现No such middleware to insert after: "::ActionDispatch::Static"错误。这个错误还不是调用insert_after的时候立即出错的,否则倒是可以捕捉异常后做点 fallback 操作的。为了让 middleware 总是插入在我想要的位置上,我只能把代码写成这样:

class Railtie < Rails::Railtie
  initializer 'phpfaker.initialize_middleware' do |app|
    dependency =  if app.config.serve_static_assets
                    "::ActionDispatch::Static"
                  elsif not app.config.allow_concurrency
                    "::Rack::Lock"
                  else
                    "::Rack::Runtime"
                  end
    app.middleware.insert_after dependency, Middleware
  end
end if defined?(Rails)

毫无疑问这段代码非常不优雅,也不容易测试,而且事实上高度依赖了Rails::Application的实现,基本上是层层 fallback 直到::Rack::Runtime为止,因为最后这个 Middleware 的加载不依赖任何条件,所以总是能够成功。不过如果把这段代码拿去给一个 Engine 执行,那恐怕还是要出错的,因为 Engine 可能什么默认 Middleware 都没有,任何insert_after都是不行的。如果用户自定义了 Rails 的 Middleware,取消了部分 Middleware 的话,那同理,这段代码依然会发生错误。 事实上我这个 Middleware 对位置并不是特别敏感,只要排名靠前就行了,但是我反复查阅了 Rails 对于 Middleware 的实现,竟然没有任何允许我将 Middleware 插在最前面的操作,或是大致插入在某个位置的操作(当然我也完全可以自己实现一套,不过老是做这样的傻事不好啊。。)。又由于在启动阶段针对 MiddlewareStack 的操作实质上都是在操作 Proxy,没有办法直接获取 Build Stack 时 Middleware 的名单。因此我没法在运行时确定一个排名靠前的 Middleware 然后抱佛脚。所以。。额。。悲催了。。

方向错了... 不是看 rails 而是要看 rack

我没看仔细,请忽略...

#1 楼 @luikore 大神 说具体点。。

直接改 config.middleware 的数组行么?我看到 这里 有一个 insert 方法,有一个 index 参数。不知道这个行不行,没试过,不负责任猜测。

https://github.com/rails/rails/blob/master/railties/lib/rails/application/default_middleware_stack.rb#L12 这里看,靠前的 middleware 都是有条件的,直到Rack::Runtime是固定存在的,但是Rack::Runtime这个很多人会去掉,也不保险,所以如果可以insert_before "Rack::MethodOverride"是比较保险的,MethodOverride把它干掉,几乎 rails 就没法玩了,所以没啥人会去掉。

#3 楼 @zgm config.middleware得到是Rails::Configuration::MiddlewareStackProxy,不是真正最后的 stack,这里的方法有限,只有insert_before,insert_after,use等这些。

#6 楼 @kenshin54 insert_before 方法可以传入一个 index,不一定就是 middleware 名字,比如 Mytest::Application.config.middleware.insert_before 0, MyMiddleware。

#7 楼 @zgm 嗯 这个似乎确实可行

def assert_index(index, where)
  i = index.is_a?(Integer) ? index : middlewares.index(index)
  raise "No such middleware to insert #{where}: #{index.inspect}" unless i
  i
end

这里确实同时支持了直接传入位置和 Middleware

#7 楼 @zgm 嗯,有 2 个action_dispatch/middleware/stack.rb,还有一个是railties/lib/rails/configuration.rb,本来config.middleware得到是MiddlewareStackProxy,不是ActionDispatch::MiddlewareStack,他们都有 insert_before 方法,最终在 merge 的时候,MiddlewareStackProxy代理ActionDispatch::MiddlewareStack,额,以前还真没注意,在MiddlewareStackProxy也可以用 index,官方文档也没写。。。

#9 楼 @kenshin54 哎 Ruby 领域就这样的 功能要通过看源码去挖掘的。。我已经习惯这种做法了。。

#9 楼 @kenshin54 文档写的都是普通需求,高级需求要看源码甚至要 hack 了。 一般文档不会把所有的功能都写出来,因为有些隐形功能仅仅是程序员写的时候为了易扩展,根本就没打算给别人用。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号