Доброго времени суток, Хабр! Если Вам хотелось разделить своё приложение на сервер и клиент, если Вы хотите добавить API к своему vibe-сайту или если Вам просто нечего делать.
Эти ситуации мало чем отличаются, поэтому сначала мы рассмотрим простой случай:
Итак, что нам нужно сделать, чтобы из одного обычного приложения сделать 2 — rest-сервер и тонкого клиента:
Итак выделим интерфейс:
Декораторы
Код сервера будет по классике vibe записан в статическом конструкторе модуля:
И наконец изменения в коде, использующем модель:
Фреймворк сам реализует обращения к серверу и [де]сериализацию типов данных.
В итоге мы разделили приложение на сервер и клиент минимально изменив существующий код! Кстати говоря, выброшенные исключения пробрасываются vibe'ом в клиентское приложение, к сожалению, без сохранения типа исключения.
Рассмотрим более сложный случай — в модели имеются методы, возвращающие массивы несериализуемых объетов (классов). Тут без изменения существующего кода, к сожалению, не обойтись. Реализуем такую ситуацию в нашем примере.
Будем возвращать разные агрегаторы точек:
По сути
Используется это так:
Если возвращаемый тип сменить с
При этом формат запроса останется прежним. Не совсем понятна роль
На закуску реализуем web версию нашего клиента. Для этого нужно:
Шаблонизатор diet, используемый в vibe, очень похож на jade:
Выглядит, конечно, так себе, но для примера норм:
![](https://habrastorage.org/r/w1560/files/bc2/ea3/dca/bc2ea3dcaced4350a35555957194ae4b.png)
Изменения в коде сервера:
Как мы можем заметить, vibe за нас генерирует js-код для обращения к нашему API.
В заключение можно отметить, что на данном этапе есть некоторые шероховатости, например неправильная генерация js кода для всех возвращаемых интерфейсов (забыли добавить
На этом всё! Код примера можно скачать на github.
Эти ситуации мало чем отличаются, поэтому сначала мы рассмотрим простой случай:
- Есть какая-то модель:
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-сервер и тонкого клиента:
- Выделить интерфейс модели;
- Создать код сервера;
- Вместо настоящей модели создать rest-реализацию.
Скучные, но важные моменты
Cначала немного о модели. На момент написания vibe-d-0.7.30-beta.1 не поддерживал перегрузку функций (вообще), что, отчасти, логично, так как мы бы пытались вызвать метод не имея точной информации об аргументах, ибо мы передаём их по сети, vibe даже не знал бы к какому типу их приводить — нужно было бы выяснять это перебором, но тут есть тонкие моменты («5» можно привести и к int и к float, например).
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя vibe.data.json. Это умеют все встроенные типы данны и прострые структуры (без private полей). Для реализации [де]сериализации можно объявить 2 метода
Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающийэкземпляр класса, реализующего возвращаемый интерфейс объект, не должен принимать аргументов. Тут объяснить можно лишь одним: для регистрации rest-интерфейса используется экземпляр, а если функция принимает аргументы, то, возможно, с аргументами, имеющими init-значения создать экземпляр нельзя, а создать как-то надо для регистрации вложенного интерфейса.
Помимо этого аргументы и возвращаемые данные методов должны уметь [де]сериализовываться используя vibe.data.json. Это умеют все встроенные типы данны и прострые структуры (без private полей). Для реализации [де]сериализации можно объявить 2 метода
static MyType fromJson(Json data)
и Json toJson() const
, где описывается процесс перевода сложных структур в Json тип, пример.Это не касается возвращаемых интерфейсов, они так же работают через передачу аргументов по сети, но есть другой момент: метод, возвращающий
Итак выделим интерфейс:
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 версию нашего клиента. Для этого нужно:
- Создать html-страницу с js-кодом, использующим наш rest API;
- Немного добавить кода в серверную часть.
Шаблонизатор diet, используемый в vibe, очень похож на jade:
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
Выглядит, конечно, так себе, но для примера норм:
![](https://habrastorage.org/files/bc2/ea3/dca/bc2ea3dcaced4350a35555957194ae4b.png)
Изменения в коде сервера:
...
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
ни на что не заменяется). Но эти шероховатости легко поправимы, думаю их исправят в ближайшем будущем.На этом всё! Код примера можно скачать на github.