TABLE OF CONTENTS

文档

Mojolicious::Guides::Cookbook - Cookbook

概述

许多有趣的烹饪 Mojolicious 的食谱.

本文档更新到 6.0.

部署

我们可以让 MojoliciousMojolicious::Lite 的应用运行在不同的平台上, 注意时实 web 特性是基于 Mojo::IOLoop 的事件循环, 因此, 需要一个内置的 Web 服务器才能够使用它们来充分发挥其潜力.

内置的 Web 服务器

Mojolicious 内部包含了一个非常方便的非阻塞的 I/O HTTP 和 WebSocket 的 Mojo::Server::Daemon 的服务器. 这个常用于在开发中, 也可以用它来创建更加先进的 web 服务器.

但是对于中小型的应用来讲, 这个也足够快了.

$ ./script/myapp daemon
Server available at http://127.0.0.1:3000.

它可以使用 Mojolicious::Command::daemon 中所有可用的命令, 其中有许多的配置选项, 并且它本身的单进程架构的可以很好的工作在已知的所有平台.

$ ./script/myapp daemon -h
...List of available options...

另一个巨大的好处是, 它默认支持 TLS 和 WebSocket. 出于测试的目的, 我们内置了一个开发证书, 所以它可以直接工作, 你只需要使用 "listen" in Mojo::Server::Daemon 指定.

$ ./script/myapp daemon -l https://[::]:3000
  Server available at https://[::]:3000.

在 Unix 架构的平台上, 你可以通过 Mojolicious::Command::prefork 的命令来创建多进程的程序, 好处是可以使用多 CPU 核心和内存的写时 copy.

$ ./script/myapp prefork
  Server available at http://127.0.0.1:3000.

由于所有内置的 Web 服务器是基于 Mojo::IOLoop 事件循环, 所以它们最好是非阻塞的操作. 但是, 如果你的应用程序需要执行很多阻塞操作, 可以通过增加工作进程数量和降低每个 worker 的并发连接 ( 通常低至 1 ) 来提高性能.

$ ./script/myapp prefork -m production -w 10 -c 1
  Server available at http://127.0.0.1:3000.

你的应用在启动的时候就会预装入的管理进程, 这时并不会运行事件程序, 所以每当一个新的 worker 进程被 fork 出来你可以运行 "next_tick" in Mojo::IOLoop 来让事件循环开始.

use Mojolicious::Lite;

Mojo::IOLoop->next_tick(sub {
  app->log->info("Worker $$ star...ALL GLORY TO THE HYPNOTOAD!");
});

get '/' => {text => 'Hello Wor...ALL GLORY TO THE HYPNOTOAD!'};

app->start;

Morbo

如果你之前读过 Mojolicious::Guides::Tutorial 这个指南, 我想你已经知道 Mojo::Server::Morbo 这个东西了.

Mojo::Server::Morbo
+- Mojo::Server::Daemon

它是基于 Mojo::Server::Daemon 的 web 服务器 fork 出来并在你的代码变化时支持 restarter 功能, 所以也只是用于开发当中. 你可以直接 morbo 脚本来启动.

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

Hypnotoad

对于更加大的应用, Mojolicious 包含 UNIX 优化过的 preforking 的 Web 服务器 Mojo::Server::Hypnotoad. 它可以让利用多 CPU 和 copy-on-write 来扩展到数千个并发客户端连接.

Mojo::Server::Hypnotoad
|- Mojo::Server::Daemon [1]
|- Mojo::Server::Daemon [2]
|- Mojo::Server::Daemon [3]
+- Mojo::Server::Daemon [4]

这个是基于 Mojo::Server::Prefork 的 Web 服务器, 增加了 preforking 到 Mojo::Server::Daemon 中, 但是对于生产环境进行了优化. 我们可以使用 hypnotoad 脚本来启动, 默认监听 8080, 并自动的为 MojoliciousMojolicious::Lite 的环境设置为生产环境模式 production.

$ hypnotoad script/myapp
Server available at http://127.0.0.1:8080.

在您的应用程序, 你可以调整许多设置, 有关设置的完整列表 "SETTINGS" in Mojo::Server::Hypnotoad.

use Mojolicious::Lite;

app->config(hypnotoad => {listen => ['http://*:80']});

get '/' => {text => 'Hello Wor...ALL GLORY TO THE HYPNOTOAD!'};

app->start;

你也可以给这些有关 hypnotoad 的设置写到你的 Mojolicious::Plugin::ConfigMojolicious::Plugin::JSONConfig 的配置文件中, 只要加入 hypnotoad 这段的配置就行.

# myapp.conf
{
    hypnotoad => {
      #listen => ['http://*:80'], 
      listen  => ['https://*:443?cert=/etc/server.crt&key=/etc/server.key'],
      workers => 10
    }
};

但其最大的优点之一可以支持象 Nginx 一样的零停机的软件升级. 这意味着你可以不用停止服务的前提下升级 Mojolicious, Perl 或者是系统库. 在你运行的时候你不用停止服务, 也不用担心断开正在处理的连接, 只需要在次运行上面的命令, 象下面一样, 如果出现 hot deployment 就行了.

$ hypnotoad script/myapp
Starting hot deployment for Hypnotoad server 31841.

如果你的 Hypnotoad 使用的环境是在反向代理之后, 你可以让 Mojolicious 自动的取得 X-Forwarded-ForX-Forwarded-Proto 的头.

