Dart на сервере: Руководство для инженера Flutter

Dart на сервере: Руководство для инженера Flutter

FlutterPulse

Эта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Как разработчик Flutter, вы освоили искусство создания красивых, высокопроизводительных пользовательских интерфейсов. Вы живете и дышите виджетами…

Как разработчик Flutter, вы освоили искусство создания красивых, высокопроизводительных пользовательских интерфейсов. Вы живете и дышите виджетами, состоянием и языком Dart. Но что происходит, когда вам нужно получить данные, аутентифицировать пользователя или запустить некоторую логику, которая не принадлежит клиенту?

Традиционно, это означало передачу работы специальной бэкенд-команде или болезненное изучение нового языка и фреймворка, такого как Node.js, Python или Go.

Но что, если бы вы могли создать весь приложение, от кнопки в вашем приложении до запроса базы данных на сервере, используя один язык, который вы уже знаете и любите?

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

Для обзора экосистемы Dart с полным стеком, посмотрите мою статью Dart на сервере: разблокировка истинного потенциала полного стека"

"Почему": superheroes ваши навыки с серверной частью Dart

Давайте перейдем сразу к делу. Почему вы, занятый инженер Flutter, должны инвестировать время в изучение написания Dart на сервере?

Один язык, чтобы править всем

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

  • Меньшая когнитивная нагрузка: Нет больше запоминания разных синтаксисов, инструментов сборки и стандартных библиотек.
  • Быстрая разработка: Вы уже профессионал в Dart. Вы можете начать строить логику бэкенда сразу.
  • Последовательные инструменты: Используйте один и тот же фантастический анализатор Dart, форматировщик (dart format), и отладчик на всем вашем стеке.

От специалиста по UI до архитектора полного стека

Написание своего бэкенда наделяет вас возможностью делать больше и строить полные приложения от начала до конца.

  • Строить функции независимо: Вам нужен новый конечный пункт API для того экрана, который вы только что спроектировали? Вы можете его создать сами.
  • Прототипировать быстрее: Быстро создайте макет сервера или полный API, чтобы протестировать функциональность вашего приложения Flutter, не дожидаясь кого-либо.
  • Понимать полную картину: Вы получите глубокое понимание того, как течет информация, как работает аутентификация и что делает приложение масштабируемым. Эти знания делают вас более ценными и эффективными разработчиками, даже когда вы работаете только на фронте.

Это проще, чем вы думаете

Современные фреймворки серверной части Dart предназначены для того, чтобы быть минимальными и интуитивными. Фреймворк Shelf, который мы будем использовать в этом руководстве, предоставляет простой и мощный способ обработки запросов. Если вы можете написать функцию в Dart, вы можете построить конечную точку API.

Переход: что вы знаете, и что вам нужно узнать

Переход от Flutter к серверной части Dart более естественен, чем вы думаете. Вот быстрый обзор с точки зрения разработчика Flutter:

Что вы уже знаете (и можете повторно использовать)

  • Язык Dart: Все ваши знания синтаксиса, типовой безопасности, async/await, и коллекций применяются напрямую.
  • Основные библиотеки:dart:core, dart:convert (для JSON), и dart:async являются вашими лучшими друзьями на сервере тоже.
  • Экосистема Pub: Вы уже знаете, как использовать pub.dev и управлять зависимостями в pubspec.yaml.
  • IDE & Инструменты: VS Code или IntelliJ с плагином Dart работают идеально для разработки бэкенда.

Знания, которые можно расширить

  • Базы данных: Вы использовали sqflite в Flutter? Знания SQL-запросов напрямую применимы к серверным реляционным базам данных, таким как PostgreSQL или MySQL. Использовали Firebase Firestore? Вы обнаружите, что его модель, основанная на документах, очень похожа на MongoDB, популярный выбор для серверов Dart.
  • Концепции управления состоянием: Концепции провайдеров и внедрения зависимостей в Flutter напрямую аналогичны тому, как вы будете управлять зависимостями (например, соединениями с базой данных) на сервере.

Что вам нужно изучить: Основы

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

  • Основные веб-концепции (HTTP и REST): Вам нужно будет изучить цикл запроса/ответа HTTP, методы (GET, POST, PUT, DELETE), коды состояния и заголовки. Понимание принципов REST является основой для проектирования чистых API.
  • Обработка запросов с помощью серверного фреймворка (Shelf, Dart Frog): Как и Flutter предоставляет фреймворк для UI, вам понадобится серверный фреймворк для обработки веб-запросов. Отличным местом для начала является Shelf, минимальный и составной фреймворк для Dart.
  • Маршрутизация: Вам нужно будет понять, как сервер сопоставляет входящие пути/точки запроса (например, /users/123) с определенными функциями. Хотя вы знакомы с маршрутизацией в Flutter (как GoRouter), маршрутизация на сервере отличается и не涉ивает навигацию по экранам UI.
  • Мидлвэр: Это является ключевой концепцией бэкенда. Вам нужно будет изучить, как использовать мидлвэр для выполнения задач, которые применяются к многим запросам, таких как ведение журнала, проверка токенов аутентификации, добавление стандартных заголовков и перехват ошибок.
  • Аутентификация и авторизация: Вам нужно будет изучить, как проверить личность пользователя (Аутентификация) и определить, что он может делать (Авторизация). Это часто включает в себя методы, такие как JSON Web Tokens (JWT).
  • Подключение к базе данных: Вам нужно будет изучить, как подключить ваш сервер к реальной базе данных (например, MongoDB или PostgreSQL), чтобы хранить и извлекать данные постоянно.
  • Управление конфигурацией и секретами: Вам нужно будет изучить, как управлять конфиденциальной информацией, такой как пароли баз данных и ключи API, с помощью переменных среды, чтобы они никогда не были закодированы в вашем исходном коде.
  • Развертывание: Вам нужно будет изучить, как взять ваше серверное приложение и запустить его на облачной платформе, чтобы оно было доступно вашему приложению Flutter и другим клиентам в Интернете.

Освоение этих тем обеспечит вам способность справляться с большинством распространенных сценариев разработки бэкенда. Лучшее место для изучения всех этих концепций в деталях - это Руководство по бэкенду Dart и Shelf, бесплатное, практическое руководство, которое я создал специально для этой цели.

"Как": пошаговый обзор кода

Давайте проанализируем весь жизненный цикл запроса с кодом, от кнопки Flutter до базы данных и обратно.

Шаг 1: Пользователь нажимает кнопку (UI Flutter)

Все начинается в вашем приложении Flutter. У вас есть простая кнопка, которая, при нажатии, должна извлечь список заметок с сервера.

// В вашем проекте Flutter: lib/notes_view.dart
import 'package:flutter/material.dart';
// Предположим, что ApiService определен в следующем шаге

class NotesView extends StatelessWidget {
  final _apiService = ApiService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Заметки на сервере")),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            print('Кнопка нажата! Извлекаем заметки...');
            final notes = await _apiService.fetchNotes();
            // В реальном приложении вы бы использовали эти заметки для обновления вашего UI
            print('Получены заметки: $notes');
          },
          child: Text("Извлечь заметки с сервера"),
        ),
      ),
    );
  }
}

Шаг 2: Вызов API (Приложение Flutter)

Обратный вызов кнопки onPressed вызывает метод в классе сервиса. Этот класс использует пакет http для выполнения сетевого запроса.

// В вашем проекте Flutter: lib/api_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

class ApiService {
  final String _baseUrl = "http://localhost:8080"; // Ваш серверный адрес

  Future<List<Map<String, dynamic>>> fetchNotes() async {
    try {
      final response = await http.get(Uri.parse('$_baseUrl/notes'));
      if (response.statusCode == 200) {
        // Успех! Распарсить тело ответа JSON.
        final List<dynamic> data = jsonDecode(response.body);
        return data.cast<Map<String, dynamic>>();
      } else {
        throw Exception('Не удалось загрузить заметки: ${response.statusCode}');
      }
    } catch (e) {
      throw Exception('Ошибка при получении заметок: $e');
    }
  }
}

Шаг 3: Обработчик на стороне сервера (Shelf)

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

// В вашем серверном проекте: bin/server.dart
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
// Предположим, что NoteService определен в следующем шаге

