Dancer 框架自带有一个 Simple 模版,不过推荐使用 Template 模块作为替代品。不过从性能上来说,TT2 比之前博客里陆续介绍过的 HTML::TemplateText::MicroTemplate 都要差。而这方面最好的,就是 Text::XSlate 模块了。今天尝试将一个 Dancer 应用迁移到 Text::XSlate 上。踩进两个坑,特此记录。

关于语法什么的,可以看 POD ,扶凯 有翻译的 中文版POD 。足以十分钟入门。就不多说了。

  • 第一个坑:session 的处理

website 少不了 session 的运用。在 template 里使用 [% session.username %] 可以很方便的控制显示面板还是登陆啊什么的。

不过切换成 XSlate 后(即 <: $session.username :>),请求会 crash 掉,报错大意是: $session 没有 username 这个 method

XSlate 提供了 dump 语法糖,让我们可以直接使用 <: $session | dump :> 检查问题。这时候发现显示如下:

    $VAR1 = { blessed( { id => '2131232131', username => 'user1' } ), Dancer::Session::YAML };

尝试使用 <: $session.id :> ,发现可以正常输出 2131232131 。

进去看 Dancer::Session 的代码,原来在 Dancer::Session::Abstract 里,有这么一行:

    __PACKAGE__->attributes('id');

说实话不太理解这行的用法,不过不妨碍我们用简单办法解决问题…… 在我们的应用中给 Dancer::Session::YAML 定义一个叫 username 的 method 就可以骗过去了:

    package DancerApp;
    use Dancer ':syntax';
    use Dancer::Session::YAML;
    sub Dancer::Session::YAML::username {
        return session('username');
    };
    use Dancer::Plugin::Auth::Extensible;
    get '/' => sub :RequireLogin { template 'index' };
    ...;
    true;

2013 年 03 月 25 日更新

今天莫莫也换成 Xslate 模板,顺带告诉我这里一个更通用和优雅的修改方式:

    package DancerApp;
    use Dancer ':syntax';
    use Dancer::Session::Abstract;
    use Dancer::Plugin::Auth::Extensible;
    Dancer::Session::Abstract->attributes( qw(username) );
    get '/' => sub :RequireLogin { template 'index' };
    ...;
    true;

这样可以在各种 Session 引擎下通用了。

更新完毕

  • 第二个坑:flashmessage 的处理

这是一个外加模块,叫做 Dancer::Plugin::FlashMessage 。用它配合模版的 layout 功能,可以很方便的给应用提供全局的消息通知。使用方法如下:

首先在模块里加载 flash 变量:

    package DancerApp::First;
    use Dancer ':syntax';
    use Dancer::Plugin::FlashMessage;
    use Dancer::Plugin::Auth::Extensible;
    get '/first/:name' => sub :RequireRole('MAN') {
        flash message => 'Hello! You are the first man here.';
        template 'first', { name => param('name') };
    };
    true;

然后在模版里判断显示:

    [% IF flash.message %]
      <div class="alert alert-success">
        [% flash.message %]
      </div>
    [% END %]

同样,在修改成 XSlate 后,模版是这样:

    : if $flash.message {
      <div class="alert alert-success">
        <: flash.message :>
      </div>
    : }

结果发现页面上的 div 一直保持,而且显示着 CODE(0x39a5c30) 这样的字样。同样使用 dump 语法糖,看到 $flash 其实是 { message => sub {"DUMMY"} }

这个就有趣了,居然是个代码段~~于是翻源码来看:

    hook before_template => sub {
        shift->{$token_name} = {
            map { my $key = $_; my $value;
                ( $key, sub { defined $value and return $value;
                    my $flash = session($session_hash_key) || {};
                    $value = delete $flash->{$key};
                    session $session_hash_key, $flash;
                    return $value;
                } );
            } ( keys %{session($session_hash_key) || {} })
        };
    };

map 里面,确实是一个 $key => sub {}

这个时候我切换两个 template 做了个测试。在里面那个匿名 sub 里写了一行 die;。结果。XSlate “正常”运行过去,在页面上显示前面说过的 CODE();而在 Template 模版下,500 了。看 console 的日志,发现 die 这个动作不是在 before_template 阶段发生的。而是在随后的 render 阶段,Dancer::Template::Abstract 里才挂了。

所以,最终,两个坑归结起来并成了一个问题:模版系统是支持 coderef 还是支持 object 的问题。就在我写着这句话的同时,IRC 上还为 Dancer::Plugin::FlashMessage 的新实现而争论不休。xdg 童鞋已经在我提问的一个小时内快速的搞出来一个把 flash “object 化”的 patch,而 bigpresh 童鞋坚定的认为应该把 delete 操作放在 hook after_template 里完成。原作者 dams 则”相信”更多的模版是支持 coderef 不支持 object 的。

不过我觉得,其实改动最小的办法,就是别用 map 这么高档的语法。拆成两段处理,确保传递给 template_render 的是字符串即可:

    hook before_template => sub {
        my %hash;
        my $flash = session($session_hash_key) || {};
        for ( keys %{$flash} ) {
            $hash{$_} = delete $flash->{$_};
        };
        session $session_hash_key, $flash;
        shift->{$token_name} = \%hash;
    };

最后的最后,就在我测试完我的改动版本在两种模版下都可以运行的时候,dams 已经决定先同时保持 coderef 和 object 的写法并提供 setting 配置。然后慢慢搜集各种模版系统做覆盖测试。

20 日增

最后最后的最后,在 github 上搜到两个用 dancer 和 xslate 写的 repo。他们都采用了在应用 app 里自定义 hook before_template ,把 session('username')flash('message') 两个变量传递给 $token 哈希的办法。