# myapp.conf
{hypnotoad => {proxy => 1}};

零停机的软件升级(热部署)

你可以在上面看到 Hypnotoad 使得零停机的软件升级(热部署)很简单, 但在支持 SO_REUSEPORT 套接字选项现代操作系统, 里面还有另一种适用于所有内置的 Web 服务器的方法可用.

$ ./script/myapp prefork -P /tmp/first.pid -l http://*:8080?reuse=1
Server available at http://127.0.0.1:8080.

你只需要启动第二个 Web 服务器程序监听相同的端口, 并优雅的停掉第一个就行.

$ ./script/myapp prefork -P /tmp/second.pid -l http://*:8080?reuse=1
Server available at http://127.0.0.1:8080.
$ kill -s TERM `cat /tmp/first.pid`

只要记住这两个 Web 服务器需要使用 reuse 重用参数启动

Nginx

这可能是最流行的一个设置了, 让你的应用的内置 Web 服务器工作在 Nginx 之后, 甚至支持 WebSockets.

upstream myapp {
  server 127.0.0.1:8080;
}
server {
  listen 80;
  server_name localhost;
  location / {
    proxy_pass http://myapp;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto "http";
  }
}

Apache/mod_proxy

另一个好的反向代理是 mod_proxy, 看起来和上面 Nginx 的配置非常相似.

<VirtualHost *:80>
  ServerName localhost
  <Proxy *>
    Order deny,allow
    Allow from all
  </Proxy>
  ProxyRequests Off
  ProxyPreserveHost On
  ProxyPass / http://localhost:8080/ keepalive=On
  ProxyPassReverse / http://localhost:8080/
  RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>

Apache/CGI

当然你的 Mojolicious 应用也支持 CGI 的应用. 这会自动检查你的环境, 如果代理过来的是 CGI 的话.

ScriptAlias / /home/sri/myapp/script/myapp/

PSGI/Plack

PSGI 是你的 Perl 的 Web 框架和 Web 服务器之间的接口. 其中的 Plack 是一个 Perl 的模块和包含 PSGI 中间件的一个工具, 它可以帮助你适配你的 Web 服务器. PSGIPlack 灵感来自于 Python 的 WSGI 和 Ruby 的结构.

$ plackup ./script/myapp
HTTP::Server::PSGI: Accepting connections at http://0:5000/

Plack 为你提供了许多服务器和协议适配器, 例如 FCGI, uWSGImod_perl. 你需要在你的应用的当前目录下来运行应用程序, 不然可能有些库会找不到.

$ plackup ./script/myapp -s FCGI -l /tmp/myapp.sock

因为是通过 plackup 来加载你的应用, Mojolicious 这时并不能发现你的应用的主目录, 当然你可以设置你的 MOJO_HOME 的环境变量. 当然你也可以使用 app->start 在你的应用的启动脚本中来声明. 以解决这个问题.

$ MOJO_HOME=/home/sri/myapp plackup ./script/myapp
HTTP::Server::PSGI: Accepting connections at http://0:5000/

这不一定必须 .psgi 文件, 这只要让服务器适配你的应用脚本就行, 它会自动的生成 PLACK_ENV 环境的变量.

Plack 的中间件

给你的程序使用脚本包起来成 myapp.fcgi 这种来进行分离你的应用的逻辑是个很好的主意.

#!/usr/bin/env plackup -s FCGI
use Plack::Builder;

builder {
  enable 'Deflater';
  require 'myapp.pl';
};

你甚至可以用在您的应用程序中使用中间件.

use Mojolicious::Lite;
use Plack::Builder;

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

builder {
  enable 'Deflater';
  app->start;
};

重写

有时您可能需要将应用程序部署在一个黑盒的环境下, 你不能只是更改服务器配置或只是在一个后端反向代理服务器上, 只有传送过来的 X-* 的头, 你可以使用 before_dispatch 来重写传入的请求.

# Change scheme if "X-Forwarded-HTTPS" header is set
app->hook(before_dispatch => sub {
  my $c = shift;
  $c->req->url->base->scheme('https')
    if $c->req->headers->header('X-Forwarded-HTTPS');
});

由于有时使用反向代理不会传递相关请求的路径前缀, 您的应用程序部署在这种环境下, 重写请求的传送进来的基本路径也是很常见的.

# Move first part and slash from path to base path in production mode
app->hook(before_dispatch => sub {
  my $c = shift;
  push @{$c->req->url->base->path->trailing_slash(1)},
    shift @{$c->req->url->path->leading_slash(0)};
}) if app->mode eq 'production';

