TABLE OF CONTENTS

文档

Mojolicious::Guides::Growing - 成长成更大型的项目

概述

本文档介绍中, 我们来讲怎么从 Mojolicious::Lite 开始做的原型项目, 成长为一个结构良好的的全或功能 Mojolicious 应用.

本文档更新到版本 6.05

概念

所有 Mojolicious 的开发者都需要知道.

Model View Controller

MVC 是一种现代的软件体系结构的模式, 它起源于 Smalltalk-80 的图形界面编程, 用于分离应用程序逻辑, 表示和输入.

         +------------+    +-------+    +------+
Input -> | Controller | -> | Model | -> | View | -> Output
         +------------+    +-------+    +------+

目前基本只需要给现在有程序小量的修改就可以转到 controller 这种模式上来. 目前基本每个 Web 的框架都是基于 MVC 的结构, 包括 Mojolicious.

            +----------------+     +-------+
Request  -> |                | <-> | Model |
            |                |     +-------+
            |   Controller   |
            |                |     +-------+
Response <- |                | <-> | View  |
            +----------------+     +-------+

在上面这个结构中 controller 接收到用户的请求后, 传送这些数据给 model 处理完数据, 然后通过 view 转化为实际的响应. 但要注意, 这种模式只是一个指导方针, 最重要的目标是有更干净和易于维护的代码.

REpresentational State Transfer ( REST )

RESR 是一种 Web 软件架构的风格, 近来常常用于给 HTTP 做协议. 在 REST 中, 你可以打开 http://mojolicio.us/foo 这个 URL 在你的浏览器中, 你相当于告诉你的 Web 服务来进行一个 HTML 的表示这个地址的资源.

+--------+                                +--------+
|        | -> http://mojolicio.us/foo  -> |        |
| Client |                                | Server |
|        | <- <html>Mojo rocks!</html> <- |        |
+--------+                                +--------+

这里的基本思想是, 所有的资源都是有一个唯一的 URL 来进行查寻, 并且每个资源都可以有不同的表示方式, 如 HTML, RSS 和 JSON . 让界面从数据层分离出来只需要注意和用户会议的状态进行互交.

+---------+                        +------------+
|         | ->    PUT /foo      -> |            |
|         | ->    Hello world!  -> |            |
|         |                        |            |
|         | <-    201 CREATED   <- |            |
|         |                        |            |
|         | ->    GET /foo      -> |            |
| Browser |                        | Web Server |
|         | <-    200 OK        <- |            |
|         | <-    Hello world!  <- |            |
|         |                        |            |
|         | ->    DELETE /foo   -> |            |
|         |                        |            |
|         | <-    200 OK        <- |            |
+---------+                        +------------+

虽然 HTTP 的方法象 PUT, GETDELETE 并不是 REST 的一部分, 但他们用来管理资源非常不错.

会话 (Sessions)

Web 服务上的 HTTP 本来就被设计成一个无状态的协议, 所以我们并不知道是否是以前的请求, 这使得让用户友好登录到系统变得非常棘手. Sessions 就是用来解决这个问题, 使得网络应用中跨多个 HTTP 请求后还能保留状态信息.

GET /login?user=sri&pass=s3cret HTTP/1.1
Host: mojolicio.us

HTTP/1.1 200 OK
Set-Cookie: sessionid=987654321
Content-Length: 10
Hello sri.

GET /protected HTTP/1.1
Host: mojolicio.us
Cookie: $Version=1; sessionid=987654321

HTTP/1.1 200 OK
Set-Cookie: sessionid=987654321
Content-Length: 16
Hello again sri.

传统的上, 所有的 session 会话数据存储在服务器端的, 只有 Session 中会话的 ID 以 Cookie 的形式在 Web 服务器和浏览器之间交换.

HTTP/1.1 200 OK
Set-Cookie: session=base64(hmac-sha1(json($session)))

Mojolicious 中对 session 进行了进一步的处理, 使用 HMAC-SHA1 签署 cookie, 这更兼容 REST 的理念并降低一些其它的要求.

测试驱动开发(Test Driven Development)

TDD, 是一种不同于传统软件开发流程的新型的开发方法. 它要求在编写某个功能的代码之前先编写测试代码, 然后只编写使测试通过的功能代码, 通过测试来推动整个开发的进行. 这有助于编写简洁可用和高质量的代码, 并加速开发过程. 有许多优点, 如始终具有良好的测试覆盖率和代码的可测试性设计, 这将反过来往往防止未来的变化影响旧的代码的功能. 大多的 Mojolicious 使用 TDD 开发;

原型

Mojolicious 和其他 Web 框架的主要区别之一是, 它还包括有一个 Mojolicious::Lite, 这是一个微型的 Web 框架, 为快速实现原型优化过的.

差异

你有一些很好的主意, 有很酷的想法, 你想尽可能快地尝试实现它, 这也是为什么 Mojolicious::Lite 写的应用程序不需要超过一个单个文件的原因.

myapp.pl   # Templates and even static files can be inlined

全功能的 Mojolicious 应用程序更加接近象一个 CPAN 的发布包一样, 有着良好结构, 以最大限度的提高可维护性.

myapp                      # 应用程序的目录 
|- script                  # 脚本的目录 
|  +- myapp                # 应用程序的脚本  
|- lib                     # Library 的目录 
|  |- MyApp.pm             # 应用程序的类 
|     +- Controller        # 控制器名字空间
|        +- Example.pm     # 控制器的类 
|- t                       # 测试目录 
|  +- basic.t              # Random test
|- log                     # 日志目录 
|  +- development.log      # 开发模式的日志
|- public                  # 静态文件的目录 (象 css, js 之类)
|  +- index.html           # 静态 HTML 文件
+- templates               # 模板目录
   |- layouts              # 模板目录和 layout
   |  +- default.html.ep   # Layout 模板 
   +- example              # "Example"  controller 的模板目录
      +- welcome.html.ep   # "welcome" 动作的模板

这二种应用类型的骨架, 可以使用 generate 来自动生成, 我们只需要使用命令 Mojolicious::Command::generate::lite_appMojolicious::Command::generate::app.

$ mojo generate lite_app
$ mojo generate app

基础

我们启动我们新的应用程序需要使用一个可执行的文件

$ mkdir myapp
$ cd myapp
$ touch myapp.pl
$ chmod 744 myapp.pl

这是我们 login manager 的样例程序的一个基础.

#!/usr/bin/env perl
use Mojolicious::Lite;

get '/' => sub {
  my $c = shift;
  $c->render(text => 'Hello world!');
};

app->start;

内置的 Web 开发服务器有个非常好的地方, 就是会自动的在你的程序变化更新后重新加载.

$ morbo myapp.pl
Server available at http://127.0.0.1:3000.

当你保存你的修改后, 会在你下一次刷新浏览器的时候就会自动生效了.

A birds-eye view

我们可以通过浏览器发送一个 HTTP 请求, 就象下面这样

GET / HTTP/1.1
Host: localhost:3000

一旦请求在 Web 服务器上通过事件循环被接收到后, 接下来会通过 Mojolicious 进行几个简单的处理.

1. 如果请求的只是一个静态文件就行, 就输出这个文件.
2. 尝试找一个能满足要求的路由信息.
3. 调度这个请求到这个路由上指定的一个或者多个方法和动作.
4. 处理请求, 可能产生的渲染器的响应. 
5. 将控制器内返回到 Web 服务器, 如果没有响应生成, 就会通过事件循环来等一个非阻塞操作.

如果你的应用进入了路由中第二步, 和第四步, 就会响应一些内容, 就象下面这个一样, 并在你的浏览器上显示.

HTTP/1.1 200 OK
Content-Length: 12
Hello world!

Model

Mojolicious 中, 我们认为 Web 应用是现有前端的简单业务逻辑, 这意味着 Mojolicious 设计完全是和 model 层无关的, 你可以使用任意的 Perl 模块来替换.

$ mkdir -p lib/MyApp/Model
$ touch lib/MyApp/Model/Users.pm
$ chmod 644 lib/MyApp/Model/Users.pm

我们的登录管理, 将只使用一个普通的原有 Perl 模块抽象出, 来匹配用户名和密码这个工作相关的逻辑. 下面的这个 MyApp::Model::Users 只是可以选择任何你所需要的东西. 只是用于分离.

package MyApp::Model::Users;

use strict;
use warnings;

my $USERS = {
  joel      => 'las3rs',
  marcus    => 'lulz',
  sebastian => 'secr3t'
};

sub new { bless {}, shift }

sub check {
  my ($self, $user, $pass) = @_;

  # Success
  return 1 if $USERS->{$user} && $USERS->{$user} eq $pass;

  # Fail
  return undef;
}

1;

