名字
Catalyst 指南 - Catalyst 介绍
描述
本文简单的介绍了为何要用 Catalyst 以及如何使用它。文中解释了 Catalyst 的工作原理并通过一个简单应用的快速建立来加以验证。
Catalyst 是什么?
Catalyst 是一个优雅的 Web 应用框架,极为灵活又特别简单。它类似于 Ruby on Rails、Java 的 Spring 和 Maypole(原来就基于 Maypole 建立)。
MVC
Catalyst 遵循模型-视图-控制(MVC)设计模式,它擅长将内容处理、表示和流程控制方面的工作区分开来交给独立的模块来做。这种区分允许你为某一方面的问题修改代码而不影响解决其它问题的代码。这样 Catalyst 提升了原有的解决 Web 应用方面的问题的模块的重用程度。
下面就是 M、V、C 分别解决的问题,每个方面都有著名的 Perl 模块的可用。
- Model
- View
- Controller
模型
存取和修改数据内容。Class::DBI、Plucene、Net::LDAP 等
视图
向用户展示内容。Template Toolkit、Mason、HTML::Template 等
控制器
控制整个请求阶段、检查参数、派发动作、流程控制。也就是 Catalyst!
如果你不熟悉 MVC 和设计模式,你得查看一下这方面的原始资料:Gamma、Helm、Johson、Vlissides 写的 Design Patterns,也叫 Gang of Four 或 GoF。你也可以 google 一下。有很多很多的 Web 应用框架都是基于 MVC 的,如前面提到的那些。
灵活性
Catalyst 比起其他的框架来说灵活很多。我们会慢慢的解释,很快就会看到那些你喜爱的 Perl 模块在 Catalyst 里面的应用。
- 多模型、视图和控制
- 可重用组件
- 无限制 URL-to-Action 调度
- 对 CGI、mod_perl、Apache::Request 的支持
为了要建立一个 Catalyst 应用,你得用名为 组件(Components) 的模块来处理各种问题。一般这样的代码会非常简单,只是调用 MVC 下面列出的 Perl 模块。Catalyst 用很灵活的方式来使用这些组件。在一个应用里面可以使用任意数量的模板、视图和控制模块。想要操作多个数据库并读写 LDAP 数据么?没问题。想要用 Template Toolkit 和 PDF::Template 来展现同样的模型么?很简单。
Catalyst 不只是促进了对已有的 Perl 模块的重用,还允许你在多个 Catalyst 应用之间重用 Catalyst 组件。
Catalyst 允许你以 URL 方式对应用程序的 Action 进行调度,甚至允许用正则表达式匹配!不像其他大多数框架,它不依赖于 mod_rewrite 或者 URL 里面的类和方法名。
Catalyst 允许你注册动作并触发。例如:
sub hello : Global {
my ( $self, $context ) = @_;
$context->response->output('Hello World!');
}
这样 http://localhost:3000/hello 就会打印出“Hello World!”。
使用 Catalyst::Engine::Apache 或者 Catalyst::Engine::CGI。
简洁性
Catalyst 最棒的地方在于它如此简单的实现了这样的灵活性。
- 搭积木似的接口
- 自动发现组件
- 常用模块对应的预定义组件
- 内建测试框架
- Helper Scripts
组件之间可以无缝搭配。例如 Catalyst 自动给每个组件提供一个 Context 对象(语境)。通过语境可以存取请求对象,在组件之间共享数据,并控制应用的流程。建立一个 Catalyst 应用的过程感觉上很像搭积木,而且每样东西都能派上用场。
自动发现组件
不用主动的 use
所有的组件。Catalyst 会自动发现并加载它们。
例如对应于 Class::DBI 的 Catalyst::Model::CDBI 或者对应于 Template Toolkit 的 Catalyst::View::TT。你甚至可以用 Catalyst::Model::CDBI::CRUD 来立刻实现一个数据库的 Web 界面。
Catalyst 自带一个内建的轻量级 http 服务器和测试框架,这样在命令行测试应用就很容易。
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 的运行状况:
太简单了!
工作原理
我们来看看 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 只要求你定义两个配置参数:
$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
- Catalyst::Response
- Catalyst::Config
- Catalyst::Log
- Stash
$c->request
$c->req # alias
请求对象包含了各种请求相关信息,例如查询参数、cookie、上传内容、头信息等等。
$c->req->params->{foo};
$c->req->cookies->{sessionid};
$c->req->headers->content_type;
$c->req->base;
$c->response
$c->res # alias
响应对象有点类似请求对象,但是只包含响应相关的信息。
$c->res->output('Hello World');
$c->res->status(404);
$c->res->redirect('http://oook.de');
$c->config
$c->config->root;
$c->config->name;
$c->log
$c->log->debug('Something happened');
$c->log->info('Something you should know');
$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
- 正则匹配动作(Regex)
- 顶级动作
- 名字空间为前缀的动作
- 私有动作
字面动作
sub bar : Path('foo/bar') { }
仅仅匹配 http://localhost:3000/foo/bar 。
sub bar : Regex('^item(\d+)/order(\d+)$') { }
匹配任何符合动作模式的 URL 如 http://localhost:3000/item23/order42 。正则表达式周围的 '' 符号不是必须的,但是 perltidy 喜欢它。:)
正则表达式是全局的,也就是说不是相对于调用它的名字空间的,因此除非你在正则表达式里面完整的说明,MyApp::Controller::Catalog::Order::Process
里面的 bar
方法不会匹配于 bar
、Catalog
、Order
或者 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 有个非同寻常的灵活的组件系统。你可以自由的定义任意数量的 Models、Views、Controllers。
所有的组件都必须继承于 Catalyst::Base,它提供简单的类结构和通用类方法如 config
和 new
(构造器)。
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