void main() async {
  final noteService = NoteService(); // Зависимость
  final app = Router();

  // Маршрутизатор сопоставляет путь '/notes' с нашей логикой обработчика
  app.get('/notes', (Request request) async {
    // 1. Вызов сервиса для извлечения данных
    final notes = await noteService.getAllNotes();

    // 2. Кодирование списка карт Dart в строку JSON
    final responseBody = jsonEncode(notes);

    // 3. Возвращение успешного HTTP-ответа
    return Response.ok(responseBody, headers: {
      'Content-Type': 'application/json',
    });
  });
  final server = await io.serve(app, 'localhost', 8080);
  print(' Сервер слушает по адресу http://${server.address.host}:${server.port}');
}

Шаг 4: Запрос базы данных (Dart Server)

Обработчик вызывает NoteService для получения данных. Этот сервис содержит фактическую логику доступа к данным. Для этого примера мы выделим его, чтобы вернуть простой List<Map<String, dynamic>>.

// В вашем серверном проекте, например, в lib/note_service.dart

// Этот класс сервиса абстрагирует источник данных
class NoteService {
  // Фальшивая "таблица базы данных" заметок, представленная как список карт
  final List<Map<String, dynamic>> _notes = [
    {'id': 1, 'title': 'Dart на сервере круто'},
    {'id': 2, 'title': 'Общие модели с Flutter'},
  ];

  Future<List<Map<String, dynamic>>> getAllNotes() async {
    // Симулируем задержку сети, как реальный вызов базы данных
    await Future.delayed(const Duration(milliseconds: 300));
    return _notes;
  }
}

Шаг 5: Ответ и обновление UI

  • Метод Response.ok(...) в нашем обработчике маршрута (Шаг 3) автоматически создает правильный HTTP-ответ с статусом 200 OK и заголовком application/json, и отправляет его обратно клиенту.
  • Наш сервис API Flutter ApiService получает этот ответ. Инструкция print в fetchNotes покажет строку JSON в консоли вашего приложения Flutter.
  • Оттуда вы можете использовать jsonDecode и ваше любимое решение для управления состоянием (Provider, Bloc, Riverpod, и т.д.) для разбора данных в список объектов и перестроения UI для их отображения.

Вы только что отследили полный запрос от начала до конца, используя 100% Dart!

Лучший способ: Общие модели для типобезопасности

В приведенном выше коде вы могли заметить, что мы передавали Map<String, dynamic> туда и обратно. Это работает, но не идеально. Что если вы допустите ошибку в имени ключа, как notes['titel'] вместо notes['title']? Компилятор Dart не сможет вам помочь, и вы обнаружите ошибку только во время выполнения.

Именно здесь проявляется истинная сила полноценного Dart: типобезопасные модели.

Определяя класс данных один раз в общем пакете (структура "монорепозиторий"), ваше приложение Flutter и сервер могут использовать его. Это устраняет ошибки, позволяет использовать автозаполнение кода и создает единственный источник истины для ваших структур данных.

1. Общая модель Note

Представьте себе папку packages/models, к которой могут обращаться и ваше приложение, и сервер.

// В общем пакете: packages/models/lib/note.dart
class Note {
  final int id;
  final String title;

  Note({required this.id, required this.title});

  // Создание экземпляра Note из JSON-карты
  factory Note.fromJson(Map<String, dynamic> json) {
    return Note(
      id: json['id'] as int,
      title: json['title'] as String,
    );
  }
  // Преобразование экземпляра Note в JSON-карту
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
    };
  }
}

2. Перестановка сервера

Ваш код сервера становится намного чище и типобезопаснее.

Сначала обновите ваш NoteService для работы напрямую с объектами Note.

// В вашем серверном проекте: lib/note_service.dart (Переставленный)
import 'package:models/note.dart'; // Импорт из общего пакета

class NoteService {
  Future<List<Note>> getAllNotes() async { // Тип возвращаемого значения теперь List<Note>
    await Future.delayed(const Duration(milliseconds: 200));
    return [
      Note(id: 1, title: 'Изучение серверной части Dart'),
      Note(id: 2, title: 'Общие модели с Flutter!'),
    ];
  }
}

Далее, в вашем файле bin/server.dart используется метод toJson из модели перед отправкой ответа.

// В вашем серверном проекте: bin/server.dart (Переставленный)
//... импорты
import 'package:models/note.dart';
import 'note_service.dart';