如果你想实现自己的 model 的动作和模板, 你可以使用 "helper" in Mojolicious 的功能来注册你的方法.

#!/usr/bin/env perl
use Mojolicious::Lite;

use lib 'lib';
use MyApp::Model::Users;
  
# Helper to lazy initialize and store our model object
helper users => sub { state $users = MyApp::Model::Users->new };

# /?user=sri&pass=secr3t
any '/' => sub {
  my $c = shift;

  # Query parameters
  my $user = $c->param('user') || '';
  my $pass = $c->param('pass') || '';

  # Check password
  return $c->render(text => "Welcome $user.")
    if $c->users->check($user, $pass);

  # Failed
  $c->render(text => 'Wrong username or password.');
};

app->start;

这个 "param" in Mojolicious::Controller 的方法会取得请求过来的参数, POST 的参数, 文件上传送的内容和 Route 的占位符取得的内容.和其它.

测试 (Testing)

Mojolicious 中我们希望大家能都认真的采用测试驱动开发, 并努力推动它.

$ mkdir t
$ touch t/login.t
$ chmod 644 t/login.t

Test::Mojo 是一个脚本化的 HTTP user agent , 设计是专门用于测试的, 它有非常多有趣的功能如基于 CSS 选择器的 Mojo::DOM.

use Test::More;
use Test::Mojo;

# Include application
use FindBin;
require "$FindBin::Bin/../myapp.pl";

# Allow 302 redirect responses
my $t = Test::Mojo->new;
$t->ua->max_redirects(1);

# Test if the HTML login form exists
$t->get_ok('/')
  ->status_is(200)
  ->element_exists('form input[name="user"]')
  ->element_exists('form input[name="pass"]')
  ->element_exists('form input[type="submit"]');

# Test login with valid credentials
$t->post_form_ok('/' => {user => 'sebastian', pass => 'secr3t'})
  ->status_is(200)->text_like('html body' => qr/Welcome sri/);

# Test accessing a protected page
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);

# Test if HTML login form shows up again after logout
$t->get_ok('/logout')->status_is(200)
  ->element_exists('form input[name="user"]')
  ->element_exists('form input[name="pass"]')
  ->element_exists('form input[type="submit"]');

done_testing();

从现在开始, 在你的应用程序中, 你随时可以检查的你的程序, 通过 Mojolicious::Command::test 运行这些单元测试.

$ ./myapp.pl test
$ ./myapp.pl test t/login.t
$ ./myapp.pl test -v t/login.t

快速的测试 GET 请求可以执行这个 Mojolicious::Command::get 命令.

$ ./myapp.pl get /
Wrong username or password.

$ ./myapp.pl get -v '/?user=sri&pass=secr3t'
GET /?user=sri&pass=secr3t HTTP/1.1
User-Agent: Mojolicious (Perl)
Connection: keep-alive
Accept-Encoding: gzip
Content-Length: 0
Host: localhost:59472

HTTP/1.1 200 OK
Connection: keep-alive
Date: Sun, 18 Jul 2010 13:09:58 GMT
Server: Mojolicious (Perl)
Content-Length: 20
Content-Type: text/plain

Welcome sebastian.

状态保持 (State keeping)

Sessions 在 Mojolicious 中非常简单, 直接可以通过 "session" in Mojolicious::Controller 方法来使用, 不过建议你使用更加安全的设置, 通过 "secrets" in Mojolicious 来设置个密码短语用于加密 session.

app->secrets(['Mojolicious rocks']);

这个密码短语是使用的 HMAC-SHA1 算法来签名你的 cookie ,并可以随时改变让所有现有会话无效.

$c->session(user => 'sebastian');
my $user = $c->session('user');

默认的 sessions 的有效时间为一个小时, 如果你想自己控制, 就使用 expiration 的参数, 来设置 session 方法中的值就可以了.

$c->session(expiration => 3600);

可以通过设置 expires 来让会话过期, 如果要删除就使用你个过去的时间就好了.

$c->session(expires => 1);

对于只让下一个请求可见的信息, 象下面这个 302 的重定向, 你可以使用 "flash" in Mojolicious::Controller 的方法.

$c->flash(message => 'Everything is fine.');
$c->redirect_to('goodbye');

你需要知道的就是所有的会话的数据都被 Mojo::JSON 实例化并使用 HMAC-SHA1 签名 cookie 来存储的. 通常浏览器对这个数据有 4096 个字节的限制 (4KB) 需要你注意.

最终的原型

