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_cors — middleware для добавления CORS заголовков

  • dia_static — отдает файлы на скачивание из заданной папки. Может использоваться как сервер статики

  • dia_router — позволяет задать middleware для определенных url и http методов. Самое то для реализации REST API

  • dia_body — разбирает http запрос и возвращает из него переданные параметры и загруженные файлы.

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

Роутер

Первый из них — 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}';   });    /// start server   app       .listen('localhost', 8080)       .then((_) => print('Started on http://localhost:8080')); }

Если запустить этот код и открыть в браузере ссылку http://localhost:8080/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')); }

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

  • ctx.query — параметры из URL вида ?param=value в Map<String,String>

  • ctx.parsed — параметры из тела запроса, будь то json, form-data или x-www-form-urlencoded в Map<String,dynamic>

  • ctx.files — загруженные файлы в Map<String,List<UploadedFile>> где String — имя параметра, UploadedFile — класс содержащий filename и File с загруженным файлом.

По дефолту, файлы загружаются во временную системную директорию, но это можно изменить используюя необязательный именованный параметр 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 лицензией. Жду ваших ишью и пуллреквестов!

ссылка на оригинал статьи https://habr.com/ru/post/550916/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *