Ruby Y combinator 的应用: 嵌套布局保序渲染...

luikore · 2013年06月16日 · 2926 次阅读

最近在整任意嵌套 layout 时想到的...

假设有个页面 page, 若干 layout. 因为模板引擎里编写 layout 是用 <%= yield %> 的方式把渲染内容放进去的,所以如果想要 layout1layout2page, 代码就像是这样:

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

但是不管哪一种,渲染运算的顺序和客户端接受到内容的顺序是不同的:

  • 自外往内渲染顺序是:layout1_begin, layout1_end, layout2_begin, layout2_end, page
  • 自内往外渲染顺序是:page, layout2_begin, layout2_end, layout1_begin, layout1_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 解决哦。

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