Perl 完全指南 / 第 17 章:Web 开发
第 17 章:Web 开发
“Perl 是 Web 的奠基者之一”
Perl 拥有成熟的 Web 开发生态。本章介绍 Mojolicious(最流行的 Perl Web 框架)、Dancer2 以及 PSGI 标准。
17.1 PSGI/Plack — Web 接口标准
PSGI(Perl Web Server Gateway Interface)类似 Python 的 WSGI,是 Perl Web 应用的标准接口。
# 最简单的 PSGI 应用
my $app = sub {
my ($env) = @_;
return [
200,
['Content-Type' => 'text/plain'],
['Hello, World!']
];
};
Plack 工具
cpanm Plack
# 运行 PSGI 应用
plackup app.psgi
# 使用指定服务器
plackup -s Starman -p 3000 app.psgi
# 开发模式(自动重载)
plackup -r app.psgi
| 服务器 | 说明 | 生产推荐 |
|---|---|---|
| Starman | 预分叉模型 | ⭐⭐⭐⭐⭐ |
| Twiggy | 非阻塞事件驱动 | ⭐⭐⭐⭐ |
| Gazelle | 高性能(XS) | ⭐⭐⭐⭐⭐ |
| Starlet | 轻量级 | ⭐⭐⭐⭐ |
| HTTP::Server::PSGI | 纯 Perl,最简单 | ⭐⭐(仅开发) |
17.2 Mojolicious — 全栈 Web 框架
安装与创建项目
cpanm Mojolicious
# 使用生成器创建项目
mojo generate app MyApp
cd my_app
# 启动开发服务器
morbo script/my_app
# 访问 http://localhost:3000
项目结构
my_app/
├── script/
│ └── my_app # 启动脚本
├── lib/
│ └── MyApp.pm # 应用入口
│ └── MyApp/
│ └── Controller/ # 控制器
│ └── Example.pm
├── templates/ # 模板
│ └── example/
├── public/ # 静态文件
└── t/ # 测试
基本路由
# lib/MyApp.pm
package MyApp;
use Mojo::Base 'Mojolicious', -signatures;
sub startup ($self) {
my $r = $self->routes;
# 基本路由
$r->get('/')->to('Example#welcome');
# 带参数的路由
$r->get('/user/:id')->to('User#show');
# 分组路由
my $api = $r->under('/api')->to('Auth#check');
$api->get('/users')->to('API#list_users');
$api->post('/users')->to('API#create_user');
}
1;
控制器
# lib/MyApp/Controller/User.pm
package MyApp::Controller::User;
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub show ($self) {
my $id = $self->param('id');
# 渲染 JSON
$self->render(json => {
id => $id,
name => "用户 $id",
});
}
sub create ($self) {
my $data = $self->req->json;
# 验证
unless ($data->{name} && $data->{email}) {
return $self->render(json => {error => "缺少参数"}, status => 400);
}
# 处理...
$self->render(json => {success => 1, id => 42});
}
1;
模板(Embedded Perl)
<%# templates/example/welcome.html.ep %>
% layout 'default';
% title '欢迎';
<h1>欢迎来到 MyApp</h1>
<p>当前时间: <%= localtime %></p>
<% if ($users && @$users) { %>
<table>
<tr><th>姓名</th><th>邮箱</th></tr>
% for my $user (@$users) {
<tr>
<td><%= $user->{name} %></td>
<td><%= $user->{email} %></td>
</tr>
% }
</table>
<% } else { %>
<p>暂无用户</p>
<% } %>
中间件(under)
# 认证中间件
my $auth = $r->under('/admin')->to(sub ($c) {
unless ($c->session('user')) {
$c->redirect_to('/login');
return 0;
}
return 1;
});
$auth->get('/')->to('Admin#dashboard');
$auth->get('/users')->to('Admin#users');
17.3 RESTful API 实战
#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
use Mojo::JSON qw(encode_json decode_json);
my @users;
my $next_id = 1;
# 列表
get '/api/users' => sub ($c) {
$c->render(json => {users => \@users});
};
# 查询
get '/api/users/:id' => sub ($c) {
my $id = $c->param('id');
my ($user) = grep { $_->{id} == $id } @users;
$user ? $c->render(json => $user) : $c->render(json => {error => "未找到"}, status => 404);
};
# 创建
post '/api/users' => sub ($c) {
my $data = $c->req->json;
return $c->render(json => {error => "name 必填"}, status => 400)
unless $data->{name};
my $user = { id => $next_id++, %$data };
push @users, $user;
$c->render(json => $user, status => 201);
};
# 更新
put '/api/users/:id' => sub ($c) {
my $id = $c->param('id');
my ($user) = grep { $_->{id} == $id } @users;
return $c->render(json => {error => "未找到"}, status => 404) unless $user;
my $data = $c->req->json;
@{$user}{keys %$data} = values %$data;
$c->render(json => $user);
};
# 删除
del '/api/users/:id' => sub ($c) {
my $id = $c->param('id');
my $before = scalar @users;
@users = grep { $_->{id} != $id } @users;
scalar @users < $before
? $c->render(json => {success => 1})
: $c->render(json => {error => "未找到"}, status => 404);
};
app->start;
17.4 Dancer2 — 轻量 Web 框架
#!/usr/bin/env perl
use Dancer2;
get '/' => sub {
return template 'index' => { title => 'Dancer2 App' };
};
get '/hello/:name' => sub {
my $name = route_parameters->get('name');
return "Hello, $name!";
};
post '/api/data' => sub {
my $data = request->body_parameters;
return to_json({ received => $data });
};
dance;
Mojolicious vs Dancer2
| 特性 | Mojolicious | Dancer2 |
|---|---|---|
| 内置模板 | ✅ (EP) | ✅ (TT) |
| WebSocket | ✅ 内置 | 插件 |
| 非阻塞 I/O | ✅ 内置 | 有限 |
| 文档质量 | 优秀 | 良好 |
| 社区活跃度 | 高 | 中 |
| 学习曲线 | 中等 | 低 |
| 部署方式 | 内置服务器/PSGI | PSGI |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
17.5 部署
Nginx 反向代理
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static/ {
alias /var/www/myapp/public/;
expires 30d;
}
}
systemd 服务
# /etc/systemd/system/myapp.service
[Unit]
Description=My Perl Web App
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/plackup -s Starman -p 3000 -E production app.psgi
Restart=always
[Install]
WantedBy=multi-user.target
17.6 业务场景:文件上传服务
#!/usr/bin/env perl
use Mojolicious::Lite -signatures;
use Path::Tiny;
my $upload_dir = path("uploads");
$upload_dir->mkpath;
post '/upload' => sub ($c) {
my $file = $c->req->upload('file');
return $c->render(json => {error => "无文件"}, status => 400) unless $file;
my $filename = $file->filename;
$filename =~ s/[^a-zA-Z0-9._-]/_/g; # 安全化文件名
$file->move_to($upload_dir->child($filename)->stringify);
$c->render(json => {
success => 1,
filename => $filename,
size => $file->size,
});
};
app->start;
本章小结
| 要点 | 内容 |
|---|---|
| PSGI/Plack | Perl Web 标准接口 |
| Mojolicious | 全栈 Web 框架(推荐) |
| Dancer2 | 轻量级 Web 框架 |
| Starman/Gazelle | 生产级 PSGI 服务器 |
| Nginx | 反向代理部署 |
练习
- 创建一个 Mojolicious::Lite 应用,实现 CRUD REST API
- 添加 JWT 认证中间件
- 创建带模板的 Web 页面(使用布局模板)
- 编写文件上传接口
- 使用 Dancer2 重写 Mojolicious 应用