上面那个最终的原型 myapp.pl 会是下面这个样子.

#!/usr/bin/env perl
use Mojolicious::Lite;

use lib 'lib';
use MyApp::Model::Users;

# 创建一个 cookies 安全用的签名
app->secrets(['Mojolicious rocks']);

helper users => sub { state $users = MyApp::Model::Users->new };

# 主要的登陆动作
any '/' => sub {
  my $c = shift;

  # 用于得到查询的参数
  my $user = $c->param('user') || '';
  my $pass = $c->param('pass') || '';

  # 检查用户密码, 如果不对就输出 "index.html.ep" 的网页
  return $c->render unless $c->users->check($user, $pass);

  # 存储用户名到会话的 cookie 中
  $c->session(user => $user);

  # 存储一个友好的信息给下一个网页来显示
  $c->flash(message => 'Thanks for logging in.');

  # 通过 302 重定向到需要认证保护的网页
  $c->redirect_to('protected');
} => 'index';

# 这个 group 需要确保用户是登陆的.
group {
  under sub {
    my $c = shift;

    # 如果用户没有登陆就 302 重定向到主页
    return 1 if $c->session('user');
    $c->redirect_to('index');
    return undef;
  };

  # 显示被认证保护的网页 "protected.html.ep"
  get '/protected';
};


