- PVSM.RU - https://www.pvsm.ru -
Доброго времени суток!
Если Вам хотелось разделить своё приложение на сервер и клиент, если Вы хотите добавить api к своему vibe сайту или если Вам просто нечего делать
Эти ситуации мало чем отличаются, поэтому сначала мы рассмотрим простой случай:
module model;
import std.math;
struct Point { float x, y; }
float sqr(float v) { return v * v; }
float dist()(auto ref const(Point) a, auto ref const(Point) b)
{
return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
}
class Model
{
float triangleAreaByLengths(float a, float b, float c)
{
auto p = (a + b + c) / 2;
return sqrt(p * (p - a) * (p - b) * (p - c));
}
float triangleAreaByPoints(Point a, Point b, Point c)
{
auto ab = dist(a, b);
auto ac = dist(a, c);
auto bc = dist(b, c);
return triangleAreaByLengths(ab, ac, bc);
}
}
import std.stdio;
import model;
void main()
{
auto a = Point(1, 2);
auto b = Point(3, 4);
auto c = Point(4, 1);
auto m = new Model;
writeln(m.triangleAreaByPoints(a, b, c));
}
Итак, что нам нужно сделать, чтобы из одного обычного приложения сделать 2 — rest-сервер и тонкого клиента:
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя vibe.data.json. Это умеют все встроенные типы данны и прострые структуры (без private полей). Для реализации [де]сериализации можно объявить 2 метода static MyType frontJson(Json data)
и Json toJson() const
, где описывается процесс перевода сложных структур в Json тип, пример [1].
Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающий экземпляр класса, реализующего возвращаемый интерфейс объект, не должен принимать аргументов. Тут объяснить можно лишь одним: для регистрации rest-интерфейса используется экземпляр, а если функция принимает аргументы, то, возможно, с аргументами, имеющими init-значения создать экземпляр нельзя, а создать как-то надо для регистрации вложенного интерфейса.
Итак выделим интерфейс:
interface IModel
{
@method(HTTPMethod.GET)
float triangleAreaByLengths(float a, float b, float c);
@method(HTTPMethod.GET)
float triangleAreaByPoints(Point a, Point b, Point c);
}
class Model : IModel
{
...
}
Декораторы @method(HTTPMethod.GET)
необходимы для построения роутинга. Так же есть способ обойтись без них — использовать соглашение именования методов (префиксы):
get
,query
— GET
методset
, put
— PUT
add
, create
, post
— POST
remove
, erase
, delete
— DELETE
update
, patch
— PATCH
Код сервера будет по классике vibe записан в статическом конструкторе модуля:
shared static this()
{
auto router = new URLRouter;
router.registerRestInterface(new Model); // создаём конкретную реализацию модели
auto set = new HTTPServerSettings;
set.port = 8080;
set.bindAddresses = ["127.0.0.1"];
listenHTTP(set, router);
}
И наконец изменения в коде, использующем модель:
...
auto m = new RestInterfaceClient!IModel("http://127.0.0.1:8080/"); // тут мы уже используем интерфейс модели
...
Фреймворк сам реализует обращения к серверу и [де]сериализацию типов данных.
В итоге мы разделили приложение на сервер и клиент минимально изменив существующий код!
Кстати говоря, выброшенные исключения пробрасываются vibe'ом в клиентское приложение, к сожалению, без сохранения типа исключения.
Рассмотрим более сложный случай — в модели имеются методы, возвращающие массивы несериализуемых объетов (классов). Тут без изменения существующего кода, к сожалению, не обойтись. Реализуем такую ситуацию в нашем примере.
Будем возвращать разные агрегаторы точек:
interface IPointCalculator
{
struct CollectionIndices { string _name; } // необходимая структура для реализации коллекции
@method(HTTPMethod.GET)
Point calc(string _name, Point[] points...);
}
interface IModel
{
...
@method(HTTPMethod.GET)
Collection!IPointCalculator calculator();
}
class PointCalculator : IPointCalculator
{
Point calc(string _name, Point[] points...)
{
import std.algorithm;
if (_name == "center")
{
auto n = points.length;
float cx = points.map!"a.x".sum / n;
float cy = points.map!"a.y".sum / n;
return Point(cx, cy);
}
else if (_name == "left")
return points.fold!((a,b)=>a.x<b.x?a:b);
else
throw new Exception("Unknown calculator '" ~ _name ~ "'");
}
}
class Model : IModel
{
PointCalculator m_pcalc;
this() { m_pcalc = new PointCalculator; }
...
Collection!IPointCalculator calculator() { return Collection!IPointCalculator(m_pcalc); }
}
По сути IPointCalculator
это не элемент коллекции, а сама коллекция и структура CollectionIndices
как раз указывает на наличие индексов, используемых для получения элементов этой коллекции. Нижнее подчёркивание перед _name
обуславливает формат запроса к методу calc
как к calculator/:name/calc
, где :name
потом передаётся первым параметром в метод, а CollectionIndices
позволяет такой запрос построить при реализации интерфейса с помощью new RestInterfaceClient!IModel
.
Используется это так:
...
writeln(m.calculator["center"].calc(a, b, c));
...
Если возвращаемый тип сменить с Collection!IPointCalculator
на IPointCalculator
то мало что поменяется:
...
writeln(m.calculator.calc("center", a, b, c));
...
При этом формат запроса останется прежним. Не совсем понятна роль Collection
в этой комбинации.
На закуску реализуем web версию нашего клиента. Для этого нужно:
Шаблонизатор diet, используемый в vibe, очень похож на jade [2]
html
head
title Пример REST
style.
.label { display: inline-block; width: 20px; }
input { width: 100px; }
script(src = "model.js")
script.
function getPoints() {
var ax = parseFloat(document.getElementById('ax').value);
var ay = parseFloat(document.getElementById('ay').value);
var bx = parseFloat(document.getElementById('bx').value);
var by = parseFloat(document.getElementById('by').value);
var cx = parseFloat(document.getElementById('cx').value);
var cy = parseFloat(document.getElementById('cy').value);
return [{x:ax, y:ay}, {x:bx, y:by}, {x:cx, y:cy}];
}
function calcTriangleArea() {
var p = getPoints();
IModel.triangleAreaByPoints(p[0], p[1], p[2], function(r) {
document.getElementById('area').innerHTML = r;
});
}
body
h1 Расчёт площади треугольника
div
div.label A:
input#ax(placehoder="a.x",value="1")
input#ay(placehoder="a.y",value="2")
div
div.label B:
input#bx(placehoder="b.x",value="2")
input#by(placehoder="b.y",value="1")
div
div.label C:
input#cx(placehoder="c.x",value="0")
input#cy(placehoder="c.y",value="0")
div
button(onclick="calcTriangleArea()") Расчитать
p Площадь:
span#area
Выглядит, конечно, так себе, но для примера норм
Изменения в коде сервера:
...
auto restset = new RestInterfaceSettings;
restset.baseURL = URL("http://127.0.0.1:8080/");
router.get("/model.js", serveRestJSClient!IModel(restset));
router.get("/", staticTemplate!"index.dt");
...
Как мы можем заметить vibe за нас генерирует js код для обращения к нашему api.
В заключение можно отметить, что на данном этапе есть некоторые шероховатости, например неправильная генерация js кода для всех возвращаемых интерфейсов (забыли добавить this.
для этих полей в js объекте) и для коллекций в частности (неправильная генерация url — :name
ни на что не заменяется). Но эти хероховатости легко поправимы, думаю их исправят в ближайшем будущем [3].
На этом всё! Код примера можно скачать на github [4].
Автор: deviator
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/189110
Ссылки в тексте:
[1] пример: https://github.com/rejectedsoftware/vibe.d/blob/master/examples/serialization/source/app.d#L65
[2] jade: https://naltatis.github.io/jade-syntax-docs/
[3] будущем: https://github.com/rejectedsoftware/vibe.d/pull/1566
[4] github: https://github.com/deviator/restexample
[5] группа в ВК: https://vk.com/vk_dlang
[6] группа в Telegram: https://telegram.me/dlangru
[7] Источник: https://habrahabr.ru/post/310268/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.