Mojo::URL 的对象很容易操纵, 只需要确保该 URL (foo/bar?baz=yada), 它代表了路由的目标, 对于 base URL (http://example.com/myapp/) 始终是相对的, 它代表了您的应用程序的部署位置.

应用嵌入

有时, 你可能想重用 Mojolicious 的配置文件, 数据库连接和 helpers 之类的脚本. 这有个小型的服务器可以嵌入到你的应用让你可以实现这些.

use Mojo::Server;

# Load application with mock server
my $server = Mojo::Server->new;
my $app = $server->load_app('./myapp.pl');

# Access fully initialized application
say for @{$app->static->paths};
say $app->config->{secret_identity};
say $app->dumper({just => 'a helper test'});
say $app->build_controller->render_to_string(template => 'foo');

插件 Mojolicious::Plugin::Mount 使用这个功能让你可以绑定多个应用到一起.

use Mojolicious::Lite;

plugin Mount => {'test1.example.com' => '/home/sri/myapp1.pl'};
plugin Mount => {'test2.example.com' => '/home/sri/myapp2.pl'};

app->start;

Web 服务器嵌入

你可以使用 "one_tick" in Mojo::IOLoop 来嵌入内置的 Web 服务器 Mojo::Server::Daemon 中来用于外部环境, 如外来的事件循环, 由于各种原因, 我们不一定能使用指定的事件后端.

use Mojolicious::Lite;
use Mojo::IOLoop;
use Mojo::Server::Daemon;

# Normal action
get '/' => {text => 'Hello World!'};

# Connect application with web server and start accepting connections
my $daemon
  = Mojo::Server::Daemon->new(app => app, listen => ['http://*:8080']);
$daemon->start;

# Call "one_tick" repeatedly from the alien environment
Mojo::IOLoop->one_tick while 1;

实时 WEB

这个 real-time web 是一个集大成者, 包含有 Comet (long-polling), EventSource 和 WebSocket, 这可以让内容尽快的 pushed 到消费者使用 long-lived 的连接,因为这是使用的并不是传统的 pull 模型. 这个内置的 web 服务器使用 non-blocking I/O 和基于 Mojo::IOLoop 的事件循环, 它提供了超级强大的功能, 可以让你的 Web 应用同时服务数以千计的客户.

后端的 Web 服务

由于 Mojo::UserAgent 也是基于 Mojo::IOLoop 的件循环, 它并不是阻塞原生的内置 Web 服务器, 使用了非阻塞就算是高延迟的 Web 后端也可以工作的很好.

use Mojolicious::Lite;

# Search MetaCPAN for "mojolicious"
get '/' => sub {
  my $c = shift;
  $c->ua->get('api.metacpan.org/v0/module/_search?q=mojolicious' => sub {
    my ($ua, $tx) = @_;
    $c->render('metacpan', hits => $tx->res->json->{hits}{hits});
  });
};

app->start;
__DATA__

@@ metacpan.html.ep
<!DOCTYPE html>
<html>
  <head><title>MetaCPAN results for "mojolicious"</title></head>
  <body>
    % for my $hit (@$hits) {
      <p><%= $hit->{_source}{release} %></p>
    % }
  </body>
</html>

这的 "get" in Mojo::UserAgent 的回调会调用一个发送一个请求到后端的 Web 服务器, 当收到回复的时候会调用这个回调.

同步的非阻塞操作

如果我们有多个非阻塞事件的操作, 如并行的请求, 可以很容易通过 "delay" in Mojolicious::Plugin::DefaultHelpers 来做同步, 它可以帮助你避免深层嵌套的闭包来延续传递回调的风格.

use Mojolicious::Lite;
use Mojo::URL;

# Search MetaCPAN for "mojo" and "mango"
get '/' => sub {
  my $c = shift;

  # Prepare response in two steps
  $c->delay(

    # Concurrent requests
    sub {
      my $delay = shift;
      my $url   = Mojo::URL->new('api.metacpan.org/v0/module/_search');
      $url->query({sort => 'date:desc'});
      $c->ua->get($url->clone->query({q => 'mojo'})  => $delay->begin);
      $c->ua->get($url->clone->query({q => 'mango'}) => $delay->begin);
    },

    # Delayed rendering
    sub {
      my ($delay, $mojo, $mango) = @_;
      $c->render(json => {
        mojo  => $mojo->res->json('/hits/hits/0/_source/release'),
        mango => $mango->res->json('/hits/hits/0/_source/release')
      });
    }
  );
};

app->start;

Timers

Mojo::IOLoop 中另一个主要特征是定时器, 它是通过 "timer" in Mojo::IOLoop 创建和例如用于延迟渲染一个响应, 和不同于 sleep, 不会阻塞任何其他并行正在被处理的请求.

use Mojolicious::Lite;
use Mojo::IOLoop;

# Wait 3 seconds before rendering a response
get '/' => sub {
  my $c = shift;
  Mojo::IOLoop->timer(3 => sub {
    $c->render(text => 'Delayed by 3 seconds!');
  });
};

app->start;

周期性定时器 ( 'Recurring timers' ) 是通过 "recurring" in Mojo::IOLoop 创建, 它更加强大, 象 AnyEvent 一样, 但这个需要手动停止或一直执行.

use Mojolicious::Lite;
use Mojo::IOLoop;

# Count to 5 in 1 second steps
get '/' => sub {
  my $c = shift;

  # Start recurring timer
  my $i = 1;
  my $id = Mojo::IOLoop->recurring(1 => sub {
    $c->write_chunk($i);
    $c->finish if $i++ == 5;
  });

  # Stop recurring timer
  $c->on(finish => sub { Mojo::IOLoop->remove($id) });
};

app->start;

定时器不依赖于特定的连接请求, 它甚至可以在启动时创建的.

use Mojolicious::Lite;
use Mojo::IOLoop;

# Check title in the background every 10 seconds
my $title = 'Got no title yet.';
Mojo::IOLoop->recurring(10 => sub {
  app->ua->get('http://mojolicio.us' => sub {
    my ($ua, $tx) = @_;
    $title = $tx->res->dom->at('title')->text;
  });
});

# Show current title
get '/' => sub {
  my $c = shift;
  $c->render(json => {title => $title});
};

app->start;

请记住, 所有的事件都是协程, 所以你要让你的回调不应该阻塞太长时间 .

异常事件

因为记时器和其它 low-level 的事件 watchers 独立于应用, 错误不会自动的记录到日志, 如果你想修改, 可以要象下面一样订阅 "error" in Mojo::Reactor 的回调.

use Mojolicious::Lite;
use Mojo::IOLoop;

# 转发事件错误的信息到应用的日志中
Mojo::IOLoop->singleton->reactor->on(error => sub {
  my ($reactor, $err) = @_;
  app->log->error($err);
});

# Exception only gets logged (and connection times out)
get '/connection_times_out' => sub {
  my $c = shift;
  Mojo::IOLoop->timer(2 => sub {
    die 'This request will not be getting a response';
  });
};

# Exception gets caught and handled
get '/catch_exception' => sub {
  my $c = shift;
  Mojo::IOLoop->timer(2 => sub {
    eval { die 'This request will be getting a response' };
    $c->reply->exception($@) if $@;
  });
};

app->start;

默认 subscriber 会给所有的错误通过 Mojo::IOLoop 的事件回调出来.

Mojo::IOLoop->singleton->reactor->unsubscribe('error');

During development or for applications where crashing is simply preferable, you can also make every exception that gets thrown in a callback fatal by removing all of its subscribers.

WebSocket web service

WebSocket 协议提供完整的双向低延迟的在通信客户端和服务器之间的通道, 接收到信息可以非常方便的通过 "message" in Mojo::Transaction::WebSocket 中的 "on" in Mojolicious::Controller 方法来回调, 并可以通过 "send" in Mojolicious::Controller 来返回.

use Mojolicious::Lite;

# Template with browser-side code
get '/' => 'index';

# WebSocket echo service
websocket '/echo' => sub {
  my $c = shift;

  # Opened
  $c->app->log->debug('WebSocket opened.');

  # Increase inactivity timeout for connection a bit
  $c->inactivity_timeout(300);

  # Incoming message
  $c->on(message => sub {
    my ($c, $msg) = @_;
    $c->send("echo: $msg");
  });

  # Closed
  $c->on(finish => sub {
    my ($c, $code, $reason) = @_;
    $c->app->log->debug("WebSocket closed with status $code.");
  });
};

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head><title>Echo</title></head>
  <body>
    <script>
      var ws = new WebSocket('<%= url_for('echo')->to_abs %>');

      // Incoming messages
      ws.onmessage = function(event) {
        document.body.innerHTML += event.data + '<br/>';
      };

      // Outgoing messages
      window.setInterval(function() {
        ws.send('Hello Mojo!');
      }, 1000);
    </script>
  </body>
</html>

在 WebSocket 的连接关掉时 "finish" in Mojo::Transaction::WebSocket 会自动的调用.

$c->tx->with_compression;

你可以通过 "with_compression" in Mojo::Transaction::WebSocket 激活 permessage-deflate, 这能让性能更加好, 但是会多使用 300KB 每个连接的内存.

测试 WebSocket 的 Web 服务

虽然在 WebSocket 上的连接的信息流可以是相当动态的, 但它往往没有规律, 这所以需要使用 Test::Mojo 的 API 来测试它.

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

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

# Test echo web service
my $t = Test::Mojo->new;
$t->websocket_ok('/echo')
  ->send_ok('Hello Mojo!')
  ->message_ok
  ->message_is('echo: Hello Mojo!')
  ->finish_ok;

# Test JSON web service
$t->websocket_ok('/echo.json')
  ->send_ok({json => {test => [1, 2, 3]}})
  ->message_ok
  ->json_message_is('/test', [1, 2, 3])
  ->finish_ok;

done_testing();

EventSource 的 Web 服务

使用基于事件源 ( EventSource ) 的长轮询, 你可以实现 "write" in Mojolicious::Controller 直接给信息从服务器发送到客户端的 DOM 事件上, 这是单向的, 也就是讲你必使用 Ajax 请求来从客户端发送数据到服务器上, 这优势是较低的基础设施的要求, 因为这样可以重复使用 HTTP 协议来传输.

use Mojolicious::Lite;

# Template with browser-side code
get '/' => 'index';

# EventSource for log messages
get '/events' => sub {
  my $c = shift;

  # Increase inactivity timeout for connection a bit
  $c->inactivity_timeout(300);

  # Change content type
  $c->res->headers->content_type('text/event-stream');

  # Subscribe to "message" event and forward "log" events to browser
  my $cb = $c->app->log->on(message => sub {
    my ($log, $level, @lines) = @_;
    $c->write("event:log\ndata: [$level] @lines\n\n");
  });

  # Unsubscribe from "message" event again once we are done
  $c->on(finish => sub {
    my $c = shift;
    $c->app->log->unsubscribe(message => $cb);
  });
};

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head><title>LiveLog</title></head>
  <body>
    <script>
      var events = new EventSource('<%= url_for 'events' %>');

      // Subscribe to "log" event
      events.addEventListener('log', function(event) {
        document.body.innerHTML += event.data + '<br/>';
      }, false);
    </script>
  </body>
</html>

这个 "message" in Mojo::Log 的事件可以被每个日志的信息激发. 然后通过 "finish" in Mojo::Transaction 事件传送.

流式多段上传

Mojolicious 中包含了基于 Mojo::EventEmitter 的一些非常复杂的事件系统, 准备使用事件实现了几乎所有层的工作, 可以用来结合处理 Web 开发中最麻烦的部分.

use Mojolicious::Lite;
use Scalar::Util 'weaken';

# Intercept multipart uploads and log each chunk received
hook after_build_tx => sub {
  my $tx = shift;

  # Subscribe to "upgrade" event to indentify multipart uploads
  weaken $tx;
  $tx->req->content->on(upgrade => sub {
    my ($single, $multi) = @_;
    return unless $tx->req->url->path->contains('/upload');

    # Subscribe to "part" event to find the right one
    $multi->on(part => sub {
      my ($multi, $single) = @_;

      # Subscribe to "body" event of part to make sure we have all headers
      $single->on(body => sub {
        my $single = shift;

        # Make sure we have the right part and replace "read" event
        return unless $single->headers->content_disposition =~ /example/;
        $single->unsubscribe('read')->on(read => sub {
          my ($single, $bytes) = @_;

          # Log size of every chunk we receive
          app->log->debug(length($bytes) . ' bytes uploaded.');
        });
      });
    });
  });
};

# Upload form in DATA section
get '/' => 'index';

# Streaming multipart upload
post '/upload' => {text => 'Upload was successful.'};

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head><title>Streaming multipart upload</title></head>
  <body>
    %= form_for upload => (enctype => 'multipart/form-data') => begin
      %= file_field 'example'
      %= submit_button 'Upload'
    % end
  </body>
</html>

事件循环

在内部的 Mojo::IOLoop 的事件环可以使用多种不同的后端, 例如, 安装了 EV 就会自动的使用. 这反过来, 只要 AnyEvent 可以正常工作的事件在这个中也能正常工作.

use Mojolicious::Lite;
use EV;
use AnyEvent;

# Wait 3 seconds before rendering a response
get '/' => sub {
  my $c = shift;
  my $w;
  $w = AE::timer 3, 0, sub {
    $c->render(text => 'Delayed by 3 seconds!');
    undef $w;
  };
};

app->start;

后端到底是使用的什么事件循环并不重要.

use Mojo::UserAgent;
use EV;
use AnyEvent;

# Search MetaCPAN for "mojolicious"
my $cv = AE::cv;
my $ua = Mojo::UserAgent->new;
$ua->get('api.metacpan.org/v0/module/_search?q=mojolicious' => sub {
  my ($ua, $tx) = @_;
  $cv->send($tx->res->json('/hits/hits/0/_source/release'));
});
say $cv->recv;

你也可以为你的内置 Web 服务器的应用来使用 AnyEvent.

use Mojolicious::Lite;
use Mojo::Server::Daemon;
use EV;
use AnyEvent;

# Normal action
get '/' => {text => 'Hello World!'};

# Connect application with web server and start accepting connections
my $daemon
  = Mojo::Server::Daemon->new(app => app, listen => ['http://*:8080']);
$daemon->start;

# Let AnyEvent take control
AE::cv->recv;

USER AGENT

这个 Mojolicious 只是一个 web 框架.

Web 元素挖掘 (Web scraping)

从网站找到你喜欢的信息出来是一个很有意思的事情. 你可以使用原生的 HTML/XML 的解析器 Mojo::DOM, 这个 "dom" in Mojo::Message 支持全部的 CSS 的选择器.

use Mojo::UserAgent;

# Fetch web site
my $ua = Mojo::UserAgent->new;
my $tx = $ua->get('mojolicio.us/perldoc');

# Extract title
say 'Title: ', $tx->res->dom->at('head > title')->text;

# Extract headings
$tx->res->dom('h1, h2, h3')->each(sub { say 'Heading: ', shift->all_text });

# Visit all nodes recursively to extract more than just text
for my $n ($tx->res->dom->descendant_nodes->each) {

  # Text or CDATA node
  print $n->content if $n->type eq 'text' || $n->type eq 'cdata';

  # Also include alternate text for images
  print $n->{alt} if $n->type eq 'tag' && $n->tag eq 'img';
}

特别是对于 Mojolicious 应用的单元测试来讲, 这是一个非常强大的工具.要查看所有的 CSS 的选择器可以查看 "SELECTORS" in Mojo::DOM::CSS.

JSON Web 服务

现在很多的 web 服务都是基于 JSON 的数据交换格式. 这也是为什么 Mojolicious 中有一个可能是最快的纯 Perl 来实现的 Mojo::JSON 的原因. 可以通过 "json" in Mojo::Message 来调用.

use Mojo::UserAgent;
use Mojo::URL;

# Fresh user agent
my $ua = Mojo::UserAgent->new;

# Search MetaCPAN for "mojolicious" and list latest releases
my $url = Mojo::URL->new('http://api.metacpan.org/v0/release/_search');
$url->query({q => 'mojolicious', sort => 'date:desc'});
for my $hit (@{$ua->get($url)->res->json->{hits}{hits}}) {
  say "$hit->{_source}{name} ($hit->{_source}{author})";
}

基本的认证

你直接可以增加用户和密码到你的 URL 中就好了.

use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
say $ua->get('https://sri:secret@example.com/hideout')->res->body;

修饰后续请求

Mojo::UserAgent 可以自动的 follow 重定向, 在 "start" in Mojo::UserAgent 的事件执行的地方是在你每次初始化访问后并后端建立连接之前.

use Mojo::UserAgent;

# User agent following up to 10 redirects
my $ua = Mojo::UserAgent->new(max_redirects => 10);

# Add a witty header to every request
$ua->on(start => sub {
  my ($ua, $tx) = @_;
  $tx->req->headers->header('X-Bender' => 'Bite my shiny metal ass!');
  say 'Request: ', $tx->req->url->clone->to_abs;
});

# Request that will most likely get redirected
say 'Title: ', $ua->get('google.com')->res->dom->at('head > title')->text;

这个也可以工作在代理服务时的 CONNECT 的请求上.

内容生成器

内容生成器 ( 'Content generators' ) 是通过 "add_generator" in Mojo::UserAgent::Transactor 来注册用于生成反复多次请求的相同类型的内容.

use Mojo::UserAgent;
use Mojo::Asset::File;

# 增加一个 "stream" 的内容生成器
my $ua = Mojo::UserAgent->new;
$ua->transactor->add_generator(stream => sub {
  my ($transactor, $tx, $path) = @_;
  $tx->req->content->asset(Mojo::Asset::File->new(path => $path));
});

# 通过 streaming 在 PUT 和 POST 的时候发送多个文件
$ua->put('http://example.com/upload'  => stream => '/home/sri/mojo.png');
$ua->post('http://example.com/upload' => stream => '/home/sri/minion.png');

默认的 jsonform 也是内容生成器.

use Mojo::UserAgent;

# PATCH 的时候发送 "application/json" 类型的内容
my $ua = Mojo::UserAgent->new;
my $tx = $ua->patch('http://api.example.com' => json => {foo => 'bar'});

# PATCH 的时候发送查询参数
my $tx2 = $ua->get('http://search.example.com' => form => {q => 'test'});

# POST 的时候发送 application/x-www-form-urlencoded" 类型的内容
my $tx3 = $ua->post('http://search.example.com' => form => {q => 'test'});

# PUT 的时候发送 "multipart/form-data" 类型的内容
my $tx4 = $ua->put('http://upload.example.com' =>
  form => {test => {content => 'Hello World!'}});

更多内容生成器的信息请看 "tx" in Mojo::UserAgent::Transactor.

大文件下载

当你使用 Mojo::UserAgent 下载大文件时, 你不需要担心内存的使用, 因为它会自动的给每 250KB 的大小来存成临时文件. 然后直接使用 "move_to" in Mojo::Asset::File 的对象来存成文件.

use Mojo::UserAgent;

# Lets fetch the latest Mojolicious tarball
my $ua = Mojo::UserAgent->new(max_redirects => 5);
my $tx = $ua->get('https://www.github.com/kraih/mojo/tarball/master');
$tx->res->content->asset->move_to('mojo.tar.gz');

为了保护避免过大的文件也有一个极限默认值 10MB. 你可以通过 "max_message_size" in Mojo::Message 或者 MOJO_MAX_MESSAGE_SIZE 的环境变量来调整.

# 增加限制的大小到 1GB
$ENV{MOJO_MAX_MESSAGE_SIZE} = 1073741824;

大文件上传

上传一个大文件更容易.

use Mojo::UserAgent;

# 上传文件通过 POST 的 HTTP 方法, 并且会修改成  "multipart/form-data" 的类型
my $ua = Mojo::UserAgent->new;
$ua->post('example.com/upload' =>
  form => {image => {file => '/home/sri/hello.png'}})

当然一样, 你还是不必担心内存的使用, 因为这个也是直接流式传送这个文件.

流响应

大多数 HTTP 客户端来讲接收流媒体响应可真是棘手, 但 Mojo::UserAgent 会使得它非常容易.

use Mojo::UserAgent;

# Build a normal transaction
my $ua = Mojo::UserAgent->new;
my $tx = $ua->build_tx(GET => 'http://example.com');

# Accept response of indefinite size
$tx->res->max_message_size(0);

# Replace "read" events to disable default content parser
$tx->res->content->unsubscribe('read')->on(read => sub {
  my ($content, $bytes) = @_;
  say "Streaming: $bytes";
});

# Process transaction
$tx = $ua->start($tx);

"read" in Mojo::Content 的事件会在每次 chunk 的数据到达时调用, 每个 chunked 的编码会被透明地处理, 如果必要的话.

流请求

发送流式的请求也一样很容易.

use Mojo::UserAgent;

# Build a normal transaction
my $ua = Mojo::UserAgent->new;
my $tx = $ua->build_tx(GET => 'http://example.com');

# Prepare body
my $body = 'Hello world!';
$tx->req->headers->content_length(length $body);

# Start writing directly with a drain callback
my $drain;
$drain = sub {
  my $content = shift;
  my $chunk   = substr $body, 0, 1, '';
  $drain      = undef unless length $body;
  $content->write($chunk, $drain);
};
$tx->req->content->$drain;

# Process transaction
$tx = $ua->start($tx);

在前一个块写真的写完后, 会调用在 drain 回调使用 "write" in Mojo::Content 的方法.

非阻塞

这个 Mojo::UserAgent 从底层开始就是设计成非阻塞的, 整个阻塞的 API 仅仅是一个简单方便的包装. 特别是对于高延迟的任务, 如网络抓取, 这种功能非常有用, 因为可以使你同时并行很多连接.

use Mojo::UserAgent;
use Mojo::IOLoop;

# Concurrent non-blocking requests
my $ua = Mojo::UserAgent->new;
$ua->get('http://metacpan.org/search?q=mojo' => sub {
  my ($ua, $mojo) = @_;
  say $mojo->res->dom->at('title')->text;
});
$ua->get('http://metacpan.org/search?q=minion' => sub {
  my ($ua, $minion) = @_;
  say $minion->res->dom->at('title')->text;
});

# Start event loop if necessary
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

您可以完全控制的 Mojo::IOLoop 的事件循环.

并行阻塞请求

你可以使用 "delay" in Mojo::IOLoop 仿真阻塞请求, 来实现多个非阻塞请求的同步.

use Mojo::UserAgent;
use Mojo::IOLoop;

# Synchronize non-blocking requests
my $ua    = Mojo::UserAgent->new;
my $delay = Mojo::IOLoop->delay(sub {
  my ($delay, $mojo, $minion) = @_;
  say $mojo->res->dom->at('title')->text;
  say $minion->res->dom->at('title')->text;
});
$ua->get('http://metacpan.org/search?q=mojo'   => $delay->begin);
$ua->get('http://metacpan.org/search?q=minion' => $delay->begin);
$delay->wait;

在这调用 "wait" in Mojo::IOLoop::Delay 可以使的代码可以移植, 可以工作在已经运行的事件循环或启动一个.

WebSockets

WebSockets 不只是服务器端, 你可以使用 "websocket" in Mojo::UserAgent 来打开新的连接, 它们总是无阻塞的. 握手过程会有一些额外的一个正常的 HTTP 请求的头文件, 它甚至可以包含 cookies, 其次服务器的 101 响应还可以通知我们的用户代理和这个连接开始使用双向的 WebSocket 协议. 头文件

use Mojo::UserAgent;
use Mojo::IOLoop;

# Open WebSocket to echo service
my $ua = Mojo::UserAgent->new;
$ua->websocket('ws://echo.websocket.org' => sub {
  my ($ua, $tx) = @_;

  # Check if WebSocket handshake was successful
  say 'WebSocket handshake failed!' and return unless $tx->is_websocket;

  # Wait for WebSocket to be closed
  $tx->on(finish => sub {
    my ($tx, $code, $reason) = @_;
    say "WebSocket closed with status $code.";
  });

  # Close WebSocket after receiving one message
  $tx->on(message => sub {
    my ($tx, $msg) = @_;
    say "WebSocket message: $msg";
    $tx->finish;
  });

  # Send a message to the server
  $tx->send('Hi!');
});

# Start event loop if necessary
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

命令行

在使用命令行检查时, 不知道你们是不是很反感很大的 HTML 文件. 得益于 mojo get 这个命令可以帮助我们改变这些. 你只需要随意的使用一下 CSS 的选择器 Mojo::DOM 和 JOSNP 的 Mojo::JSON::Pointer 就能搞定.

$ mojo get http://mojolicio.us 'head > title'

怎么样列出全部属性的 id?

$ mojo get http://mojolicio.us '*' attr id

或文字标题标签的所有内容?

$ mojo get http://mojolicio.us 'h1, h2, h3' text

也许只是文本的第三个标题?

$ mojo get http://mojolicio.us 'h1, h2, h3' 3 text

你还可以从嵌套的子元素提取所有的文字?

$ mojo get http://mojolicio.us '#mojobar' all

发送定制的请求.

$ mojo get -M POST -c 'Hello!' http://mojolicio.us
$ mojo get -H 'X-Bender: Bite my shiny metal ass!' http://google.com

您可以跟随重定向和查看 header 的所有消息.

$ mojo get -r -v http://google.com 'head > title'

从 JSON 的数据结构中提取信息.

$ mojo get https://api.metacpan.org/v0/author/SRI /name

这可以是一个非常好的工具, 用于测试您的应用程序

$ ./myapp.pl get /welcome 'head > title'

单行

为了快速的 hacks 和测试, ojo 的单行也是一个不错的选择.

$ perl -Mojo -E 'say g("mojolicio.us")->dom->at("title")->text'

应用程序

玩转 Mojolicious 应用来 hacks 各种部分

Basic authentication

对于 Basic 认识的数据我们可以直接通过 Authorization 的 header 来导出. Basic authentication data will be automatically extracted from the Authorization header.

use Mojolicious::Lite;

get '/' => sub {
  my $c = shift;

  # Check for username "Bender" and password "rocks"
  return $c->render(text => 'Hello Bender!')
    if $c->req->url->to_abs->userinfo eq 'Bender:rocks';

  # Require authentication
  $c->res->headers->www_authenticate('Basic');
  $c->render(text => 'Authentication required!', status => 401);
};

app->start;

你可以使用 TLS 来生成一个更加安全的机器上来连接.

$ ./myapp.pl daemon -l 'https://*:3000?cert=./server.crt&key=./server.key'

添加一个配置文件

添加一个配置文件到你的应用中非常容易实现, 只要在我们的主目录增加一个文件并加载 Mojolicious::Plugin::Config. 这默认的名字基于是 "moniker" in Mojolicious 的值 (myapp), 附加 .conf 的扩展名 (myapp.conf).

$ mkdir myapp
$ cd myapp
$ touch myapp.pl
$ chmod 744 myapp.pl
$ echo '{name => "my Mojolicious application"};' > myapp.conf

这个配置文件只是一个 Perl 的脚本, 用于返回一个哈希的引用, 全部的设置可以通过 "config" in Mojo"config" in Mojolicious::Plugin::DefaultHelpers 来获得.

use Mojolicious::Lite;

plugin 'Config';

my $name = app->config('name');
app->log->debug("Welcome to $name.");

get '/' => 'with_config';

app->start;
__DATA__
@@ with_config.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= config 'name' %></title></head>
  <body>Welcome to <%= config 'name' %></body>
</html>

你也可以使用 JSON 格式的配置文件, 你只需要使用 Mojolicious::Plugin::JSONConfig 就行.

增加命令到 Mojolicious

你现在可能使用了很多 Mojolicious::Commands 原生的命令, 对于新增加一个命令到你的接口, 也非常的方便.

package Mojolicious::Command::spy;
use Mojo::Base 'Mojolicious::Command';

has description => 'Spy on application';
has usage       => "Usage: APPLICATION spy [TARGET]\n";

sub run {
  my ($self, @args) = @_;

  # Leak secret passphrases
  say for @{$self->app->secrets} if $args[0] eq 'secrets';

  # Leak mode
  say $self->app->mode if $args[0] eq 'mode';
}

1;

Mojolicious::Command 中有很多有用的方法和属性, 当然你可以重载它们.

$ mojo spy secrets
HelloWorld

$ ./myapp.pl spy secrets
secr3t

在你的应用中加入新的特别的命令, 你还可以指定不同的名字空间.

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

sub startup {
  my $c = shift;

  # Add another namespace to load commands from
  push @{$c->commands->namespaces}, 'MyApp::Command';
}

1;

这有些选项 -h/--help, --home and -m/--mode 这些会由 Mojolicious::Commands 自动处理并共享全部的命令.

$ ./myapp.pl spy -m production mode
production

针对您的应用程序来运行代码

没有想过对 Mojolicious 的应用程序使用一个快速的单行来测试? 得益于的 eval 命令, 你可以方便的做到这一点, 直接通过 app 来访问应用程序对象本身.

$ mojo generate lite_app myapp.pl
$ ./myapp.pl eval 'say for @{app->static->paths}'

使用 verbose 选择会让 print 和 return 的值直接打印到 STDOUT.

$ ./myapp.pl eval -v 'app->static->paths->[0]'
$ ./myapp.pl eval -V 'app->static->paths'

应用程序安装

你有没有想过使用 CPAN 来安装你的 Mojolicious 的应用, 这个比实际的想象还要容易.

$ mojo generate app MyApp
$ cd my_app
$ mv public lib/MyApp/
$ mv templates lib/MyApp/

这个关键是要给 publictemplates 的目录移到能够自动安装的模块的地方.

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

use File::Basename 'dirname';
use File::Spec::Functions 'catdir';

# Every CPAN module needs a version
our $VERSION = '1.0';

sub startup {
  my $self = shift;

  # Switch to installable home directory
  $self->home->parse(catdir(dirname(__FILE__), 'MyApp'));

  # Switch to installable "public" directory
  $self->static->paths->[0] = $self->home->rel_dir('public');

  # Switch to installable "templates" directory
  $self->renderer->paths->[0] = $self->home->rel_dir('templates');

  $self->plugin('PODRenderer');

  my $r = $self->routes;
  $r->get('/welcome')->to('example#welcome');
}

1;

这对你的应用的脚本需要几个简单的改变. 首行推荐写成 #!perl, 这样工具会自动的在安装的时候改写这个. 你也需要使用 FindBin 来让你的 lib 使用当前路径下的 lib.

#!perl

use strict;
use warnings;

use FindBin;
BEGIN { unshift @INC, "$FindBin::Bin/../lib" }

# Start command line interface for application
require Mojolicious::Commands;
Mojolicious::Commands->start_app('MyApp');

这就是所有啦, 你现在可以象任何其它的 CPAN 模块一样给你的程序打包.

$ ./script/my_mojolicious_app generate makefile
$ perl Makefile.PL
$ make test
$ make manifest
$ make dist

然后使用你的 PAUSE 的帐号 ( 通过 http://pause.perl.org 注册 ) 来上传.

$ mojo cpanify -u USER -p PASS MyMojoliciousApp-0.01.tar.gz

Hello World

每写一个字都很重要, 你可以使用最少的字数来写一个基于 Mojolicious::LiteHello World 的应用.

use Mojolicious::Lite;
any {text => 'Hello World!'};
app->start;

这个程序, 没有模式路径选择, 所以会使用默认的 / 然后自动的调用 stash 中的值来生成一个响应.

单行的 Hello World 应用

这个 Hello World 的例子也可以使用 ojo 的单行实现.

$ perl -Mojo -E 'a({text => "Hello World!"})->start' daemon

这时你还是可以使用全部的 Mojolicious::Commands 的命令的.

$ perl -Mojo -E 'a({text => "Hello World!"})->start' get -v /

更多

你可以看看 Mojolicious::GuidesMojolicious wiki 来了解更多的东西.