Wednesday, July 11, 2007

Catalyst 介绍








名字



Catalyst 指南 - Catalyst 介绍






描述


本文简单的介绍了为何要用 Catalyst 以及如何使用它。文中解释了 Catalyst 的工作原理并通过一个简单应用的快速建立来加以验证。




Catalyst 是什么?


Catalyst 是一个优雅的 Web 应用框架,极为灵活又特别简单。它类似于 Ruby on Rails、Java 的 Spring 和 Maypole(原来就基于 Maypole 建立)。





MVC


Catalyst 遵循模型-视图-控制(MVC)设计模式,它擅长将内容处理、表示和流程控制方面的工作区分开来交给独立的模块来做。这种区分允许你为某一方面的问题修改代码而不影响解决其它问题的代码。这样 Catalyst 提升了原有的解决 Web 应用方面的问题的模块的重用程度。


下面就是 M、V、C 分别解决的问题,每个方面都有著名的 Perl 模块的可用。



如果你不熟悉 MVC 和设计模式,你得查看一下这方面的原始资料:Gamma、Helm、Johson、Vlissides 写的 Design Patterns,也叫 Gang of Four 或 GoF。你也可以 google 一下。有很多很多的 Web 应用框架都是基于 MVC 的,如前面提到的那些。




灵活性


Catalyst 比起其他的框架来说灵活很多。我们会慢慢的解释,很快就会看到那些你喜爱的 Perl 模块在 Catalyst 里面的应用。






简洁性


Catalyst 最棒的地方在于它如此简单的实现了这样的灵活性。





快速起步



快速起步


下面就是安装 Catalyst 并创建简单的可运行的应用的过程。这里用到了上面说到的辅助脚本。




Install



$ perl -MCPAN -e 'install Bundle::Catalyst'



Setup




$ catalyst.pl MyApp
# output omitted
$ cd MyApp
$ script/myapp_create.pl controller Library::Login



Run



$ script/myapp_server.pl

现在可以用你喜欢的浏览器或者代理程序来访问下面的地址来检查 Catalyst 的运行状况:



http://localhost:3000/


http://localhost:3000/library/login/


太简单了!




工作原理


我们来看看 Catalyst 如何工作,下面就开始仔细查看 Catalyst 组件和应用的其它部分。




应用类


应用类



在模板、视图和控制组件以外还有一个代表你的应用的类。在这个类里面可以配置应用、加载插件、定义应用级的动作、扩展 Catalyst。



package MyApp;


use strict;
use Catalyst qw/-Debug/;


MyApp->config(
name => 'My Application',
root => '/home/joeuser/myapp/root',



# You can put anything else you want in here:
my_configuration_variable => 'something',
);


sub default : Private {
my ( $self, $context ) = @_;
$context->response->output('Catalyst rockz!');
}


1;


对大多数应用来说,Catalyst 只要求你定义两个配置参数:



  • name

  • 应用的名字。



  • root

  • 模板文件、图片或者其他静态数据所在的路径。



$context->config->{$param_name}.



然而,你还可以定义传给插件或者其他东西的参数。可以在应用的任何位置用 $context->config->{$param_name} 来存取它们。




语境


Catalyst 自动把 Context 对象“赐”给你的应用类,这样整个应用里都能访问。Context 不但用来和 Catalyst 打交道,也能把应用的 Components 联系起来。比如想要在 Template Toolkit 模板里面使用 Context,只要这样:




<h1>Welcome to [% c.config.name %]!</h1>

好像前面的从 URL 派发动作的例子所展示的,Context 总是方法的第二个参数。它前面的参数是 Component 对象的引用或类名。我们以前为了清晰叫它 $context,但是绝大多数 Catalyst 开发人员叫它 $c



sub hello : Global {
my ( $self, $c ) = @_;
$c->res->output('Hello World!');
}


The Context contains several important objects:


