强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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

特性MojoliciousDancer2
内置模板✅ (EP)✅ (TT)
WebSocket✅ 内置插件
非阻塞 I/O✅ 内置有限
文档质量优秀良好
社区活跃度
学习曲线中等
部署方式内置服务器/PSGIPSGI
推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐

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/PlackPerl Web 标准接口
Mojolicious全栈 Web 框架(推荐)
Dancer2轻量级 Web 框架
Starman/Gazelle生产级 PSGI 服务器
Nginx反向代理部署

练习

  1. 创建一个 Mojolicious::Lite 应用,实现 CRUD REST API
  2. 添加 JWT 认证中间件
  3. 创建带模板的 Web 页面(使用布局模板)
  4. 编写文件上传接口
  5. 使用 Dancer2 重写 Mojolicious 应用

扩展阅读