# Logout action
get '/logout' => sub {
  my $c = shift;

  # Expire and in turn clear session automatically
  $c->session(expires => 1);

  # Redirect to main page with a 302 response
  $c->redirect_to('index');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
%= form_for index => begin
  % if (param 'user') {
    <b>Wrong name or password, please try again.</b><br>
  % }
  Name:<br>
  %= text_field 'user'
  <br>Password:<br>
  %= password_field 'pass'
  <br>
  %= submit_button 'Login'
% end

@@ protected.html.ep
% layout 'default';
% if (my $msg = flash 'message') {
  <b><%= $msg %></b><br>
% }
Welcome <%= session 'user' %>.<br>
%= link_to Logout => 'logout'

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title>Login Manager</title></head>
  <body><%= content %></body>
</html>

全部的原生的 helpers 可以看 Mojolicious::Plugin::DefaultHelpersMojolicious::Plugin::TagHelpers.

完整结构的应用

Mojolicious 非常的灵活, 所以在实际变成完整应用时有很多的变化, 这给你很好的概括了各种可能性.

导出内部模板 (Inflating templates)

所有在 DATA 的部分的内容的静态文件和模板, 可以通过这个方法自动的生成并放在 templatespublic 的目录, 并保持原来的结构.

$ ./myapp.pl inflate

在项目目录下的模板和静态文件有比起 DATA 部分的有着更高的优先级, 使用 inflate 之后, 可以让你更加方便的定制你的应用.

简单完整应用程序的类

这些东西是整个 Mojolicious 应用的心脏, 这些内容在 Web 服务启动的过程中被实例化.

$ touch lib/MyApp.pm
$ chmod 644 lib/MyApp.pm

我们开始从原来的 myapp.pl 单文件中提取出所有 action 的方法的实现代码, 放到指定的目录中, 变成 Mojolicious::Routes 中混合成 route . 我们并不需要改变实际的功能代码.

 package MyApp;
 use Mojo::Base 'Mojolicious';

 use MyApp::Model::Users;

 sub startup {
   my $self = shift;

   $self->secrets(['Mojolicious rocks']);
   $self->helper(users => sub { state $users = MyApp::Model::Users->new });

   my $r = $self->routes;

   $r->any('/' => sub {
     my $c = shift;

     my $user = $c->param('user') || '';
     my $pass = $c->param('pass') || '';
     return $c->render unless $c->users->check($user, $pass);

     $c->session(user => $user);
     $c->flash(message => 'Thanks for logging in.');
     $c->redirect_to('protected');
   } => 'index');

   my $logged_in = $r->under(sub {
     my $c = shift;
     return 1 if $c->session('user');
     $c->redirect_to('index');
     return undef;
   });
   $logged_in->get('/protected');

   $r->get('/logout' => sub {
     my $c = shift;
     $c->session(expires => 1);
     $c->redirect_to('index');
   });
 }

 1;

这个 startup 的方法调用后整个应用程序被创建并实例化, 由于全功能的 Mojolicious 可以使用嵌套的路径选择器, 所以没必要使用 group 块.

简单的应用启动的脚本

这个 myapp.pl 本身可以变成一个简单的应用程序来让你在次运行所有的程序.

#!/usr/bin/env perl

use strict;
use warnings;

use lib 'lib';
use Mojolicious::Commands;

# Start commands for application
Mojolicious::Commands->start_app('MyApp');

控制器类 (Controller)

这个控制器是用于实现详细的功能的类.

上面那种给路由和实现的动作混合起来的很方便, 但要最大限度地提高可维护性, 需要从路由中给动作的方法代码分离出来, 这是非常有意义的, 所以我们需要分离操作代码和它的路由配置, 下面我们来达成这个目标.

$ mkdir lib/MyApp/Controller
$ touch lib/MyApp/Controller/Login.pm
$ chmod 644 lib/MyApp/Controller/Login.pm

当然, 实际的功能代码还是不需要修改. 我们只需要改变 $self 变成更加好理解的 $c 来表示这是控制器.

package MyApp::Controller::Login;
use Mojo::Base 'Mojolicious::Controller';

sub index {
  my $c = shift;

  my $user = $c->param('user') || '';
  my $pass = $c->param('pass') || '';
  return $c->render unless $c->users->check($user, $pass);

  $c->session(user => $user);
  $c->flash(message => 'Thanks for logging in.');
  $c->redirect_to('protected');
}

sub logged_in {
  my $c = shift;
  return 1 if $c->session('user');
  $c->redirect_to('index');
  return undef;
}

sub logout {
  my $c = shift;
  $c->session(expires => 1);
  $c->redirect_to('index');
}

1;

全部的 Mojolicious::Controller 的控制器就是普通的 Perl 的类.

应个应用的主类 (Application)

应用类, 是用于指示这个应用本身的全局信息的类.

这个 lib/MyApp.pm 的应用的类, 可以现在可以减少到只有模式和路由的信息.

package MyApp;
use Mojo::Base 'Mojolicious';

use MyApp::Model::Users;

sub startup {
  my $c = shift;

  $c->secrets(['Mojolicious rocks']);
  $c->helper(users => sub { state $users = MyUsers->new });

  my $r = $c->routes;
  $r->any('/')->to('login#index')->name('index');

  my $logged_in = $r->under->to('login#logged_in');
  $logged_in->get('/protected')->to('login#protected');

  $r->get('/logout')->to('login#logout');
}

1;

这个 Mojolicious::Routes 可以有非常多的路由的变化, 选出你最喜欢的方式都行.

模板

我们给模板绑定到控制器, 所以需要给他们移动到相应的目录.

$ mkdir templates/login
$ mv templates/index.html.ep templates/login/index.html.ep
$ mv templates/protected.html.ep templates/login/protected.html.ep

Script

最终的 myapp.pl 需要移到 script 的目录中并重命名为 my_app, 这是 CPAN 的标准.

$ mkdir script
$ mv myapp.pl script/my_app

简单的测试

标准的 Mojolicious 应用程序更容易测试, 所以 t/login.t 可以简化.

use Test::More;
use Test::Mojo;

# Load application class
my $t = Test::Mojo->new('MyApp');
$t->ua->max_redirects(1);

$t->get_ok('/')
  ->status_is(200)
  ->element_exists('form input[name="user"]')
  ->element_exists('form input[name="pass"]')
  ->element_exists('form input[type="submit"]');

$t->post_form_ok('/' => {user => 'sebastian', pass => 'secr3t'})
  ->status_is(200)->text_like('html body' => qr/Welcome sebastian/);

$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);

$t->get_ok('/logout')->status_is(200)
  ->element_exists('form input[name="user"]')
  ->element_exists('form input[name="pass"]')
  ->element_exists('form input[type="submit"]');

done_testing();

最后我们的目录看起来会是这样:

myapp
|- script
|  +- my_app
|- lib
|  |- MyApp.pm
|  +- MyApp
|     |- Controller
|     |  +- Login.pm
|     +- Model
|        +- Users.pm
|- t
|  +- login.t
+- templates
   |- layouts
   |  +- default.html.ep
   +- login
      |- index.html.ep
      +- protected.html.ep

测试驱动开发需要多一点时间, 但很值得!

MORE

你现在可以看看 Mojolicious::Guides . 现在也可以看看 Mojolicious wiki, 这有很多文档和不同作者的例子.

SUPPORT

If you have any questions the documentation might not yet answer, don't hesitate to ask on the mailing-list or the official IRC channel #mojo on irc.perl.org.