Русский
Русский
English
Статистика
Реклама

Client-server

Recovery mode TypedAPI клиент-сервер для TypeScript

21.04.2021 20:21:36 | Автор: admin


Предлагаю вашему вниманию TypedAPI: библиотеку API клиента-сервера для проектов, в которых фронт и бэк написаны на TypeScript. Акцент делается на максимальной простоте использования. Суть такая: вы пишете API как обычный TypeScript класс с некоторыми ограничениями, потом автоматом из класса строится интерфейс, которым будет пользоваться клиентское приложение. Также генерируется некоторая служебная информация. Остается только настроить коннекторы (доступны HTTP и WebSocket). Валидацию, хранение подключений, и др. берет на себя TypedAPI. Есть поддержка событий.

Чтобы поиграться, есть два простеньких демонстрационных приложения: Hello, world! и Демо-чат.

Установка


TypedAPI состоит из нескольких библиотек, которые подключаются в зависимости от типа использования.



Пример, если мы хотим использовать WebSocket соединение:
# на сервереnpm install --save typedapi-core typedapi-server typedapi-server-wsnpm install --save-dev typedapi-parser# на клиентеnpm install --save typedapi-core typedapi-client typedapi-client-browser-ws

Для HTTP соединения:
# for servernpm install --save typedapi-core typedapi-servernpm install --save-dev typedapi-parser# for clientnpm install --save typedapi-core typedapi-client typedapi-client-browser-http


Требования к API классу


  • Все методы должны возвращать Promise
  • Есть ограничения по типу данных, которые могут принимать и возвращать методы (см. ниже)
  • Класс может содержать дочерние свойства-объекты, построеные по тем же правилам

Дополнительно класс может содержать свойства типа Event и ParametricEvent для реализации событий, и есть заранее определенные инъекции для авторизации и хранения базовой информации о пользователе (см. ниже).
Пример класса для Hello, world!:

export class Api {    async hello(name: string): Promise<string> {        return `Hello, ${name}!`    }}

Ограничения по типам данных


Методы могут возвращать/принимать следующие типы данных:

  • Скалярные: number, string, boolean, Date, undefined, null
  • Array, Tuple, Enum, Union. Могут содержать только описанные здесь типы.
  • Объекты-структуры без методов
  • Индексированные объекты типа { [key: string | number]: SomeOtherType }

Прием/передача any, unknown запрещена.

Генерация интерфейса


Генерация интерфейса производится путем использования команды typedapi-parse из пакета typedapi-parser:

typedapi-parse [sourceFilename] [sourceObjectName] [outFilename] [reflectionOutFileName]

где:

  • sourceFilename: Путь к файлу, где хранится ваш API
  • sourceObjectName: Название класса в этом файле
  • outFilename: Путь к файлу, в который будет записан интерфейс для клиента
  • reflectionOutFileName: Путь к файлу, который будет хранить reflections для всех методов и данных. Он используется на сервере для валидации данных


Создание соединения


После того, как вы настроили API и сегенерировали интерфейсы, можно настраивать соединение. Пример как это может выглядеть для WebSocket:

Сервер
import { WebSocketServer } from "typedapi-server-ws"import { buildMap } from "typedapi-server"// файл, сгенерированный командой typedapi-parse для сервераimport { reflection } from "./apiReflection"// Ваш API классimport { Api } from "./Api"new WebSocketServer({    apiMap: buildMap(reflection, new Api),    port: 8090})

Клиент
import { WebSocketTransport } from "typedapi-client-browser-ws"// файл, сгенерированный командой typedapi-parse для клиентаimport { createClient } from "./apiReflection"const api = createClient({ transport })// теперь можно вызывать методы APIlet result = await api.hello(name)


Настройка HTTP соединения происходит аналогичным способом, можно посмотреть в документации.

События


TypedAPI поддерживает события. При WebSocket соединении оповещение происходит обычным образом, отправкой данных через сокет. При HTTP соединении используется HTTP polling.

Пример cоздания события на сервере:
import { Event } from "typedapi-server"export class Api {    someEvent = new Event<string>()}// отправляем событие всем продписчикамapi.someEvent.fire(data)// отправляем событие одному пользователюapi.someEvent.fireForUser(data, userId)// Отправляем группе пользователейapi.someEvent.fireForGroup(data, groupName)// отправляем конкретной сессииapi.someEvent.fireForSession(data, sessionId)// Отправялем конкретному соединениюapi.someEvent.fireForConnection(data, connectionId)

Обработка на клиенте:
// Подписываемся на событиеconst subscription = await api.someEvent.subscribe(data => {    // обработка события})// Отпискаawait subscription.unsubscribe()


Параметрические события


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

Авторизация


Для того, чтобы реализовать авторизацию в вашем API, надо:
  • Реализовать SessionProviderInterface и передать его в конструктор сервиса. По умолчанию используется MemorySessionProvider, который будет сбрасываться при каждой перезагрузке сервера.
  • Реализовать методы, которые возвращают AuthDataResponse. Это специальный ответ метода, который обрабатывается по другому, нежели другие ответы.

Интерфейс AuthDataResponse выглядит так:
type AuthDataResponse = {    newAuthData: {        id?: string | number        groups?: string[]        name?: string        email?: string        phone?: string    }    // ответ, который будет отправлен пользователю    // newAuthData используется только для внутренних нужд    response: boolean}

пример реализации API с авторизацией:
import { AuthDataResponse } from "typedapi-server"export class ClientApi {    /**     * Проверяем логин и пароль, если подходит авторизуем    **/    async login(username: string, password: string): Promise<AuthDataResponse> {        let user = await usersRepository.login(username, password)        if(!user) {            return {                response: false,                newAuthData: {}            }        } else {            return {                response: true,                newAuthData: {                    id: user.id,                    groups: user.groups                }            }        }    }    /**    * Logout    **/    async logout(): Promise<AuthDataResponse> {        return {            response: true,            newAuthData: {}        }    }    /**    * в apiUserID будет автоматически добавлен идентификатор пользователя.    * Если пользватель не авторизован, он получит NotAuthorizedError    **/    async getUserData(apiUserId: number): Promise<SomeUserData> {        let userData = await usersRespotory.getUserData(apiUserId)        return userData    }}


Также помимо apiUserId можно получать и другие данные о пользователе, подробнее в документации.

Микросервисы


Для разбиения API на микросервисы используется следующая идея: на каждом микросервисе поднимается экземпляр API, и с помощью для каждого экземпляра устанавливается, какой сервис будет отвественен за обработку определенных методов. Для этого есть следующие инструменты:
  • HttpProxyClient
    Это объект проксирует вызовы методов со входного API на внутренний сервис
  • HttpTrustServer
    Этот объект принимает вызов от HttpProxyClient и вызывает запрашиваемый метод
  • RedisPublisher
    Присоединяется к API, слушает события, и перенаправляет их в Redis. Используется во внутренних сервисах.
  • RedisSubscriber
    Слушает события из Редиса, и запускает их на входном API

Архитектура может выглядеть примерно так:

Примеры кода можно найти в документации.

Заключение


Я описал ключевые особенности TypedAPI, для дополнительной информации можно почитать документацию. Я уже использую эту библиотеку в паре своих проектов, но пока она все же находится в режиме беты. Я активно ее развиваю, и надеюсь, что она будет полезна людям. Буду рад любой конструктивной критике.

Спасибо за внимание.
Подробнее..
Категории: Javascript , Typescript , Api , Client-server

Основы Flutter для начинающих (Часть V)

04.06.2021 10:04:57 | Автор: admin

Наконец-то мы добрались до одной из самых важных тем, без которой идти дальше нет смысла.

План довольно простой: нам предстоит познакомиться с клиент-серверной архитектурой и реализовать получение списка постов.

В конце мы правильно организуем файлы наших страниц и вынесем элемент списка в отдельный файл.

Полетели!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 (текущая статья) - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - работа с формами, текстовые поля и создание поста.

  • Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 - немного о тестировании;

Client и Server

Модель Client / Server лежит в основе всего Интернета и является наиболее распространенной.

В чем её суть?

Сначала разберемся что такое клиент и сервер:

  • Клиент - пользовательское устройство, которое отправляет запросы за сервер и получает ответы. Это может быть смартфон, компьютер или MacBook.

  • Сервер - специальный компьютер, который содержит данные, необходимые для пользователя.

Вся модель сводиться к примитивному принципу: клиент отправил запрос, сервер принял его, обработал и передал ответ клиенту.

Для организации взаимодействия сервера и клиента используются специальные протоколы. На текущий момент одним из самых распространенных протоколов в сети Интернет является http / https (s означает защищенный, secure).

http / https позволяет передавать почти все известные форматы данных: картинки, видео, текст.

Мы будем работать с JSON форматом.

JSON - простой и понятный формат данных, а главное легковесный, т.к. передается только текст.

Пример JSON:

[  {    "userId": 1,    "id": 1,    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"  },  {    "userId": 1,    "id": 2,    "title": "qui est esse",    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"  },  ...]  

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

Обратите внимание: квадратные скобки указывает на массив данных, а фигурные на отдельный объект.

JSON позволяет создавать глубокую вложенность объектов и массивов:

{  "total_items" : 1  "result" : [  {  "id" : 1,  "name" : "Twillight Sparkle",  "pony_type" : "alicorn",  "friends" : [  "Starlight Glimmer", "Applejack", "Rarity", "Spike"  ]}  ]}

Понятие запроса

Для обмена данными клиент должен отправлять запросы на сервер.

Т.к. интернет в большинстве случаев использует http / https то запросы называются HTTP запросами.

Структура HTTP запроса:

  • URL - уникальный адрес в Интернете, который идентифицирует сервер и его конкретный ресурс, данные которого мы собираемся получить. В нашем случае URL выглядит следующим образом: https://jsonplaceholder.typicode.com/posts. (об структуре самого URL'а можно почитать в Википедии)

  • Метод, который определяет типа запроса. GET используется только для получения данных, POST позволяет клиенту добавить свои данные на сервер, DELETE - удалить их, PUT - изменить.

  • Данные запроса обычно называются телом запроса и используются совместно с POST, PUT и DELETE методами. Для GET метода в основном используются параметры самого URL'а. Выглядит это следующим образом: https://jsonplaceholder.typicode.com/posts/1 (здесь мы обращаемся к конкретному посту по его id = 1)

Запрос и вывод списка постов

Мы будем использовать довольно мощный и простой пакет http для отправки запросов на сервер.

Сначала убедимся, что мы указали его в pubspec.yaml файле:

# блок зависимостейdependencies:  flutter:    sdk: flutter  # подключение необходимых pub-пакетов  # используется для произвольного размещения  # компонентов в виде сетки  flutter_staggered_grid_view: ^0.4.0  # мы будем использовать MVC паттерн  mvc_pattern: ^7.0.0  # http предоставляет удобный интерфейс для создания# запросов и обработки ошибок  http: ^0.13.3

Переходим к созданию классов модели.

Для этого создайте файл post.dart в папке models:

// сначала создаем объект самого постаclass Post {  // все поля являются private  // это сделано для инкапсуляции данных  final int _userId;  final int _id;  final String _title;  final String _body;    // создаем getters для наших полей  // дабы только мы могли читать их  int get userId => _userId;  int get id => _id;  String get title => _title;  String get body => _body;  // Dart позволяет создавать конструкторы с разными именами  // В данном случае Post.fromJson(json) - это конструктор  // здесь мы принимаем JSON объект поста и извлекаем его поля  // обратите внимание, что dynamic переменная   // может иметь разные типы: String, int, double и т.д.  Post.fromJson(Map<String, dynamic> json) :    this._userId = json["userId"],    this._id = json["id"],    this._title = json["title"],    this._body = json["body"];}// PostList являются оберткой для массива постовclass PostList {  final List<Post> posts = [];  PostList.fromJson(List<dynamic> jsonItems) {    for (var jsonItem in jsonItems) {      posts.add(Post.fromJson(jsonItem));    }  }}// наше представление будет получать объекты// этого класса и определять конкретный его// подтипabstract class PostResult {}// указывает на успешный запросclass PostResultSuccess extends PostResult {  final PostList postList;  PostResultSuccess(this.postList);}// произошла ошибкаclass PostResultFailure extends PostResult {  final String error;  PostResultFailure(this.error);}// загрузка данныхclass PostResultLoading extends PostResult {  PostResultLoading();}

Одной из наиболее неприятных проблем является несоответствие типов.

Если взглянуть на JSON объект поста:

{  "userId": 1,  "id": 1,  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"}

То можно заметить, что userId и id являются целыми числами, а title и body строками, поэтому в конструкторе Post.fromJson(json) мы не замарачиваемся с привидением типов.

Пришло время создать Repository класс.

Для этого создадим новую папку data и в нем файл repository.dart:

import 'dart:convert';// импортируем http пакетimport 'package:http/http.dart' as http;import 'package:json_placeholder_app/models/post.dart';// мы ещё не раз будем использовать // константу SERVERconst String SERVER = "https://jsonplaceholder.typicode.com";class Repository {  // обработку ошибок мы сделаем в контроллере  // мы возвращаем Future объект, потому что  // fetchPhotos асинхронная функция  // асинхронные функции не блокируют UI  Future<PostList> fetchPosts() async {    // сначала создаем URL, по которому    // мы будем делать запрос    final url = Uri.parse("$SERVER/posts");    // делаем GET запрос    final response = await http.get(url);// проверяем статус ответаif (response.statusCode == 200) {  // если все ок то возвращаем посты  // json.decode парсит ответ   return PostList.fromJson(json.decode(response.body));} else {  // в противном случае говорим об ошибке  throw Exception("failed request");}  }}

Вы скажите: мы могли все запихнуть в контроллер, зачем создавать ещё один класс?

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

К тому же это не очень гибко. Вдруг нам нужно будет поменять URL адрес сервера.

Реализуем PostController:

import '../data/repository.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostController extends ControllerMVC {  // создаем наш репозиторий  final Repository repo = new Repository();  // конструктор нашего контроллера  PostController();    // первоначальное состояние - загрузка данных  PostResult currentState = PostResultLoading();  void init() async {    try {      // получаем данные из репозитория      final postList = await repo.fetchPosts();      // если все ок то обновляем состояние на успешное      setState(() => currentState = PostResultSuccess(postList));    } catch (error) {      // в противном случае произошла ошибка      setState(() => currentState = PostResultFailure("Нет интернета"));    }  }}

Заключительная часть: подключим наш контроллер к представлению и выведем посты:

import 'package:flutter/material.dart';import '../controllers/post_controller.dart';import '../models/post.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC {  // ссылка на наш контроллер  PostController _controller;  // передаем наш контроллер StateMVC конструктору и  // получаем на него ссылку  _PostListPageState() : super(PostController()) {    _controller = controller as PostController;  }  // после инициализации состояния  // мы запрашивает данные у сервера  @override  void initState() {    super.initState();    _controller.init();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: _buildContent()    );  }  Widget _buildContent() {    // первым делом получаем текущее состояние    final state = _controller.currentState;    if (state is PostResultLoading) {      // загрузка      return Center(        child: CircularProgressIndicator(),      );    } else if (state is PostResultFailure) {      // ошибка      return Center(        child: Text(          state.error,          textAlign: TextAlign.center,          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red)        ),      );    } else {      // отображаем список постов      final posts = (state as PostResultSuccess).postList.posts;      return Padding(        padding: EdgeInsets.all(10),        // ListView.builder создает элемент списка        // только когда он видим на экране        child: ListView.builder(          itemCount: posts.length,          itemBuilder: (context, index) {            return _buildPostItem(posts[index]);          },        ),      );    }  }  // элемент списка   Widget _buildPostItem(Post post) {    return Container(        decoration: BoxDecoration(            borderRadius: BorderRadius.all(Radius.circular(15)),            border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3)        ),        margin: EdgeInsets.only(bottom: 10),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(              decoration: BoxDecoration(                borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),                color: Theme.of(context).primaryColor,              ),              padding: EdgeInsets.all(10),              child: Text(                post.title,                textAlign: TextAlign.left,                style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),),            ),            Container(              child: Text(                post.body,                style: Theme.of(context).textTheme.bodyText2,              ),              padding: EdgeInsets.all(10),            ),          ],        )    );  }}

Не пугайтесь если слишком много кода.

Все сразу освоить невозможно, поэтому не спешите)

Запуск

Попробуем запустить:

Вуаля! Теперь отключим интернет:

Все работает!

Небольшая заметка

Одним из важных принципов программирования является стремление к минимизации кода и его упрощению.

Файл post_list_page.dart содержит всего 110 строк кода, это не проблема. Но если бы он был в 10 или даже в 20 раз больше!

Какой ужас был бы на глазах у того, кто взглянул бы на него.

Лучшей практикой считается выносить повторяющие фрагменты кода в отдельные файлы.

Давайте попробуем вынести функцию Widget _buildItem(post) в другой файл.

Для этого создадим для каждой группы страниц свою папку:

Затем в папке post создадим новый файл post_list_item.dart:

import 'package:flutter/material.dart';import '../../models/post.dart';// элемент спискаclass PostListItem extends StatelessWidget {    final Post post;    // элемент списка отображает один пост  PostListItem(this.post);    Widget build(BuildContext context) {    return Container(        decoration: BoxDecoration(            borderRadius: BorderRadius.all(Radius.circular(15)),            border: Border.all(color: Colors.grey.withOpacity(0.5), width: 0.3)        ),        margin: EdgeInsets.only(bottom: 10),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            Container(              decoration: BoxDecoration(                borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),                color: Theme.of(context).primaryColor,              ),              padding: EdgeInsets.all(10),              child: Text(                post.title,                textAlign: TextAlign.left,                style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white),),            ),            Container(              child: Text(                post.body,                style: Theme.of(context).textTheme.bodyText2,              ),              padding: EdgeInsets.all(10),            ),          ],        )    );  }}

Не забудьте удалить ненужный код из post_list_page.dart:

import 'package:flutter/material.dart';import '../../controllers/post_controller.dart';import '../../models/post.dart';import 'post_list_item.dart';import 'package:mvc_pattern/mvc_pattern.dart';class PostListPage extends StatefulWidget {  @override  _PostListPageState createState() => _PostListPageState();}// не забываем расширяться от StateMVCclass _PostListPageState extends StateMVC {  // ссылка на наш контроллер  PostController _controller;  // передаем наш контроллер StateMVC конструктору и  // получаем на него ссылку  _PostListPageState() : super(PostController()) {    _controller = controller as PostController;  }  // после инициализации состояние  // мы запрашивает данные у сервера  @override  void initState() {    super.initState();    _controller.init();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post List Page"),      ),      body: _buildContent()    );  }  Widget _buildContent() {    // первым делом получаем текущее состояние    final state = _controller.currentState;    if (state is PostResultLoading) {      // загрузка      return Center(        child: CircularProgressIndicator(),      );    } else if (state is PostResultFailure) {      // ошибка      return Center(        child: Text(          state.error,          textAlign: TextAlign.center,          style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.red)        ),      );    } else {      // отображаем список постов      final posts = (state as PostResultSuccess).postList.posts;      return Padding(        padding: EdgeInsets.all(10),        // ListView.builder создает элемент списка        // только когда он видим на экране        child: ListView.builder(          itemCount: posts.length,          itemBuilder: (context, index) {            // мы вынесли элемент списка в            // отдельный виджет            return PostListItem(posts[index]);          },        ),      );    }  }  }

Заключение

В последующих частях мы ещё не раз будем сталкиваться с созданием сетевых запросов.

Я постарался кратко рассказать и показать на наглядном примере работу с сетью.

Надеюсь моя статья принесла вам пользу)

Ссылка на Github

Всем хорошего кода!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru