Стоит ли писать код Dart на стороне сервера?

Недавно мы выпустили Dropzone Plus  —  сервис, который работает с загрузкой HTML-форм. Большая его часть написана на Dart. И в последнее время многих заинтересовала настройка сервера при создании веб-приложения на Flutter, так что сегодня расскажем об этом.

Как выглядит Dart на серверной стороне?

Код Dart на стороне сервера очень похож на node. Вот простейший код для запуска HTTP-сервера на Dart:

import 'dart:io';

main() async {
final server = await HttpServer.bind(InternetAddress.anyIPv6, 80);

server.listen((HttpRequest request) {
request.response.write('Hello, world!');
request.response.close();
});
}

Конечно, приложение целиком так никто писать не станет.

Следующий шаг  —  использование пакета shelfshelf_router и shelf_static), поддерживаемого командой Dart и очень похожего на express в мире JavaScript.

Shelf  —  отличное решение для небольшого REST API. С таким подходом вы далеко продвинетесь.

Вот хороший пример такого сервера.


Чтобы написать что-то покрупнее и посложнее, нужен реальный фреймворк для API.

Фреймворк gRPC

gRPC  —  это фреймворк от Google, позволяющий описывать API с помощью буферов протокола (о них  —  позже) и генерировать серверный и клиентский код для любого языка.

RPC расшифровывается как Remote Procedure Call («Удаленный вызов процедур»). Проще говоря, функция (процедура) вызывается как локальная, но выполняется на сервере, и не нужно думать о деталях взаимодействия.

Принцип работы gRPC вкратце:

В файлах .proto определяются сервисы (конечные точки API) и сообщения, отправляемые между клиентом и сервером:

syntax = "proto3";

package dropzone.public_account;

import "shared.proto";

// Очень упрощенная версия сервиса учетной записи.
service AccountService {
rpc SendEmailVerification(VerificationRequest) returns(shared.Empty);
rpc SignInWithPassword(PasswordSignInRequest) returns(User);
rpc ChangePassword(ChangePasswordRequest) returns(shared.Empty);
}

message VerificationRequest {
string email = 1;
string name = 2;
}

enum UserStatus { DISABLED = 0; ACTIVE = 1; }

message User {
string id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
}

message PasswordSignInRequest {
string email = 1;
string password = 2;
}

message ChangePasswordRequest {
string oldPassword = 1;
string newPassword = 2;
}

Затем это определение используется для создания клиентских библиотек и реализации сервера-заглушки на любом языке.

Dart и gRPC

К счастью, есть поддерживаемая Google реализация на Dart фреймворка gRPC. Создав клиентские библиотеки, взаимодействуем с сервером:

/// Канал взаимодействия библиотеки gRPC
/// предоставляется библиотекой gRPC.
final channel = GrpcWebClientChannel.xhr(Uri.parse('https://your.api.url:8080'));

/// Класс [AccountServiceClient] генерируется библиотекой gRPC из
/// определения .proto.
final client = AccountServiceClient(channel);

Future<void> changePassword() async {
/// Сообщение, отправляемое в API. Тоже генерируется из
/// определения .proto.
final request =
ChangePasswordRequest(oldPassword: 'old-pass', newPassword: 'new-pass');

/// Просто вызываем метод .changePassword() на клиенте в виде
/// локального вызова и асинхронно получаем ответ от сервера.
///
/// В этом случае вызов ничего не возвращает.
await client.changePassword(request);
}

Перейдем к серверу. Библиотека gRPC создает абстрактные классы. Мы расширяем их, чтобы реализовать логику сервера: просто включаем сгенерированную AccountServiceBase и расширяем ее до class AccountService extends AccountServiceBase {}.

Затем получаем предупреждение, что в этом классе нет нужных переопределений (реализаций метода):

Выбрав быстрое исправление Create missing override(s) («Создать отсутствующие переопределения»), Dart создаст весь класс:

class AccountService extends AccountServiceBase {
@override
Future<Empty> sendEmailVerification(ServiceCall call, VerificationRequest request) {
// TODO (ЗАДАЧА): реализовать sendEmailVerification
throw UnimplementedError();
}

@override
Future<User> signInWithPassword(ServiceCall call, PasswordSignInRequest request) {
// TODO (ЗАДАЧА): реализовать signInWithPassword
throw UnimplementedError();
}

@override
Future<Empty> changePassword(ServiceCall call, ChangePasswordRequest request) {
// TODO (ЗАДАЧА): реализовать changePassword
throw UnimplementedError();
}
}

Остается реализовать отдельные методы и запустить сервер с помощью: Server([AccountService]).serve(port: 8080);.

Это более общий подход. Конкретные примеры и помощь в реализации gRPC  —  в официальной документации.

С базовым кодом сервера разобрались. Посмотрим, что хорошего нам сулит общий язык на сервере и клиенте.

Общий код с приложениями на Flutter

Больше или меньше кода будет для совместного использования  —  зависит от приложения. В нашем случае общим был в основном код подтверждения (надежность пароля, проверка адреса электронной почты и т. д.), логика часового пояса (благодаря пакету Dart timezone у нас есть простые вспомогательные классы, которые обеспечивают загрузку часовых поясов и предоставляют значения по умолчанию, когда он не определяем) и другие вспомогательные функции, например класс Config, используемый во всех проектах.

Мы настроили проект так:

/api <-- Сервер API
  pubspec.yaml
  lib/
  proto/ <-- Все файлы .proto здесь
  и т. д...

/client <-- Приложение на Flutter
  pubspec.yaml
  lib/
  и т. д...

/_shared <-- Общий код сервера и клиента
  pubspec.yaml
  lib/

У нас здесь простая зависимость в api/pubspec.yaml и client/pubspec.yaml:

dependencies:
_shared:
path: ../_shared

Примечание: в нашем случае настройка немного сложнее (с несколькими серверами на Dart [в кластере Kubernetes] и возможностью создать несколько клиентов). Внутри _shared/lib/ есть 3 папки: клиентская, серверная и общая (client, server, generic). В общей (generic), содержится все, что используется сервером и клиентом, в остальных  —  код, используемый только клиентами или только серверами.

Другие преимущества

Запуск Dart имеет еще несколько преимуществ.

Самое очевидное  —  он чуть облегчает разработку, ведь при переключении контекста не нужно писать на другом языке. Хотите реализовать новую функцию в клиенте? Начинаете с реализации сервера на Dart и продолжаете реализацию клиента тоже на Dart. Легче, когда остаешься в одном языке. И проще повторно задействовать одних и тех же разработчиков для всех аспектов приложения.

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

Есть еще непрерывная интеграция. Мы запускаем все тесты, создаем проект и публикуем новые выпуски в GitHub Actions. Для каждого сервиса/приложения в проекте нужно настроить конвейер непрерывной интеграции, запускающий тесты и создающий контейнер Docker. Это немного проще, когда все на одном языке. Нужно просто один раз разобраться во всем процессе непрерывной интеграции, вот и все.

И есть еще AOT-компилятор на Dart! Создания контейнера Docker с установленным node, копирования файлов в этот контейнер и обычного его запуска не происходит. Вместо этого серверное приложение целиком компилируется в единый бинарный код, который оптимизируется, мгновенно запускается и помещается в крошечный контейнер.

Недостатки кода Dart на стороне сервера

Главный недостаток: неясно, сколько усилий готов продолжать вкладывать Google в код Dart на стороне сервера. Думаю: пока есть Flutter, его поддержку не прекратят. Но по мере развития Flutter, возможно, остановят разработку библиотеки gRPC (как уже происходило с библиотекой RPC, бывшей до нее).

Тем не менее факт появления в Google библиотеки gRPC после прекращения поддержки RPC  —  признак того, что фреймворк на Dart на стороне сервера им нужен. А значит, gRPC еще побудет с нами, поэтому в этом отношении я особо не переживаю.

Сторонние библиотеки

Будем откровенны: при работе со сторонней библиотекой придется многое делать самому. Использовали для отправки электронных писем mandrill или socketlabs? Тогда придется написать для этого библиотеку (я написал и для того, и для другого). Использовали в качестве платежного сервиса Stripe? Придется самостоятельно реализовать библиотеку сервера. И т. д и т. п.

У Dart отличный инструментарий для реализации таких API, а библиотека json_serializable просто фантастическая. Но при большом количестве сервисов это может быть утомительным.

С json_serializable мы используем хорошую библиотеку MongoDB для Dart, но она поддерживается в основном одним разработчиком (это немного беспокоит).

В самом Google хорошо справляются с поддержкой библиотек для своих сервисов, включая Firebase, но мне не хочется ограничиваться только их экосистемой.

Необходимость в общем коде

Конечно, многое зависит и от размера команды. При использовании такого инструмента оркестрации контейнеров, как Kubernetes, все равно приходится разрабатывать и развертывать микросервисы. Поэтому, если у вас над отдельными проектами работают отдельные команды, им на самом деле все равно, используется один и тот же код или нет.

Особенно это относится к использованию такого фреймворка для API, как gRPC, который генерирует код для любого языка.

Рекомендовал бы я его?

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

Вам крайне важна стабильность? Тогда, Dart на серверной стороне, вероятно, не ваш вариант. В этом случае фреймворк типа Java Spring будет безопаснее.

У вас на сервере и клиенте работают специальные команды или вы нанимаете сотрудников отдельно для сервера и клиента? В этом случае преимущества использования Dart быстро исчезают. Пусть каждая команда выберет любимый фреймворк.

Если же у вас небольшая команда  —  или вы сами по себе  —  и только начали работать с Flutter или просто любите язык Dart, тогда полный вперед!🙂 gRPC будет существовать еще довольно долго. У Dart отличный инструментарий, и его очень интересно разрабатывать.

Если используете библиотеку типа gRPC, то перейти на другой серверный язык тоже будет несложно: просто генерируете серверную заглушку для другого языка  —  вся работа, связанная с проектированием базы данных, уже выполнена.

Используете инструмент оркестрации контейнеров типа Kubernetes? В этом случае тоже довольно легко постепенно переключиться на API.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Matias Meno: Writing server side Dart code

Предыдущая статья6 SQL-запросов, о которых должен знать каждый дата-инженер
Следующая статьяТОП-10 признаков плохого кода: хардкод и спагетти-код в примерах на JavaScript