Mojolicious
Introduction
Other Mojolicious resources
- Glen Hinkle: Mojocast
- Glen Hinkle: Intro to Mojolicious
- Joel Berger: Introducing Mojolicious (source)
- Mojolicious web site: documentation and tutorial
- MojoExample on GitHub
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];
- dom returns a Mojo::DOM
- find returns a Mojo::Collection
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
-
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>