Context 包含了几个重要的对象:



  • Catalyst::Request


  • $c->request
    $c->req # alias

    请求对象包含了各种请求相关信息,例如查询参数、cookie、上传内容、头信息等等。




    $c->req->params->{foo};
    $c->req->cookies->{sessionid};
    $c->req->headers->content_type;
    $c->req->base;

  • Catalyst::Response



  • $c->response
    $c->res # alias

    响应对象有点类似请求对象,但是只包含响应相关的信息。



    $c->res->output('Hello World');
    $c->res->status(404);
    $c->res->redirect('http://oook.de');


  • Catalyst::Config


  • $c->config


    $c->config->root;
    $c->config->name;

  • Catalyst::Log



  • $c->log


    $c->log->debug('Something happened');
    $c->log->info('Something you should know');

  • Stash



  • $c->stash


    $c->stash->{foo} = 'bar';


最后的那个 stash 是特别设立出来在应用的各组件之间共享数据的哈希表。例如我们可以这样回应 hello 动作:



sub hello : Global {
my ( $self, $c ) = @_;
$c->stash->{message} = 'Hello World!';
$c->forward('show_message');
}



sub show_message : Private {
my ( $self, $c ) = @_;
$c->res->output( $c->stash->{message} );
}

注意 stash 只能在每次请求周期内传递数据,对新的请求它会被清空。如果你需要更加持久的数据,请使用 session。




动作(行为)


Catalyst 控制器是由动作来定义的。动作是一个带有属性的子程序。你已经在本文里面看到了几个动作的例子。URL(例如 http://localhost.3000/foo/bar )可分为两部分:基础部分(这个例子里面是 http://localhost.3000/ )和路径部分(foo/bar)。请注意 hostname[:port] 后面的正斜杠是属于基础部分而非路径部分的。



Catalyst 支持几类动作:



  • Literal

  • 字面动作

    sub bar : Path('foo/bar') { }

    仅仅匹配 http://localhost:3000/foo/bar




  • 正则匹配动作(Regex)


  • sub bar : Regex('^item(\d+)/order(\d+)$') { }

    匹配任何符合动作模式的 URL 如 http://localhost:3000/item23/order42 。正则表达式周围的 '' 符号不是必须的,但是 perltidy 喜欢它。:)



    正则表达式是全局的,也就是说不是相对于调用它的名字空间的,因此除非你在正则表达式里面完整的说明,MyApp::Controller::Catalog::Order::Process 里面的 bar 方法不会匹配于 barCatalogOrder 或者 Process 这样的正则表达式。



    如果你用了小括号来捕捉 URL 里面的值(在上面的例子里面是 23 和 42),这些值就可以通过 $c->req->snippets 数组来存取。如果你想在 URL 的末尾带上参数,你得用正则匹配。参见下面的 URL 参数处理



  • 顶级动作


  • package MyApp;
    sub foo : Global { }


    匹配 http://localhost:3000/foo 。函数名字直接匹配在 URL 的基础部分后面。




  • 名字空间为前缀的动作


  • package MyApp::C::My::Controller;
    sub foo : Local { }

    匹配于 http://localhost:3000/my/controller/foo


    这个动作类型匹配的 URL 必须带有组件的类名(包名)相应的前缀。首先从类名中除去对 Catalyst 有意义的前一部分(这个例子里面是 MyApp::C),然后用 / 来代替 ::,再把名字转为小写的。参考 Components 关于 Catalyst 组件的类名的预定义部分的说明。




  • 私有动作


  • sub foo : Private { }

    不匹配 URL,也不可以被相应的正则匹配的 URL 的请求来调用。私有的动作只能在 Catalyst 应用内部通过 forward 方法来调用:



    $c->forward('foo');


    参考 流程控制 里面关于 forward 的解释。注意按照它所说的,从组件外部调用一个动作的时候必须使用绝对路径,因此倘若从外部调用 MyApp::Controller::Catalog::Order::Process 控制里面私有的 bar 方法,就得用 $c->forward('/catalog/order/process/bar') 来指明路径。




注意:看了这些例子后,你可能在想给正则和路径动作定义名字的目的何在。实际上任何公共的动作同时也是私有的,因此在组件间统一的调用方法是 forward




内建私有动作


为了响应某些特殊的应用需要,Catalyst 会自动调用你的应用类里面的某些内建私有动作。



  • default : Private

  • 这个动作在没有其他的动作匹配的时候调用。可以用来显示一个主应用的通用首页,或者某个控制器的错误页面。




  • begin : Private

  • 在请求的开始被调用,在任何匹配的动作之前被调用。



  • end : Private

  • 在请求的最后(所有的动作之后)被调用。





内建控制动作/反应链




Package MyApp::C::Foo;
sub begin : Private { }
sub default : Private { }


还可以在控制里面定义内建私有动作。它会覆盖高抽象级别的控制(或是应用类)的动作。换句话说在单个请求周期内,所有前面提到的三类内建私有动作,都只有一个能运行。好比 MyApp::C::Catalog::begin 存在的话,它就会在 catalog 名空间中代替 MyApp::begin 执行。而它也又会被 MyApp::C::Catalog::Order::begin 覆盖。



在普通的内建动作以外,还可以用 auto 来实现级连动作。这种 auto 动作在 begin 之后、其他的动作之前被调用。与其他内建动作不同之处在于 auto 动作不会彼此覆盖,而是从应用类向细节的类依次调用。这和普通内建动作相互覆盖的顺序相反



这是用来验证各种内建动作调用顺序的例子:



以一个 /foo/foo 请求来说



MyApp::begin
MyApp::auto
MyApp::C::Foo::default # in the absence of MyApp::C::Foo::Foo
MyApp::end


以一个 /foo/bar/foo 请求来说




MyApp::C::Foo::Bar::begin
MyApp::auto
MyApp::C::Foo::auto
MyApp::C::Foo::Bar::auto
MyApp::C::Foo::Bar::default # for MyApp::C::Foo::Bar::foo
MyApp::C::Foo::Bar::end



对于 auto 动作来说还有一个特点就是可以用返回 0 的方式终止级连调用。如果 auto 动作返回 0 的话,所有剩下的动作(除 end 以外)都将被跳过。因此对于上面的请求,如果第一个 auto 返回假的话,级连调用看来像这样:




以第一个 auto 返回值为假 /foo/bar/foo 的请求来说



MyApp::C::Foo::Bar::begin
MyApp::auto
MyApp::C::Foo::Bar::end




这个例子在鉴权的动作来说很有用:你可以用应用类里面 auto 动作(总是最先调用)来判定权限,如果鉴权失败的话就返回 0 来跳过 URL 请求中剩下的动作。


注意: 换个角度看,auto 必须返回真值才能继续处理!你还可以在自动级连动作中调用 die,那样请求会直接跳到结束阶段,不会进一步处理。




URL 路径处理



可以把 URL 路径的一部分作为可变参数来传递。为此得在定义动作键(下面 sub foo : Regex 中的 foo 就是动作键)的正则匹配关键字的时候两边要用 ^ 和 $ 站岗,还得用正斜杠来分隔参数。例如要处理 /foo/$bar/$baz(其中 $bar$baz 可变):




sub foo : Regex('^foo$') { my ($self, $context, $bar, $baz) = @_; }

但是如果同时还给 /foo/boo/foo/boo/hoo 定义了动作呢?



sub boo : Path('foo/boo') { .. }
sub hoo : Path('foo/boo/hoo') { .. }

Catalyst 会按照细节到抽象的顺序来匹配动作:




/foo/boo/hoo
/foo/boo
/foo # might be /foo/bar/baz but won't be /foo/boo/hoo

这样 Catalyst 永远不会错误派发前面两个 URL 到 ^foo$ 动作。




参数处理


在 URL 查询串里面传递的参数用 Catalyst::Request 类的方法来处理。它的 param 方法和 CGI.pm 里面的 param 方法有相同的功能。




# http://localhost:3000/catalog/view/?category=hardware&page=3
my $category = $c->req->param('category');
my $current_page = $c->req->param('page') || 1;


# multiple values for single parameter name
my @values = $c->req->param('scrolling_list');



# DFV requires a CGI.pm-like input hash
my $results = Data::FormValidator->check($c->req->params, \%dfv_profile);



流程控制


forward 方法来控制应用流程,它按照传递给它的动作键来执行。这可能是同一个或者不同的 Catalyst 控制器中的动作,或者是一个类名(有可能带一个方法名)。在 forward 结束后,控制会返回到发起 forward 的方法。



看起来 forward 很类似方法调用。主要的区别在于它用 eval 来包装调用过程以提供意外处理。它会主动用($c$context)来发送语境对象,还可以对每个调用计时(打开调试模式以后显示在 log 里面)。



sub hello : Global {
my ( $self, $c ) = @_;
$c->stash->{message} = 'Hello World!';
$c->forward('check_message'); # $c is automatically included
}



sub check_message : Private {
my ( $self, $c ) = @_;
return unless $c->stash->{message};
$c->forward('show_message');
}


sub show_message : Private {
my ( $self, $c ) = @_;
$c->res->output( $c->stash->{message} );
}

forward 不会发起一个新的请求,因此请求对象($c->req)不会变化。这是 forward 和 redirect 的主要区别。



可以用匿名数组给 forward 传递新的参数。这样 $c->req->args 会在 forward 调用期间被改变。在返回以后 $c->req->args 会恢复原始的值。




sub hello : Global {
my ( $self, $c ) = @_;
$c->stash->{message} = 'Hello World!';
$c->forward('check_message',[qw/test1/]);
# now $c->req->args is back to what it was before
}


sub check_message : Private {
my ( $self, $c ) = @_;
my $first_argument = $c->req->args[0]; # now = 'test1'
# do something...
}

在此可以看到,如果只想调用同一个控制里面的方法就可以单单用方法名字作为参数。如果想要调用不同控制(或主应用)里面的动作就得用绝对路径。




$c->forward('/my/controller/action');
$c->forward('/default'); # calls default in main application

下面是用类名和方法名来调用的例子。



sub hello : Global {
my ( $self, $c ) = @_;
$c->forward(qw/MyApp::M::Hello say_hello/);
}


sub bye : Global {
my ( $self, $c ) = @_;
$c->forward('MyApp::M::Hello'); # no method: will try 'process'
}



package MyApp::M::Hello;


sub say_hello {
my ( $self, $c ) = @_;
$c->res->output('Hello World!');
}


sub process {
my ( $self, $c ) = @_;
$c->res->output('Goodbye World!');
}

注意 forward 返回到调用它的动作并在动作完成之后接着处理。如果你忽略方法名字 Catalyst 会自动调用 process() 方法。





组件



Catalyst 有个非同寻常的灵活的组件系统。你可以自由的定义任意数量的 ModelsViewsControllers



所有的组件都必须继承于 Catalyst::Base,它提供简单的类结构和通用类方法如 confignew(构造器)。




package MyApp::C::Catalog;


use strict;
use base 'Catalyst::Base';


__PACKAGE__->config( foo => 'bar' );


1;


你不必 use 或注册模型、视图、控制器。在你调用 setup 的时候 Catalyst 会自动发现他们并创建实例。你所需要的只是把它们放在按照组件类型区分的目录里面。显然你可以用很简短的别名来标记它们。



  • MyApp/Model/

  • MyApp/M/

  • MyApp/View/


  • MyApp/V/

  • MyApp/Controller/

  • MyApp/C/




Views


视图



为要展示如何定义视图,我们要用代表 Template Toolkit 的基类 Catalyst::View::TT。我们需要做的只是继承这个类:



package MyApp::V::TT;


use strict;
use base 'Catalyst::View::TT';


1;


(还可以用辅助脚本来自动生成这个:



script/myapp_create.pl view TT TT

这里第一个 TT 告诉脚本创建一个 Template Toolkit 视图,第二个 TT 告诉脚本它将被命名为 TT。)


这就产生了一个 process() 方法,因此可以用 $c->forward('MyApp::V::TT') 来套用模板。基类已经提供了 process() 方法,因此不用再说 $c->forward(qw/MyApp::V::TT process/)了。




sub hello : Global {
my ( $self, $c ) = @_;
$c->stash->{template} = 'hello.tt';
}


sub end : Private {
my ( $self, $c ) = @_;
$c->forward('MyApp::V::TT');
}

通常总是在请求的末尾来套用模板,因此使用全局的 end 动作来完成是最好的。


还得记住把模板放在 $c->config->{root} 所指向的路径下面,否则就等着看满眼的 debug 信息吧。 :D





Models


模型


为了展示模型的定义,我们还是使用现存的基类。这次是 Catalyst::Model::CDBI 代表的 Class::DBI


但是我们得先有个数据库。



-- myapp.sql
CREATE TABLE foo (
id INTEGER PRIMARY KEY,
data TEXT
);



CREATE TABLE bar (
id INTEGER PRIMARY KEY,
foo INTEGER REFERENCES foo,
data TEXT
);


INSERT INTO foo (data) VALUES ('TEST!');


% sqlite /tmp/myapp.db < myapp.sql

现在来给这个数据库创建一个 CDBI 组件。




package MyApp::M::CDBI;


use strict;
use base 'Catalyst::Model::CDBI';


__PACKAGE__->config(
dsn => 'dbi:SQLite:/tmp/myapp.db',
relationships => 1
);



1;

Catalyst 会自动载入表结构和关系。用 stash 来传递数据给模板。



package MyApp;


use strict;
use Catalyst '-Debug';


__PACKAGE__->config(
name => 'My Application',
root => '/home/joeuser/myapp/root'
);



__PACKAGE__->setup;


sub end : Private {
my ( $self, $c ) = @_;
$c->stash->{template} ||= 'index.tt';
$c->forward('MyApp::V::TT');
}


sub view : Global {
my ( $self, $c, $id ) = @_;
$c->stash->{item} = MyApp::M::CDBI::Foo->retrieve($id);
}



1;


The id is [% item.data %]



控制器


多个控制器分工可以很好的将应用分割成不同的逻辑功能域。



package MyApp::C::Login;



sign-in : Local { }
new-password : Local { }
sign-out : Local { }


package MyApp::C::Catalog;


sub view : Local { }
sub list : Local { }


package MyApp::C::Cart;



sub add : Local { }
sub update : Local { }
sub order : Local { }



Testing


测试


Catalyst 有一个内建的 http server 用于测试!(当然可以用更强大的服务器如 Apache/mod_perl 来满足生产环境的需要。)


在命令行来启动程序 ......



script/myapp_server.pl


然后用浏览器访问 http://localhost:3000/ 来查看输出。


还可以完全从命令行完成:



script/myapp_test.pl http://localhost/

好好享受吧!







支持


支持


IRC:



请加入 irc.perl.org 的 #catalyst 频道。

邮件列表:



http://lists.rawmode.org/mailman/listinfo/catalyst
http://lists.rawmode.org/mailman/listinfo/catalyst-dev






作者


Sebastian Riedel, sri@oook.de
David Naughton, naughton@umn.edu
Marcus Ramberg, mramberg@cpan.org
Jesse Sheidlower, jester@panix.com

Danijel Milicevic, me@danijel.de
翻译:joejiang, joejiang799 at gmail.com
校审:cnhackTNT,cnhackTNT at perlchina.org






版权声明


这份文档属于自由软件, 你可以在 Perl 许可的条款下对其进行分发或者修改。

No comments:

Post a Comment