Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mojolicious

Introduction

Other Mojolicious resources

What is Mojolicious

  • Created by sri (Sebastian Riedel) who also created Catalyst

  • Based on Ruby Sinatra

  • Full framework - Both Web server and client (web user agent)

  • No Dependencies (IO::Socket::SSL, IO::Socket::INET6)

  • Non-blocking, based on events

  • Cutting edge

  • Pretty

  • Fun

Overview

  • Mojo::UserAgent the web client side

  • Mojolicious::Lite for content heavy web sites

  • Mojolicious::Lite for Single Page Applications

  • Mojolicious

  • We don't look at the CSS selectors

  • We don't look at the JavaScript needed for SPA

Installation

  • $ cpanm Mojolicious
  • $ curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious
  • Mojolicious

Client

Mojo::UserAgent

  • Mojo::UserAgent
  • Mojo::Message::Response
  • res
  • body
use strict;
use warnings;

use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
print $ua->get('http://www.yapcna.org/yn2016/')
    ->res->body;

CSS Selectors

  • dom
  • find
  • Mojo::DOM
  • Mojo::Collection
use strict;
use warnings;

use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
print $ua->get('http://www.yapcna.org/yn2016/')
    ->res->dom->find('div[class=row h1]')->[0];

Mojo::UserAgent and JSON

  • json
use strict;
use warnings;

use Mojo::UserAgent;

use Data::Dumper qw(Dumper);

my $ua = Mojo::UserAgent->new;
print Dumper $ua->get('https://api.metacpan.org/v0/release/_search?q=status:latest&fields=name,status,date&sort=date:desc&size=10')
    ->res->json;

Mojo::UserAgent and JSON details

use strict;
use warnings;

use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
my $json = $ua->get('https://api.metacpan.org/v0/release/_search?q=status:latest&fields=name,status,date&sort=date:desc&size=10')
    ->res->json;

print "$json->{hits}{hits}[0]{fields}{name}\n";

Mojo::UserAgent and JSON Pointers

  • Mojo::JSON::Pointer
use strict;
use warnings;

use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
my $res = $ua->get('https://api.metacpan.org/v0/release/_search?q=status:latest&fields=name,status,date&sort=date:desc&size=10')
    ->res;
print $res->json("/hits/hits/0/fields/name"), "\n";



Easy Debugging

  • MOJO_USERAGENT_DEBUG
MOJO_USERAGENT_DEBUG=1 perl application.pl

Mojo::UserAgent on the command line

$ mojo get www.yapcna.org/yn2016/ 'title'

Application

Hello World

  • get
  • start
  • render
use Mojolicious::Lite;

get '/' => { text => 'Hello World' };
 
app->start;

morbo hello_world.pl

  • Visit http://127.0.0.1:3000/
  • Look at the console
  • Visit http://127.0.0.1:3000/other to see the error
  • Look at the console

External template

use Mojolicious::Lite  -signatures;

get '/' => sub ($c) {
  $c->render(template => 'index');
};

app->start;

Hi
perl app.pl daemon

use strict use warnings

  • use strict;
  • use warnings;
  • use utf8;
  • use 5.010;

Your secret passphrase needs to be changed

  • secrets
use Mojolicious::Lite;

get '/' => { text => 'Hello World' };
 
app->secrets(['My very secret passphrase.']);

app->start;

Echo form

use Mojolicious::Lite;

get '/' => { text => 'Hello World' };

get '/echo' => { text => q{
    <form method="POST">
    <input name="q">
    <input type="submit" value="Echo">
    </form>
}};
 
app->secrets(['My very secret passphrase.']);

app->start;

  • Visit http://127.0.0.1:3000/echo
  • Fill the form and submit

Echo

  • post
  • param
  • $c
  • Mojolicious::Controller
use Mojolicious::Lite;



get '/' => { text => 'Hello World' };

get '/echo' => { text => q{
    <form method="POST">
    <input name="q">
    <input type="submit" value="Echo">
    </form>
}};

post '/echo' => sub {
    my $c = shift;
    $c->render( text => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

Embedded templates

  • sub
  • render
  • DATA
use Mojolicious::Lite;

get '/' => { text => 'Hello World' };

get '/echo' => sub {
    my $c = shift;
    $c->render('echo');
};

post '/echo' => sub {
    my $c = shift;
    $c->render( text => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__
     
@@ echo.html.ep
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

Embedded templates

  • <%=
  • %>
use Mojolicious::Lite;

get '/' => { text => 'Hello World' };

get '/echo' => sub {
    my $c = shift;
    $c->render('echo');
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'response', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__
     
@@ echo.html.ep
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

@@ response.html.ep
     
You typed: <%= $msg %>

Merged templates

use Mojolicious::Lite;

get '/' => { text => 'Hello World' };

get '/echo' => sub {
    my $c = shift;
    $c->render('echo');
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'echo', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__
     
@@ echo.html.ep
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

You typed: <%= $msg %>

Conditionals in templates

  • if
  • %
use Mojolicious::Lite;

get '/' => { text => 'Hello World' };

get '/echo' => sub {
    my $c = shift;
    $c->render('echo', msg => undef);
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'echo', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__
     
@@ echo.html.ep
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

% if (defined $msg) {
    You typed: <%= $msg %>
% }

  • Had to add undef to the first call as well.

Common layout

  • layout
use Mojolicious::Lite;

get '/' => sub {
    my $c = shift;
    $c->render('index');
};

get '/echo' => sub {
    my $c = shift;
    $c->render('echo', msg => undef);
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'echo', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__


@@ layouts/wrapper.html.ep
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
<div id="menu">
<ul>
  <li><a href="/">home</a></li>
  <li><a href="/echo">echo</a></li>
</ul>
</div>

<%= content %>

</body>
</html>

     
@@ echo.html.ep
% layout 'wrapper';
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

% if (defined $msg) {
    You typed: <%= $msg %>
% }

@@ index.html.ep
% layout 'wrapper';

Hello World<br>
Try <a href="/echo">Echo</a>

Embedded Stylesheet

  • style
use Mojolicious::Lite;

get '/' => sub {
    my $c = shift;
    $c->render('index');
};

get '/echo' => sub {
    my $c = shift;
    $c->render('echo', msg => undef);
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'echo', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__


@@ layouts/wrapper.html.ep
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>

<style>
html,body {
  margin: 0;
  padding: 0;
}
#menu {
  margin-top: 10px;
  margin-bottom: 10px;
  font-weight:bold;
}
#menu ul {
  list-style: none;
  display: inline;
}
#menu li {
  margin-left: 10px;
  float: left;
}
#menu a {
  text-decoration:none;
}
</style>

<body>
<div id="menu">
<ul>
  <li><a href="/">home</a></li>
  <li><a href="/echo">echo</a></li>
</ul>
</div>

<%= content %>

</body>
</html>

     
@@ echo.html.ep
% layout 'wrapper';
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

% if (defined $msg) {
    You typed: <%= $msg %>
% }

@@ index.html.ep
% layout 'wrapper';

Hello World<br>
Try <a href="/echo">Echo</a>

External Stylesheet - static files in the 'public' directory

  • public
use Mojolicious::Lite;

get '/' => sub {
    my $c = shift;
    $c->render('index');
};

get '/echo' => sub {
    my $c = shift;
    $c->render('echo', msg => undef);
};

post '/echo' => sub {
    my $c = shift;
    $c->render( 'echo', msg => $c->param('q') );
};
 
app->secrets(['My very secret passphrase.']);

app->start;

__DATA__


@@ layouts/wrapper.html.ep
<!DOCTYPE html>
<html>
<head>
  <title></title>
  <link href="/style.css" rel="stylesheet">
</head>

<body>
<div id="menu">
<ul>
  <li><a href="/">home</a></li>
  <li><a href="/echo">echo</a></li>
</ul>
</div>

<%= content %>

</body>
</html>

     
@@ echo.html.ep
% layout 'wrapper';
     
What are you looking for?
<form method="POST"><input name="q"><input type="submit" value="Echo"></form>

% if (defined $msg) {
    You typed: <%= $msg %>
% }

@@ index.html.ep
% layout 'wrapper';

Hello World<br>
Try <a href="/echo">Echo</a>
html,body {
  margin: 0;
  padding: 0;
}
#menu {
  margin-top: 10px;
  margin-bottom: 10px;
  font-weight:bold;
}
#menu ul {
  list-style: none;
  display: inline;
}
#menu li {
  margin-left: 10px;
  float: left;
}
#menu a {
  text-decoration:none;
}

Generate Mojolicious::Lite skeleton

  • mojo

  • $ mojo generate lite_app myapp.pl

  • Warning: will overwrite your existing file without asking questions

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

# Documentation browser under "/perldoc"
plugin 'PODRenderer';

get '/' => sub {
  my $c = shift;
  $c->render(template => 'index');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>
To learn more, you can browse through the documentation
<%= link_to 'here' => '/perldoc' %>.

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>
  • Visit http://127.0.0.1:3000/ and click on the Perldoc link

Generate Mojolicious skeleton

  • mojo

  • $ mojo generate app

  • Will generate subdirectory 'my_app' with module name MyApp in my_app/lib/MyApp.pm

$ tree my_app/

my_app/
    lib/
        MyApp/
            Controller/
                Example.pm
        MyApp.pm
    public/
        index.html
    script/
        my_app
    t/
        basic.t
    templates/
        example/
            welcome.html.ep
        layouts/
            default.html.ep
  • $ mojo generate app My::Example
  • Will generate subdirectory 'my_example' with module name My::Example in my_example/lib/My/Example.pm

Mojolicious Routing

  • get '/list/home' a route with an exact match
  • get '/item/:id' a route with a placeholder won't match . or /
  • get '/phone/#number' also accepts . as in /phone/1.234.567890
  • *get '/path/anything' also accepts /
$c->param('id');
$c->param('number');
$c->param('anything');

GET routes

use Mojolicious::Lite;

get '/' => { text => q{
<ul>
    <li><a href="/list/home">/list/home</a></li>
    <li><a href="/user?id=19">/user?id=19</a></li>
    <li><a href="/item/42">/item/42</a></li>
    <li><a href="/phone/1.234.567890">/phone/1.234.567890</a></li>
    <li><a href="/path/one/two/three">/path/one/two/three</a></li>
</ul>
}};

get '/list/home' => { text => q{
    Found /list/home
}};

get '/user' => sub {
    my $c = shift;
    $c->render( text => "user ID " . $c->param('id') );
};

get '/item/:id' => sub {
    my $c = shift;
    $c->render( text => "item ID " . $c->param('id') );
};

get '/phone/#number' => sub {
    my $c = shift;
    $c->render( text => "phone number: " . $c->param('number') );
};
   
get '/path/*anything' => sub {
    my $c = shift;
    $c->render( text => "path: " . $c->param('anything') );
};

app->secrets(['My very secret passphrase.']);

app->start;


EP (Embedded Perl) Templates

%= $name 

% for (@$names) {
   ...
% }

% if ($cond) {
    ...
% }

EP (Embedded Perl) Templates (HTML)

<%= $name %>

<% for (@$names) %>
    ...
<% } %>

<% if ($cond) { %>
    ...
<%  } %>

Return JSON

use Mojolicious::Lite;

get '/' => { text => q{
<ul>
    <li><a href="/list">/list</a></li>
</ul>
}};

get '/list' => sub {
    my $c = shift;
    my @people = (
        {
            name => 'Foo',
            id  => 19,
        },
        {
            name => 'Bar',
            id  =>  23,
        },
        {
            name => 'Qux',
            id  =>  42,
        },
    );

    $c->render( json => \@people );
};

app->secrets(['My very secret passphrase.']);

app->start;

Testing

{% embed include file="src/examples/lite/test.t)

Deployment

Mojolicious::Guides::Cookbook

  • Apache/CGI

  • PSGI/Plack - any such server (without the real-time features of Mojo)

  • (Built in) Daemon

  • Morbo (Automatically reloads the server)

  • Hypnotoad Hot-code reloading production server

  • Hot deployment: Zero downtime software upgrades

hypnotoad project      (start or reload)
hypnotoad -s project   (stop)

Web Sockets

Web Sockets: Hello World

# Source: http://mojolicious.org/perldoc/Mojolicious/Guides/Tutorial#WebSockets
use Mojolicious::Lite;

websocket '/echo' => sub {
  my $c = shift;
  $c->on(json => sub {
    my ($c, $hash) = @_;
    $hash->{msg} = "echo: $hash->{msg}";
    $c->send({json => $hash});
  });
};

get '/' => 'index';

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Echo</title>
    <script>
      var ws = new WebSocket('<%= url_for('echo')->to_abs %>');
      ws.onmessage = function (event) {
        document.body.innerHTML += JSON.parse(event.data).msg;
      };
      ws.onopen = function (event) {
        ws.send(JSON.stringify({msg: 'I ♥ Mojolicious!'}));
      };
    </script>
  </head>
</html>

Web Sockets: Echo

use Mojolicious::Lite;

websocket '/echo' => sub {
  my $c = shift;
  $c->on(json => sub {
    my ($c, $hash) = @_;
    $hash->{msg} = "echo: $hash->{msg}";
    $c->send({json => $hash});
  });
};

get '/' => 'index';

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Echo</title>
    <input type="text" id="msg">
    <div id="output">
    </div>
    <script>
      var ws = new WebSocket('<%= url_for('echo')->to_abs %>');
      ws.onmessage = function (event) {
          var msg = JSON.parse(event.data).msg;
          console.log('Received', msg);
          document.getElementById('output').innerHTML += msg + '<br>';
      };
      //ws.onopen = function (event) {
      //};

      function send(e) {
          if (e.keyCode !== 13) {
              return false;
          }
          var msg = document.getElementById('msg').value;
          document.getElementById('msg').value = '';
          console.log('send', msg);
          ws.send(JSON.stringify({msg: msg}));
      }

      document.getElementById('msg').addEventListener('keypress', send);
      document.getElementById('msg').focus();
    </script>
  </head>
</html>

Web Sockets: Chat

use Mojolicious::Lite;
use DateTime;
use Data::Dumper qw(Dumper);
# Chrome seems to reach timeout after 15 sec
# Firefox seems to reach timeout after 35-40 sec
# When I enabled the heartbeat from JavaScript the timeout went down in FF to 15 sec


my %clients;

websocket '/chat' => sub {
    my $c = shift;
    my $tx_id = sprintf "%s", $c->tx;
    $clients{$tx_id} = { ws => $c->tx };

    $c->on(json => sub {
        my ($ws, $hash) = @_;
        $c->app->log->debug("From $ws received " . Dumper $hash);

        if ($hash->{heartbeat}) {
            $ws->send({json => {heartbeat => $hash->{heartbeat}}});
            return;
        }

        my $dt   = DateTime->now( time_zone => 'Asia/Tokyo');

        if ($hash->{login}) {
            $clients{$tx_id}{user_name} = $hash->{login};
            $ws->send({json => {login => 'ok'}});
            # TODO: notify everyone who joined the conversation
            foreach my $ws (keys %clients) {
                next if $ws eq $tx_id;
                $clients{$ws}{ws}->send({json => {
                    msg => $dt->hms . " $clients{$tx_id}{user_name} has joined the conversation"
                }});
            }

            return;
        }

        foreach my $ws (keys %clients) {
            my $msg = $dt->hms . ($ws eq $tx_id ? " $hash->{msg}" : " $clients{$tx_id}{user_name}: $hash->{msg}");
            $clients{$ws}{ws}->send({json => {
                msg => $msg,
            }});
        }

        return;
    });

    $c->on(finish => sub {
        my ($ws, $code, $reason) = @_;
        $c->app->log->debug( "Finished $ws Code $code reason: '" . ( $reason // '' ) .  "'");
        # code was 1006
        # reason was undef
        # TODO: notify everyone who left the conversation

        my $dt   = DateTime->now( time_zone => 'Asia/Tokyo');
        foreach my $ws (keys %clients) {
            next if $ws eq $tx_id;
            $clients{$ws}{ws}->send({json => {
                msg => $dt->hms . " $clients{$tx_id}{user_name} has left the conversation"
            }});
        }

        delete $clients{$ws};
        return;
    });
};

get '/' => 'index';

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Chat</title>
    <script>
        var ws;
        var user_name;
        var heartbeat_interval = null;
        function login(e) {
            if (e.keyCode !== 13) {
                  return false;
            }
            user_name = document.getElementById('name').value;
            //console.log(user_name);
            document.getElementById('login_name').innerHTML = user_name;
            document.getElementById('login').style.display = 'none';
            document.getElementById('chat').style.display = 'block';

            setup();
        }

        function setup() {
            ws = new WebSocket('<%= url_for('chat')->to_abs %>');

            ws.onmessage = function (event) {
                var data = JSON.parse(event.data);
                if (! data.msg) {
                    return;
                }
                console.log('Received', data.msg);
                document.getElementById('output').innerHTML = data.msg + '<br>' + document.getElementById('output').innerHTML;
            };

            ws.onopen = function (event) {
                ws.send(JSON.stringify({login: user_name}));

                // heartbeat: not based on http://django-websocket-redis.readthedocs.io/en/latest/heartbeats.html
                if (heartbeat_interval === null) {
                    heartbeat_interval = setInterval(function() {
                        ws.send(JSON.stringify({heartbeat: user_name}));
                    }, 14000);
                }
            };

            ws.onclose = function(){
                if (heartbeat_interval !== null) {
                    clearInterval(heartbeat_interval);
                }
                setTimeout(setup, 1000);
            };
        };

        function send(e) {
            if (e.keyCode !== 13) {
                  return false;
            }
            var msg = document.getElementById('msg').value;
            document.getElementById('msg').value = '';
            console.log('send', msg);
            ws.send(JSON.stringify({msg: msg}));
        }
        
        function onload() {
            //console.log('onload');
            document.getElementById('name').addEventListener('keypress', login);
            document.getElementById('msg').addEventListener('keypress', send);
            document.getElementById('msg').focus();
        }
    </script>

    <style>
      #chat {
        display: none;
      }
    </style>
  </head>
  <body onload="onload()">
    <div id="login">Your name: <input type="text" id="name"></div>
    <div id="chat">
        Logged in as <span id="login_name"></span><br>
        <input type="text" id="msg" placeholder="Your message">
        <div id="output"></div>
    </div>
  </body>
</html>