最近在整任意嵌套 layout 时想到的...
假设有个页面 page, 若干 layout. 因为模板引擎里编写 layout 是用 <%= yield %>
的方式把渲染内容放进去的,所以如果想要 layout1
包 layout2
包 page
, 代码就像是这样:
layout1.render do
layout2.render do
page.render
end
end
但问题是写 web 框架时不知道最后到底有多少个 layout... 所以最终就是需要一个函数,接受模板和任意多布局作为参数,然后把内容渲染出来,签名就像是这样 (page 可以看作一个特殊的 layout):
def render_all [page, layout2, layout1]
能简单想到两种渲染法 ...
一种是自外往内渲染:
def render_all layouts
outside, *inside = layouts.reverse.map do |e|
e.render { '<render:yield/>' }
end
inside.each do |r|
outside.gsub! '<render:yield/>', r
end
outside
end
准确点的可以基于 xml / xslt 做结构化替换,但和上面代码是差不多的。
另一种简单点,自内往外渲染 (sinatra 就是这么做的):
def render_all layouts
layouts.inject '' do |r, e|
e.render { r }
end
end
但是不管哪一种,渲染运算的顺序和客户端接受到内容的顺序是不同的:
于是你还是必须把所有内容都渲染完了才能开始发送结果,就很难实现边处理边发送了...
如果 render_all
可以能把渲染过程翻译成下面这样,至少能保持渲染序和接收序一致:
layout1.render do
layout2.render do
page.render
end
end
观察目标代码,可以发现... 如果把 render
当作函数,self 作为它的第一个参数,block 内容作为第二个参数,那么它其实是个递归函数,而且是把自己作为参数之一的坑爹递归函数... 递归终结的地方就是找到了它的不动点 -- page.render
不需要 block 也能返回结果。
回想 Y combinator 的实现和定义 fibonacci 的写法:https://gist.github.com/luikore/2300067 (详细一点的实现可以看)
然后下面的代码就出来了:
YRender = Y -> f, n, layouts {
if n == 0
layouts[0].render
else
layouts[n].render { f.call n - 1, layouts }
end
}
YRender.call layouts.size - 1, layouts
完整 prove of concept 在 https://gist.github.com/luikore/5792186
另外一个应用场景是串接中间件 -- 用 Fiber 做也可以 , 但是如果用方法去定义中间件:
def middleware
...
yield
...
end
filter :middleware
middleware
的串接就和模板的 render
串接变成同一个问题了,也能用 Y combinator 解决哦。