void main() async {
  final noteService = NoteService();
  final app = Router();

  app.get('/notes', (Request request) async {
    final notes = await noteService.getAllNotes();
    // Преобразование каждого объекта Note в Map перед кодированием
    final notesAsMaps = notes.map((note) => note.toJson()).toList();
    final jsonResponse = jsonEncode(notesAsMaps);

    return Response.ok(
      jsonResponse,
      headers: {'Content-Type': 'application/json'},
    );
  });

  //... обслуживание приложения
}

3. Перестановка приложения Flutter

Ваш ApiService становится типобезопасным и возвращает Future<List<Note>>.

// В вашем проекте Flutter...
import 'package:models/note.dart'; // Импорт из общего пакета

class ApiService {
  final String _baseUrl = "http://localhost:8080";

  Future<List<Note>> fetchNotes() async { // Тип возвращаемого значения теперь List<Note>
    final response = await http.get(Uri.parse('$_baseUrl/notes'));
    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      // Используйте fromJson для создания списка объектов Note
      return data.map((json) => Note.fromJson(json)).toList();
    } else {
      throw Exception('Ошибка при загрузке заметок');
    }
  }
}

Теперь, если вы попытаетесь получить доступ к note.titel где-либо в вашем коде Flutter или сервере, анализатор Dart сразу же выдаст ошибку, обнаруживая ошибку до запуска. Это значительное улучшение в плане надежности и поддержки.

Создание своего первого бэкэнда: простой API для заметок

Классическая отправная точка для укрепления ваших новых навыков - это создание простого API CRUD (Create, Read, Update, Delete). Это отличный первый проект для решения после прочтения этого руководства.

Используя Shelf и shelf_router, вы бы определили обработчики для следующих конечных точек:

  • GET /todos: Извлекает все заметки.
  • POST /todos: Создает новую заметку из JSON-тела.
  • PUT /todos/:id: Обновляет конкретную заметку по ее ID.
  • DELETE /todos/:id: Удаляет конкретную заметку по ее ID.

Этот проект даст вам практический опыт работы со всем, что вы узнали.

Для полной реализации этого API и многое другое, обратитесь к Руководству по бэкэнду Dart & Shelf

Окончательное препятствие: развертывание, упрощенное с помощью Globe.dev

Хорошо, вы создали свой бэкэнд. Теперь что? Развертывание может быть пугающим. Настройка серверов, управление контейнерами и настройка CI/CD-пайплайнов - это работа сама по себе.

Именно здесь приходит на помощь Globe.dev.

Globe.dev - это платформа развертывания специально созданная для экосистемы Dart и Flutter. Они обрабатывают всю сложность за вас, делая развертывание простым процессом, который можно выполнить с помощью одной команды.

  • Нулевая конфигурация: Globe автоматически обнаруживает, что вы развертываете проект Dart Frog.
  • Глобальная сеть ребер: Ваш API будет быстрым для пользователей по всему миру.
  • Интуитивный и дружественный для разработчиков: С простым CLI, Globe делает развертывание легким.
  • Бесшовная CI/CD: Просто подключите свою учетную запись GitHub и пусть. Globe автоматически соберет и развернет ваш сервер.

Развертывание примера API

Давайте пройдемся по развертыванию примера API Dart.

  1. Получите пример проекта:git clonehttps://github.com/developerjamiu/todo_backend.git
  2. Загрузите зависимости:cd todo_backend && dart pub get
  3. Тестируйте локально:dart_frog dev (Этот пример использует Dart Frog, но процесс развертывания аналогичен для приложений Shelf). Затем вы можете протестировать конечные точки с помощью curl.
  4. Разверните с помощью Globe:
  • Установите CLI: dart pub global activate globe_cli
  • Войдите: globe login
  • Разверните: globe deploy

Как только вы это сделаете, ваш API будет доступен и доступен для всех в интернете. Для более подробного описания этого процесса развертывания, включая то, как протестировать каждую конечную точку с помощью curl, ознакомьтесь с этим руководством: Развертывание приложения Dart Frog с помощью Globe.

Заключение: ваше путешествие в полный стек начинается сейчас

Для разработчика Flutter самым большим препятствием на пути к изучению разработки бэкэнда часто является новый язык и инструменты. С серверным Dart это барьер устранен. У вас уже есть самый важный навык.

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

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

Report Page