[API, Dart] Dart на сервере

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
06-Апр-2021 12:31


Недавно столкнулся с необходимостью написать 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'));
}
Nextnext - ссылка на следующее 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 с загруженным файлом.
По дефолту, файлы загружаются во временную системную директорию, но это можно изменить используюя необязательный именованный параметр uploadDirectoryQAА как быть когда надо использовать оба эти пакета вместе? Да еще свои параметры в контекст добавить? Нет ничего проще! Именно для этого в 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 лицензией. Жду ваших ишью и пуллреквестов!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_api, #_dart, #_dart, #_httpserver (http-сервер), #_koa, #_webserver, #_api, #_dart
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:02
Часовой пояс: UTC + 5