ХабрПостыНовостиМегапосты?

Dart на сервере

Недавно столкнулся с необходимостью написать REST API сервер на Dart. Оставим за рамками этой статьи почему и зачем это было надо, но первое с чем я столкнулся - выбор библиотек. Так уж сложилось, что я привык писать на NodeJS используя KoaJS в качестве веб сервера. Простая и удобная библиотека с кучей расширений для любой необходимости. А вот Dart в этом плане несколько подкачал. На момент поисков из "живых" пакетов на pub.dev был только shelf. Что-то отдаленно похожее, но по факту жутко неудобное. Неделю промучившись с оным, понял, надо писать свое, с блэкджеком... что-нибудь в стиле того же KoaJS

Знакомство

Знакомьтесь - Dia - легковесный и простой http сервер на Dart. Основная идея проекта: контекст http-запроса, который проходит очередь middleware которые его читают и меняют при необходимости.

Второй основополагающий принцип - минимализм. Только самое необходимое. Это не фреймворк, а именно пакет. Для любого расширяющего функционала - отдельный пакет. Это позволяет сократить размер кодовой базы проекта подключив только необходимые пакеты.

Собственно поэтому сам Dia почти ничего не умеет. Только создавать и прокидывать по очереди middleware контекст. Весь остальной функционал который необходим (например мне в моих проектах), реализован в отдельных пакетах которые мы рассмотрим чуть позже.

Практик

А сейчас приступим к практике. Устанавливается все стандартно и просто, добавлением в pubspec.yaml соответствующих строк:

dependencies:
  dia: ^0.0.7

Используется тоже просто. Вот минимальный пример:

import 'package:dia/dia.dart';

main() {
  /// Create instance of Dia
  final app = App();
  
  /// Add middleware
  app.use((ctx, next) async {
    /// write response on all query
    ctx.body = 'Hello world!';
  });

  /// Listen localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}

Контекст

Кто знаком с KoaJS поймет практически с лету. Для остальных поясню основные моменты. app.use - добавляет в очередь middleware. Это по сути, асинхронная функция, принимающая в качестве аргументов контекст и ссылку на следующую middleware. Контекст представляет собой класс предоставляющий быстрые методы доступа к полям ответа (код ответа, тело, заголовки) и дополнительные методы типа throwError - который позволяет сразу отправить HTTP ошибку в качестве ответа.

Контекст можно расширить своими полями и методами. Например добавить в него поле содержащее данные об авторизованном пользователе:

class MyContext extends Context{
  User? user;

  MyContext(HttpRequest request): super(request);
}


main() {
  /// Create instance of Dia
  final app = App<MyContext>();

  app.use((ctx, next) async {
    ctx.user = new User('test');
    await next();
  });

  app.use((ctx, next) async {
    if(ctx.user==null){
      ctx.trowError(401);
    }
  });
  
  /// Add middleware
  app.use((ctx, next) async {
    /// write response on all query
    ctx.body = 'Hello world!';
  });

  /// Listen localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}

Next

next - ссылка на следующее middleware в очереди. Когда middleware не возвращает окончательный результат, а только изменяет контекст, ему часто требуется дождаться завершения следующего middleware. Это необходимо, например, чтобы добавить логирование или обработку ошибок:

app.use((ctx,next) async {
  final start = DateTime.now();
  await next();
  final diff = DateTime.now().difference(start).inMicroseconds;
  print('${ctx.request.method} ${ctx.request.uri.path} $diff ms')
});

Готовые middleware

Как я уже говорил, весь дополнительный функционал должен быть реализован в отдельных пакетах. Некоторые из них уже опубликованы:

Рассмотрим два последних пакета более подробно, ибо с ними не все так просто.

Роутер

Первый из них - dia_router. Для его применения необходимо использовать контекст с миксином Routing.

class ContextWithRouting extends Context with Routing {
  ContextWithRouting(HttpRequest request) : super(request);
}

void main() {
  /// create Dia app with Routing mixin on Context
  final app = App<ContextWithRouting>();

  /// create router
  final router = Router<ContextWithRouting>('/route');

  /// add handler to GET request
  router.get('/data/:id', (ctx, next) async {
    ctx.body = '${ctx.params}';
  });
  
  app.use(router.middleware);

  /// start server
  app
      .listen('localhost', 8080)
      .then((_) => print('Started on http://localhost:8080'));
}

Если запустить этот код и открыть в браузере ссылку http://localhost:8080/route/data/12 то мы увидим{id: 12}.

Т.е мы не только задали специальный обработчик для фиксированного URL но и выдернули из него параметр с помощью регулярки. Кто знаком с npm пакетом koa-router оценит это удобство!

Парсер

Следующий пакет - dia_body. В запросе данные передают не только в пути но и еще кучей извращенных методов. Например пихают в body голый json или шлют form-data, а некоторые, так вообще передают данные в x-www-form-urlencoded. Мало того, есть еще и те кто шлет файлы как multipart/form-data! Вот, чтобы это все обработать нам и понадобится этот пакет.

Как вы наверное уже догадались, тут нам тоже необходим расширенный контекст с миксином ParsedBody:

class ContextWithBody extends Context with ParsedBody {
  ContextWithBody(HttpRequest request) : super(request);
}

void main() {
  final app = App<ContextWithBody>();

  app.use(body());

  app.use((ctx, next) async {
    ctx.body = ''' 
    query=${ctx.query}
    parsed=${ctx.parsed}
    files=${ctx.files}
    ''';
  });

  /// Start server listen on localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}

В результате мы увидим:

По дефолту, файлы загружаются во временную системную директорию, но это можно изменить используя необязательный именованный параметр uploadDirectory

QA

А как быть когда надо использовать оба эти пакета вместе? Да еще свои параметры в контекст добавить? Нет ничего проще! Именно для этого в dart и существуют миксины:

class CustomContext extends Context with Routing, ParsedBody {
  User? user;
  ContextWithBody(HttpRequest request) : super(request);
}

Ну а если я хочу запустить сервер в режиме SSL? Тоже все просто! Дело в том, что "под капотом" Dia использует обычный HttpServer из dart:io, так что Dia автоматически поддерживает все что поддерживает он. Например ctx.request из контекста - HttpRequest из dart:io. Так что можете использовать это при написании своих middleware. А вот так запускается сервер в режиме https:

const serverKey = 'cert/key.pem';
const certificateChain = 'cert/chain.pem';

final serverContext = SecurityContext();
  serverContext
      .useCertificateChainBytes(await File(certificateChain).readAsBytes());
  serverContext.usePrivateKey(serverKey, password: 'password');

final server = await app.listen(
      'localhost', 8444,
      securityContext: serverContext);

Итоги

Dia пакет новый и обкатан пока только на одном "боевом" проекте. Вероятно в нем есть баги и недоработки. Например, в коде надо навести порядок с документированием API, да еще много чего надо бы сделать. Поэтому и версия у проекта пока еще не релизная. Буду рад любой обратной связи и помощи.

Проект опубликован на GitHub под MIT лицензией. Жду ваших ишью и пуллреквестов!

@Cobalt
06.04.2021 11:02 UTC
Первоисточник

Комментарии

@usetester
06.04.2021 07:06 UTC
0
На первый взгляд весьма похоже на github.com/rknell/alfred
Есть какие-то принципиальные отличия?
06.04.2021 07:16 UTC
+2
Есть и довольно существенные. Alfred позиционирует себя как фремверк пропогандирующий позицию: чем меньше зависимостей, тем лучше. Dia же занимает кординально противоположенную позицию: не замусориваем проект лишним кодом. Поэтому Alfred включает в себя все возможные middleware а Dia выносит их в пакеты

Скажу больше, в Dart уже было несколько фремверков типа Angel, Aqueduct — почему-то все они со временем умирают. Может из-за сложности поддержки? Гораздо проще обновить либу содержащую меньше кода, или миддлвару перевести на нулсейфети самому чем весь фремверк переписывать.
@myemuk
06.04.2021 07:33 UTC
+1
Отлично, хорошие новости, особенно после закрытия Aqueduct.
@nikita_dol
06.04.2021 13:51 UTC
0
В статье и в репозитории (dia_router), код с примером не работает (404) или не компилируется
06.04.2021 13:54 UTC
0
Все работает: http://localhost:8080/route/subroute/data/33 для примера из репозитория pub.dev/packages/dia_router/example
06.04.2021 13:59 UTC
+1
В таком случае в примере не хватает этой строки
  app.use(router.middleware);


И ссылка должна быть не http://localhost:8080/data/12, а http://localhost:8080/route/data/12
06.04.2021 14:01 UTC
0
Да, все верно. Поправил в статье. Спасибо за замечание!
06.04.2021 14:07 UTC
+1
Пример из README.md
Картинка

06.04.2021 14:09 UTC
0
Да, как я и говорил, доку надо править. Необходимо добавить import 'dart:io'; и в 10 строке убрать dia.
@Mitai
07.04.2021 18:30 UTC
0
Shelf почему показался жутко не удобным? у него такая же архитектура, минималистичный, все что нужно подключаем тот же shelf-router. Я нубас и могу ошибаться но вы вроде как сделали ту же полку вид с боку
07.04.2021 18:33 UTC
0
Не такая. У shelf надо подключить сразу все middleware и только один итоговый handler в конце. Когда апи не очень развесистое — куда не шло еще. Но как только дерево апи увеличивается сильно — код становится плохочитаемым
@pretorean
07.04.2021 18:51 UTC
0
Отлично, спасибо.
Еще бы postgres)
07.04.2021 18:54 UTC
0
Сам страдаю от отсутствия нормально ORM типа sequelize. Использую пакет postgresql2 но там автор жестит, большинство ПР отклоняет, и ведет строгую линию которая только ему ясна.
@Asazio
07.04.2021 20:58 UTC
0
Как вообще впечатления от дарта на сервере?
Как работается с многопоточностью?
Что по поводу библиотек для бекенда?
Есть ли будущее?
07.04.2021 21:00 UTC
0
Впечатления вообщем-то положительные. Экосистема по сравнению с той же нодой пока не дотягивает, но почти все есть, либо можно найти замену.
Запускать в режиме многопоточности сервер тоже не проблема — есть изоляты «из коробки».
А судя по тому что на конец марта — начало апреля опубликовано аж три новых пакета для реализации http сервера, потребность у комьюнити в этом огромная, а стало быть будущее точно есть
14.09.2021 21:55 UTC
0

Какое-то будущее, безусловно есть. Я сам на PHP долгие годы писал, а сейчас ради интереса завел себе пет-проект на Dart, где есть телеграмм-бот и rest-сервер. В целом писать приятно, языком доволен и качеством его работы в продакшене, хотя до бенчмарков дело не дошло. Но вот по наличию типичных для той же пыхи библиотек дарт безнадёжно проигрывает. И, думаю. и будет проигрывать дальше, ведь фокус самих разработчиков языка сейчас направлен на фронтенд и мобилку, для сервера делаются самые минимальные вещи, многие библиотеки пишутся в комьюнити, им же потом и забрасываются в полуфабрикатном состоянии, когда задача автора решена и больше ему ничего не надо.

В целом, думаю, Dart имеет право на жизнь в продакшене на сервере, но в силу медленно развивающейся экосистемы его ниша будет крайне узка, по крайней мере ближайшие годы.