Выпуск 3. Май 2013
← Введение в Perl XS | Содержание | Обзор CPAN за апрель 2013 г. →Введение в разработку web-приложений на PSGI/Plack. Часть 2.
Продолжение статьи о PSGI/Plack. Рассмотрены более подробно Plack::Builder, а также Plack::Middleware.
В прошлой статье мы рассмотрели спецификацию PSGI
, как она появилась, почему ей стоит пользоваться. Рассмотрели Plack
— реализацию PSGI
, основные его компоненты и написали простейшее API, которое выполняло поставленные перед ним задачи, вскользь рассмотрели основные PSGI
сервера.
Во второй части статьи мы рассмотрим следующие моменты:
Plack::Builder
— мощный маршрутизатор и не только.Plack::Middleware
— расширяем наши возможности при помощи «прослоек».
Мы по-прежнему используем Starman
, который является preforking сервером (использует модель предварительно запущенных процессов).
Более пристальный взгляд на Plack::Builder
В предыдущей статье мы вкратце рассмотрели Plack::Builder
. Теперь пришло время рассмотреть его более подробно. Решение рассматривать Plack::Builder
вместе с Plack::Middleware
весьма логично, потому что они очень тесно взаимосвязаны. Рассматривая в разных статьях эти два компонента, обе статьи содержали бы перекрестные ссылки друг на друга, что не очень удобно в формате журнала.
Базовая конструкция Plack::Builder
выглядит так:
builder {
mount '/foo' => builder { $bar };
}
Эта конструкция указывает нам на то, что по адресу /foo
будет располагаться PSGI
-приложение ($bar
). То, что мы обернули в builder
должно быть обязательно ссылкой на функцию, иначе можем получить ошибку следующего вида:
Can't use string ("stupid string") as a subroutine ref while "strict refs" in use at /usr/local/share/perl/5.14.2/Plack/App/URLMap.pm line 71.
Маршруты могут быть вложенными, например:
builder {
mount '/foo' => builder {
mount '/bar' => builder { $bar; };
mount '/baz' => builder { $baz; };
mount '/' => builder { $foo; };
};
};
Эта запись означает, что по адресу /foo
будет располагаться приложение $foo
, по адресу /foo/bar
приложение $bar
, а по адресу /foo/baz
приложение $baz
соответственно.
Однако, никто не мешает записать предыдущую запись в следующем виде:
builder {
mount '/foo/bar' => builder { $bar };
mount '/foo/baz' => builder { $baz };
mount '/foo/' => builder { $foo };
};
Обе записи эквивалентны и выполняют одну и ту же задачу, но первая выглядит проще и понятнее. Plack::Builder
можно использовать в объектно-ориентированном стиле, но лично мне удобнее использовать его в процедурном виде. Применение Plack::Builder
в объектно-ориентированном виде выглядит так:
my $builder = Plack::Builder->new;
$builder->mount('/foo' => $foo_app);
$builder->mount('/' => $root_app);
$builder->to_app;
Эта запись эквивалентна:
builder {
mount '/foo' => builder { $app; };
mount '/' => builder { $app2; };
};
Какой из способов использовать — дело сугубо индивидуальное. Мы вернемся к рассмотрению Plack::Builder
после ознакомления с Plack::Middleware
.
Plack::Middleware
Plack::Middleware
— это базовый класс для написания, как говорит нам CPAN, «простых в использовании PSGI
-прослоек». Для чего это нужно? Рассмотрим на примере реализации некоего API.
Представим, что код нашего приложения выглядит так:
my $api_app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $params = $req->parameters();
if ($params->{string} && $params->{string} eq 'data') {
$res->body('ok');
}
else {
$res->body('not ok');
}
return $res->finalize();
};
my $main_app = builder {
mount "/" => builder { $api_app };
}
Это приложение отлично работает, но теперь представим, что вдруг понадобилось принимать данные только в том случае, если они передаются методом POST.
Тривиальное решение — привести наше приложение к следующему виду:
my $api_app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $params = $req->parameters();
if ($req->method() ne 'POST') {
$res->status(403);
$res->body('Method not allowed');
return $res->finalize();
}
if ($params->{string} && $params->{string} eq 'data') {
$res->body('ok');
}
else {
$res->body('not ok');
}
return $res->finalize();
};
Понадобилось всего лишь 4 строки, чтобы решить проблему. А теперь представим, что понадобилось сделать еще одно приложение, которое тоже должно принимать данные отправленные только методом POST. Что будем делать? Писать в каждом это условие? Это не вариант по нескольким причинам:
- Увеличивается объем кода, а как следствие его энтропия (простое лучше, чем сложное).
- Больше вероятность сделать ошибку (человеческий фактор).
- Если мы передадим проект другому программисту, он может забыть и сделать что-то не так (человеческий фактор).
Итак, сформулируем проблему. Мы не можем сделать так, чтобы все наши приложения одновременно приобрели определенные свойства не изменяя их код. Или можем?
Механизм Middleware
отлично подходит для предоставления сквозного функционала всему приложению. Стоит, конечно, чувствовать меру и добавлять только действительно необходимый всей программе код.
Для того, чтобы построить свое Middleware
(свою прослойку, другими словами), необходимо добиться исполнения следующих условий:
- Находиться в пакете
Plack::Middleware::MYCOOLMIDDLEWARE
, гдеMYCOOLMIDDLEWARE
название вашегоMiddleware
. - Расширять базовый класс
Plack::Middleware
(use parent qw/Plack::Middleware/;
). - Реализовывать метод (функцию)
call
.
Итак, реализуем простейшее Middleware
учитывая все вышеперечисленное:
package Plack::Middleware::PostOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
if ($req->method() ne 'POST') {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
return $self->app->($env);
}
Рассмотрим подробнее то, что получилось. Есть код, который находится в пакете Plack::Middleware
(1 пункт), который наследует базовый класс Plack::Middleware
(2 пункт), реализовывает метод call
(3 пункт).
Представленная реализация call
делает следующее:
- Принимает в качестве параметров экземпляр
Plack::Middleware
иenv
(my ($self, $env) = @_;
). - Создает запрос, который принимает приложение (создание аналогично тому, которое использовалась в предыдущих примерах).
- Проверяет, не является ли метод запроса POST, если является, то
Middleware
пропускает обработку запроса дальше.
Рассмотрим, что происходит, если метод запроса не является POST.
Если метод не является POST создается новый объект Plack::Response
и сразу же возвращается, не вызывая приложение.
Вообще функция call
в Middleware
может делать ровно 2 действия. Это:
- Обработка
env
ПЕРЕД выполнением приложения. - Обработка результата ПОСЛЕ выполнения приложения.
Это будет проиллюстрировано в конце статьи, когда мы будем подводить итоги и разбираться в нюансах.
Совместное использование Plack::Middleware и Plack::Builder
Есть готовая прослойка Plack::Middleware::PostOnly
, у нас есть PSGI прилжение, у нас есть проблема. Напоминаю, проблема выглядит так: “На данный момент мы не можем глобально влиять на поведение приложений”. Теперь можем. Рассмотрим самый важный момент Plack::Builder — ключевое слово enable.
Ключевое слово enable
позволяет подключать Plack::Middleware
к приложению. Делается это следующим образом:
my $main_app = builder {
enable "PostOnly";
mount "/" => builder { $api_app; };
}
Это очень простой и очень мощный механизм одновременно. Объединим весь код в одном месте и посмотрим на результат.
PSGI приложение:
use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
use Plack::Middleware::PostOnly;
my $api_app = sub {
my $env = shift;
warn 'WORKS';
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $params = $req->parameters();
if ($params->{string} && $params->{string} eq 'data') {
$res->body('ok');
}
else {
$res->body('not ok');
}
return $res->finalize();
};
my $main_app = builder {
enable "PostOnly";
mount "/" => builder { $api_app };
}
Middleware:
package Plack::Middleware::PostOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
if ($req->method() ne 'POST') {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
return $self->app->($env);
}
Приложение запускается следующей командой:
/usr/bin/starman --port 8080 app.psgi
В коде использовалось enable "PostOnly"
потому, что Plack::Builder
автоматически подставляет к имени пакета Plack::Middleware
. Пишется enable "PostOnly"
, имеется в виду enable "Plack::Middleware::PostOnly"
(указать полный путь к своему классу можно используя в качестве префикса +
, например, enable "+MyApp::Middleware::PostOnly";
– прим. редактора).
Теперь, если обратиться по адресу http://localhost:8080/
при помощи метода GET, то получим сообщение о том, что Method not allowed с кодом ответа 405, тогда как при обращении методом POST все будет нормально.
Не зря в коде приложения присутствует строка warn "WORKS"
. Она подтверждает отсутствие выполнения приложения, если метод не является POST. Попробуйте отправить GET, вы не увидите этого сообщения в STDERR starman
.
У PSGI
-серверов есть еще довольно много интересных особенностей поведения, они обязательно будут рассмотрены в следующих статьях.
Рассмотрим еще несколько полезных моментов Plack::Middleware
, а именно:
- Обработка результатов ПОСЛЕ выполнения приложения.
- Передача параметров в
Middleware
.
Допустим, есть два PSGI
-приложения и необходимо сделать так, чтобы одно работало через POST, а другое только через GET. Можно решить проблему в лоб, написав еще одно Middleware
, которое будет отвечать только на метод GET, например, так:
package Plack::Middleware::GetOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
if ($req->method() ne 'GET') {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
return $self->app->($env);
}
Задача решена, однако остается много дублирования.
Решение этой проблемы как нельзя лучше поможет разобраться со следующими вещами:
- Механизмы передачи переменных в
Middleware
. - Подключение
Middleware
для приложений индивидуально.
Решение проблемы следующее: передавать желаемый метод в качестве переменной. Вернемся к рассмотрению enable
из Plack::Builder
. Оказывается, enable
умеет принимать переменные. Выглядит это следующим образом:
my $main_app = builder {
enable "Foo", one => 'two', three => 'four';
mount "/" => builder { $api_app };
}
В самом Middleware
к этим переменным можно добраться напрямую через $self
. Например, для того чтобы получить значение, переданное переменной one, необходимо обратиться к $self->{one}
в коде Middleware
. Продолжаем изменять PostOnly
.
Пример:
package Plack::Middleware::GetOnly;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
warn $self->{one} if $self->{one};
if ($req->method() ne 'GET') {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
return $self->app->($env);
}
Перезапускаем starman, делаем запрос на localhost:8080
, в STDERR видим следующее:
two at /home/noxx/perl/lib/Plack/Middleware/PostOnly.pm line 12.
Так передаются переменные в Plack::Middleware
.
Используя данный механизм, напишем Middleware
, которое теперь будет называться Only
.
package Plack::Middleware::Only;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
my $method = $self->{method};
$method ||= 'ANY';
if ($method ne 'ANY' && $req->method() ne $method) {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
return $self->app->($env);
}
1;
Теперь Middleware
умеет отвечать только на переданный в параметрах метод запроса. Немного изменившееся подключение выглядит так:
my $main_app = builder {
enable "Only", method => 'POST';
mount "/" => builder { $api_app };
};
В данном случае приложение будет исполняться только в том случае, если метод запроса был POST.
Рассмотрим обработку результатов ПОСЛЕ выполнения приложения. Допустим, необходимо чтобы в случае, если метод разрешен, к телу ответа добавлялось слово “ALLOWED”.
То есть, если приложение должно отдавать ok, оно отдаст ok ALLOWED, если, конечно, запрос будет выполнен с допустимым методом.
Приведем Only.pm
к следующему виду:
package Plack::Middleware::Only;
use strict;
use warnings;
use parent qw/Plack::Middleware/;
use Plack;
use Plack::Response;
use Plack::Request;
sub call {
my ($self, $env) = @_;
my $req = Plack::Request->new($env);
my $method = $self->{method};
$method ||= 'ANY';
if ($method ne 'ANY' && $req->method() ne $method) {
my $new_res = $req->new_response(405);
$new_res->body('Method not allowed');
return $new_res->finalize();
}
my $plack_res = $self->app->($env);
$plack_res->[2]->[0] .= 'ALLOWED';
return $plack_res;
}
1;
$self->app->($env)
возвращает ссылку на массив из трех элементов (PSGI
-спецификация), тело которого модифицируется и отдается в качестве ответа.
Убедится, что это все работает и работает так как надо, можно передав разрешенным методом переменную string=data
и string=data1
. В первом случае, если метод разрешен, ответ будет выглядеть “okALLOWED”, во втором “not okALLOWED”.
И в заключение рассмотрим, как именно можно объединить все вышеперечисленное в одно Plack
-приложение. Возвращаемся к первоначальной задаче. Необходимо разработать простейшее API, которое принимает переменную string
и если string=data
ответить ok, иначе not ok, а также соблюдать следующие правила:
- При обращении по адресу
/
отвечать на любой метод. - При обращении по адресу
/post
отвечать только на метод POST. - При обращении по адресу
/get
отвечать только на метод GET.
На самом деле, понадобится ровно одно приложение, которое написано — $api_app
и немного модифицированный builder
.
В результате, используя все вышеописанное, должно получиться приложение следующего вида:
use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
use Plack::Middleware::PostOnly;
use Plack::Middleware::Only;
my $api_app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
my $params = $req->parameters();
warn 'RUN!';
if ($params->{string} && $params->{string} eq 'data') {
$res->body('ok');
}
else {
$res->body('not ok');
}
return $res->finalize();
};
my $main_app = builder {
mount "/" => builder {
mount "/post" => builder {
enable "Only", method => 'POST';
$api_app;
};
mount "/get" => builder {
enable "Only", method => 'GET';
$api_app;
};
mount "/" => builder {
$api_app;
};
};
};
Таким образом, подключение Middleware
работает во вложенных маршрутах Plack::Builder
. Стоит обратить внимание на простоту и логичность кода.
Отложенный ответ будет рассмотрен в одной из статей посвященных асинхронным серверам (Twiggy
, Corona
, Feersum
).
← Введение в Perl XS | Содержание | Обзор CPAN за апрель 2013 г. →