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

Fullstack development

Best practices для клиент-серверного проекта PoC

22.12.2020 12:11:10 | Автор: admin
image
Типичный проект PoC (Proof of Concept) состоит из клиента с GUI, сервера c бизнес логикой и API между ними. Также используется база данных, хранящая оперативную информацию и данные пользователей. Во многих случаях необходима связь с внешними системами со своим API.
Когда у меня возникла необходимость в создании проекта PoC, и я начал разбираться в деталях, то оказалось, что порог вхождения в веб-программирование весьма высок. В крупных проектах для каждого компонента есть выделенные специалисты: front-end, back-end разработчики, UX/UI дизайнеры, архитекторы баз данных, специалисты по API и информационной безопасности, системные администраторы. В небольшом PoC надо самому во всем разобраться, выбрать подходящее техническое решение, реализовать и развернуть. Ситуацию ухудшает тот факт, что из обучающих материалов не всегда понятно, почему предлагается сделать именно так, а не иначе, есть ли альтернативы, является ли решение best practice или это частное мнение автора. Поэтому я разработал заготовку под названием Common Test DB, отвечающую лучшим практикам. Ее можно использовать для начала любого проекта, остается только наполнить функциональным смыслом.
В статье я подробно опишу примененные best practices, расскажу про имеющиеся альтернативы и в конце размещу ссылки на исходники и работающий в сети пример.

Требования к проекту PoC


Начнем с минимальных требований к проекту PoC. В статье Блокчейн: что нам стоит PoC построить? я уже рассматривал из каких частей состоит универсальная информационная система. В этой статье подробно разберем состав типичного проекта PoC. Даже минимальный проект в клиент-серверной архитектуре требует значительной функциональности и технической проработки:

Клиент:
  • Должен осуществлять запросы к серверу, используя REST API
  • Регистрировать, авторизовать, аутентифицировать пользователей
  • Принимать, разбирать и визуализировать пришедшие данные

Сервер
  • Должен принимать HTTP/HTTPS запросы от клиента
  • Осуществлять взаимодействие с внешними системами
  • Взаимодействовать с базой данных (реализовать базовую функциональности CRUD)
  • Обеспечивать безопасность данных, вызовов API и конфигурационных параметров
  • Осуществлять логирование и мониторинг работоспособности

База данных
  • Необходимо реализовать структуру DB
  • Наполнить DB начальными значениями

Архитектура
  • Проект PoC должен быть реализован с возможностью расширения функциональности
  • Архитектура должна предусматривать масштабирование решения

Технологический стек


  • В настоящее время для большинства веб проектов используетсяязык JavaScript.Над JavaScript есть надстройкаTypeScript, которая обеспечивает типизацию переменных и реализацию шаблонов.
  • Для полнофункциональных веб проектов есть несколько стандартных стеков технологий. Один из самых популярных: MEVN (MongoDB + Express.js + Vue.js + Node.js), мне больше нравится PEVN (PostgreSQL + Express.js + Vue.js + Node.js), т.к. RDB базы мне технологически ближе, чем NoSQL.
  • Для GUI существует несколькофреймворков (Vue, React, Angular). Тут выбор, скорее, определяется традициями или личными предпочтениями, поэтому сложно говорить, что лучше, а что хуже. Сначала я выбрал Vue.js, да так на нем и остался. Этот фреймворк хорошо развивается, для него есть готовыевизуальныекомпоненты встилеMaterial Design (Vuetify), которые хорошо смотрятся даже при непрофессиональном применении. Считается, что для React порог вхождения выше, чем для Vue. В React сначала надо осознать специфичную объектную модель, которая отличается от классического веб программирования: компоненты как функции, компоненты как классы, цепочки вызовов.
  • Выбор базы данных уже сложнее, чем личные предпочтения. Но для PoC, зачастую, требуется не функциональность или производительность, а простота. Поэтому в примере будем использовать самую простую в мире базу SQLite. В промышленных PoC в качестве SQL базы я использую PostgreSQL, в качестве NoSQL ее же, т.к. запас прочности уPostgreSQL огромен. Прочность, конечно, не бесконечна и когда-нибудь настанет необходимость перехода на специализированную базу, но это отдельная тема для обсуждения.

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

Сервер


HTTP сервер


В качестве HTTP сервера япробовал два варианта:

Express более зрелый, Fastify, говорят, более быстрый. Есть еще варианты серверов, примеры перечислены в статье The Best 10 Node.js Frameworks. Я использовал самый популярный Express.js.

HTTPS запросы


Говоря про HTTP, я всегда подразумеваю HTTPS, т.к. без него крайне сложно построить безопасное взаимодействие между клиентом и сервером. Для поддержки HTTPS надо:
  • Получить SSL (TLS) сертификат
  • На сервере реализовать поддержку HTTPS протокола
  • На клиенте использовать запросы к клиенту с префиксом HTTPS

Получаем сертификат
Для организации работы HTTPS протокола нужен SSL сертификат. Существуют бесплатные сертификаты Lets Encrypt, но солидные сайты получают платные сертификаты от доверительных центров сертификации CA(Certificate Authority). Для наших тестовых целей на локальном хосте достаточно, так называемого, self-signedcertificate. Он генерируется с помощью утилиты openssl:
openssl req -nodes -new -x509 -keyout server.key -out server.crt

Далее отвечаем на несколько вопросов про страну, город, имя, email и т.п. В результате получаемдва файла:
  • server.key ключ
  • server.crt сертификат

Их нужно будет подложить нашему Express серверу.

HTTPS сервер
С Express сервером все достаточно просто, можно взять стандартный пакет HTTPS из Node.js, и использовать официальную инструкцию: How to create an https server?
После реализации HTTPS сервера используем полученные файлы server.key и server.crt и стандартный для HTTPS порт443.

Взаимодействие с внешними системами


Самая известная библиотека для реализации HTTP/HTTPS запросов: Axios. Она используется как в сервере для вызова API внешних систем, так и в клиенте для вызова API сервера. Есть еще варианты библиотек, которые можно использовать дляспецифичных целей: Обзор пяти HTTP-библиотек для веб-разработки.

Взаимодействие с базой данных


Для работы с базой данных я использовал самую популярную библиотеку для Node.js: Sequelize. В ней мне больше всего нравится то, что переключиться между различными типами баз данных можно всего лишь изменив несколько настроечных параметров. При условии, конечно, что в самом коде не используется специфика определенной базы.Например, чтобы переключиться с SQLight на PostgreSQL, надо в конфигурационном файле сервера заменить:
dialect: 'sqlite'

на
dialect: 'postgres'

И при необходимости изменить имя базы, пользователя и хост.
В PoC я использовал базу данныхSQLite, которая не требует установки.Для администрирования баз данных есть хорошо развитые GUI:

Безопасность доступа к данным


В примере реализована следующая простейшая логика доступа к данным, которую можно расширять и изменять в зависимости от бизнес-задач:
  1. При регистрации каждому пользователю присваивается уникальный ID.
  2. Когда пользователь создает новые данные, в них добавляется его ID.
  3. При любом запросе к данным проверяется, что пользователь имеет на это право:

  • Получить данные могут все зарегистрированные пользователи.
  • Изменить и удалить данные может только владелец этих данных.

Такая модель доступа к данным называетсяDiscretionary Access Control (DAC), по-русски: избирательное управление доступом. В этой модели владелец данных может делать с ними любые CRUD операции, а права остальных пользователей ограниченны.
Существуют несколько моделей контроля доступа к данным. ПомимоDiscretionary Access Control (DAC)OWASP рекомендует (Access Control Cheat Sheet) использовать следующие:
  • Role-Based Access Control (RBAC)
  • Mandatory Access Control (MAC)
  • Permission Based Access Control

Безопасность вызовов API


В моей предыдущей статье я рассматривал все возможные угрозы: Безопасность REST API от А до ПИ.В PoC реализованы следующие механизмы:
  • Cross-Site Request Forgery (CSRF) (Межсайтовая подмена запросов)

От данной угрозы реализован механизм CSRF токенов когда для каждой сессии пользователя генерируется новый токен (он же SessionId) и сервер проверяет его валидность при любых запросах с клиента. Алгоритм генерации и проверки SessionId подробно описан далее в разделе Авторизация, аутентификация пользователей.
  • Cross-origin resource sharing (CORS) (Кросс-доменное использование ресурсов)

Защита реализована с помощью пакета CORS. В config файле сервера указываются адрес (origin), с которого могут поступать API запросы и список методов (methods), которые может использовать клиент. В дальнейшем сервер будет автоматически ограничивать прием запросов в соответствии с этими настройками.
Для защиты от угроз, перечисленных далее, я использовал пакетhelmet, который позволяет задать значения для определенных HTTP заголовков:
  • Cross-site Scripting (XSS) (Межсайтовое выполнение скриптов)

Для защиты выставляется заголовок, ограничивающий выполнение скриптов:
X-XSS-Protection: 1; mode=block

  • Insecure HTTP Headers

Блокируется отправка сервером заголовка, дающего дополнительную информацию злоумышленнику:
X-Powered-By: Express

  • Insecure HTTP Headers: HTTP Strict Transport Security (HSTS)

Используется Strict-Transport-Security заголовок, который запрещает браузеру обращаться к ресурсам по HTTP протоколу, только HTTPS:
Strict-Transport-Security: max-age=15552000; includeSubDomains

  • Insecure HTTP Headers: X-Frame-Options (защита от Clickjacking)

Данный заголовок позволяет защититься от атаки Clickjacking. Он разрешает использовать фреймы только в нашем домене:
X-Frame-Options: SAMEORIGIN

  • Insecure HTTP Headers: X-Content-Type-Options

Установка заголовка X-Content-Type-Options запрещает браузеру самому интерпретировать тип присланных файлов и принуждает использовать только тот, что был прислан в заголовке Content-Type:
X-Content-Type-Options: nosniff

Защита от DoS


Необходимо защитить сервер и от отказа в обслуживании (DoS-атаки). Например, ограничить число запросов от одного пользователя или по одному ресурсу в течении определенного времени.
Для Node.js существуют средства, позволяющие автоматически реализовывать ограничения на число запросови сразу посылать ответ 429 Too Many Requests, не нагружая бизнес логику сервера.
В примере реализовано простейшее ограничение на число запросов от каждого пользователя в течении определенного времени по таблице Data. В промышленных системах надо защищать все запросы, т.к. даже один запрос, оставшийся без проверки может дать возможность провести атаку.

Безопасность конфигурационных параметров


Настройки для клиента и сервера хранятся в файлах configв JSON формате. Чувствительные настройки сервера нельзя хранить вconfig файлах, т.к. они могут стать доступны в системах версионного контроля. Для такой информации как: логины/пароли для базы данных, ключи доступа к API и т.д. надо использовать специализированные механизмы:
  • Задавать эти параметры в .env файле. Файл .env прописан в gitignore и не сохраняется в системах версионного контроля, поэтомупоэтому туда можно безопасно записывать чувствительную информацию.
  • Использовать механизм переменных окружения (environment variables), которые устанавливаются вручную или прописываютсяинсталляторами.

Логирование


Стандартные требования к логированию:
  • Обеспечить разный уровень логирования (Debug; Info; Warning; Error и т.д.)
  • Логировать HTTP/HTTPS запросы
  • Дублировать консольный вывод в файлы на диске
  • Обеспечить самоочистку файлов логирования по времени иобъёму

Варианты пакетов для логирования, которые я пробовал:

В проекте я использовал log4js. Пакет log4jsработает интуитивно понятно и достаточно развит, чтобы реализовать все требования.

Мониторинг


Мы не будем останавливаться на внешних система мониторинга: Обзор систем мониторинга серверов, т.к. они начинают играть важную роль не на этапе PoC, а в промышленной эксплуатации.
Обратимся к внутренним системам, которые позволяют наглядно посмотреть, что сейчас происходит с сервером, например пакетexpress-status-monitor. Если его установить, топо endpoint
/monitor

можно наглядно мониторить простейшие параметры работы сервера, такие как загрузка CPU, потребление памяти, http нагрузку и т.п. Приведу скриншот из нашего примера. На скриншоте горит красная плашка Failed и может показаться, что что-то не так. Но, на самом деле, все в порядке, т.к. вызов API сознательно делается на несуществующий endpoint:
/wrongRoutePath

image

База данных


Структурабазы данных


В нашем примере реализованы две таблицы с говорящими именами:
  • Users
  • Data

Лучшие практики говорят, что все проверки надо помаксимуму отдавать базе данных. Поэтому в схеме базы данных аккуратно определяем все типы данных, добавляем ограничения (constraints) и внешние ключи.Для пакета sequelize данная функциональность подробно описана в стандартной документации.
В нашем пример сделан один внешний ключ UserId в таблице Data для того, чтобы гарантировать, что у каждой записи в таблицеData будет определенный владелец User. Это позволит при изменении или удалении записи в Data реализовать проверку пользователя, т.к. по нашей логике данные может удалять только их владелец.
Еще одна особенность нашей схемы в том, что один столбец таблицы Data задан в формате JSON. Такой формат удобно использовать, если внешняя система возвращает данные в JSON. В этом случае можно просто записать полученные данные, не тратя усилия на парсинг,а потом делать SQL запросы, используя расширенный синтаксис.Описание работы со столбцами JSON можно найти в официальной документации баз данных. В качестве независимого описания мне понравилась статья на Хабре, в которой описаны все варианты запросов: "JSONB запросы в PostgreSQL.

Формальная схема DB
Cхема таблицыUsers:
  • id INTEGERPRIMARY KEY AUTOINCREMENT уникальный ID
  • uuid UUID NOT NULL UNIQUE уникальный UUID, по нему идет связь с данными этого пользователя в таблице Data
  • email VARCHAR(255) NOT NULL UNIQUE
  • password VARCHAR(64)
  • ddosFirstRequest DATETIME
  • ddosLastRequest DATETIME
  • ddosRequestsNumber DECIMAL
  • lastLogin DATETIME
  • loginState VARCHAR(255) NOT NULL
  • sessionId VARCHAR(1024)
  • commonToken VARCHAR(1024)
  • googleToken VARCHAR(1024)
  • googleAccessToken VARCHAR(1024)
  • googleRefreshToken VARCHAR(1024)
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL

Схема таблицыData:
  • id INTEGERPRIMARY KEY AUTOINCREMENT
  • uuid UUID NOT NULL UNIQUE
  • ownerUuid UUID NOT NULL
  • data JSON NOT NULL
  • UserId INTEGER REFERENCES Users (id) ON DELETE CASCADE ON UPDATE CASCADE,
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL


Начальные значения


  • В таблице Users задан один пользовательuser@example.comс паролем password.
  • В таблице Data записано несколько предопределенных значений, которые можно получить из GUI.

Далее перейдем к клиенту.

Клиент


HTTPS запросы


Для безопасной передачи информации клиент использует HTTPS протокол. Для запросов используется та же библиотека, что и на сервере для вызова API внешних систем: Axios.

RESTful API


REST API лучше реализовывать, используя стандартные средства, чтобы было проще тестировать и документировать, например SwaggerHub
Наше REST API сформировано по правилам Open API и использует стандартные HTTP методы для манипуляции объектами из таблиц Users и Data.В схеме нашего API не все сделано по чистым правилам REST, но главное соблюдена общая концепция.
Хорошей практикой являетсяограничение объёма возвращаемых данных. Для этого используются параметры в запросе: фильтр (filter), ограничение (limit) и смещение (offset). В примере этого нет, но в промышленных системах эта функциональность должна быть реализована.Но даже если реализованы ограничивающие механизмы на клиенте, должна осуществляться дополнительная проверка на сервере на максимальные значения. В нашем примере в конфигурации сервера реализован ограничитель на максимальное число возвращаемых строк из базы,который подставляется вSELECT запросы:limit: 1000

YAML файл с Open API описанием
---swagger: "2.0"info:  description: "This is a common-test-db API. You can find out more about common-test-db\    \ at \nhttps://github.com/AlexeySushkov/common-test-db\n"  version: "2.0.0"  title: "common-test-db"  contact:    email: "alexey.p.sushkov@gmail.com"  license:    name: "MIT License"    url: "https://github.com/AlexeySushkov/common-test-db/blob/main/LICENSE"host: "localhost:443"basePath: "/commontest/v1"tags:- name: "data"  description: "Everything about Data"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"- name: "users"  description: "Everything about Users"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"schemes:- "https"paths:  /data:    get:      tags:      - "data"      summary: "Gets all data"      description: "Gets all data"      operationId: "getData"      produces:      - "application/json"      parameters: []      responses:        "200":          description: "successful operation"          schema:            type: "array"            items:              $ref: "#/definitions/Data"        "400":          description: "Invalid status value"      x-swagger-router-controller: "Data"    post:      tags:      - "data"      summary: "Add a new data to the db"      operationId: "addData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data object that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "500":          description: "Create data error"      x-swagger-router-controller: "Data"    put:      tags:      - "data"      summary: "Update an existing data"      operationId: "updateData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data information that needs to be changed"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Update data error"      x-swagger-router-controller: "Data"    delete:      tags:      - "data"      summary: "Delete an existing data"      operationId: "deleteData"      consumes:      - "application/x-www-form-urlencoded"      - "application/json"      produces:      - "application/json"      parameters:      - name: "uuid"        in: "formData"        description: "uuid"        required: true        type: "string"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Delete data error"      x-swagger-router-controller: "Data"  /users:    post:      tags:      - "users"      summary: "Register new user"      operationId: "userRegister"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be deleted"        required: true        schema:          $ref: "#/definitions/User"      responses:        "500":          description: "Register user error"      x-swagger-router-controller: "Users"    put:      tags:      - "users"      summary: "Update existing user"      operationId: "userUpdate"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User information that needs to be changed"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"    delete:      tags:      - "users"      summary: "Delete user"      operationId: "userDelete"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"  /users/login:    post:      tags:      - "users"      summary: "Login"      operationId: "userLogin"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Login user error"      x-swagger-router-controller: "Users"definitions:  Data:    type: "object"    required:    - "Counter1"    - "Counter2"    properties:      Counter1:        type: "integer"        format: "int64"      Counter2:        type: "integer"        format: "int64"    example:      Counter1: 10      Counter2: 20  User:    type: "object"    required:    - "email"    - "password"    properties:      email:        type: "string"      password:        type: "string"    example:      password: "password"      email: "email"  ApiResponse:    type: "object"    properties:      status:        type: "string"


Интересная возможность встроить Swagger прямо в сервер, далее подложить ему YAML, реализующий API и по endpoint:
/api-docs

получить стандартную Swagger панель управления и осуществлять тестирование и просмотр документации. Скриншот Swagger, встроенного в наш сервер:

image

GraphQL


Существуют и альтернативы REST API, например GraphQL. Данный стандарт разработан Facebook и позволяет избавиться от недостатков REST:
  • В REST API взаимодействие происходит с использованием многочисленные endpoints. В GraphQL одна.
  • В REST API для получения данных от разных endpoints необходимо осуществить множественные запросы. В GraphQL все необходимые данные можно получить одним запросом.
  • В REST API клиент не может контролировать объём и содержимое данных в ответе. В GraphQL набор возвращаемых данных предсказуем, т.к. задается в запросе. Это сразу избавляет от угрозы: Excessive Data Exposure (Разглашение конфиденциальных данных)
  • В REST API отсутствует обязательность формальной схемы. В GpaphQL создание схемы API с определенными типами данных обязательно, при этом автоматически получается документация и сервер для тестирования (самодокументироваемость и самотестируемость).
  • Клиент всегда может запросить схему API с сервера, тем самым синхронизировать формат своих запросов
  • Из коробки работают подписки, реализованные на технологии WebSockets

Какое API использовать, решается в зависимости от бизнес задач. REST API практически безальтернативно используется в промышленных системах и задачах интеграции между ними. Но для проектов, не требующих взаимодействия со сторонними системами вполне можно использовать более продвинутый GraphQL.

Регистрация,аутентификация и авторизация пользователей


Если приложение доступно из Интернета, то в форме регистрации имеет смысл использовать функциональность reCaptсha от Google для защиты от ботов и DoS атак. В нашем примере я использую пакет vue-recaptcha.
Для использованияreCaptсha нужно на сайте Google зарегистрировать приложение, получить sitekey и прописать его в настройках клиента. После этого на форме появится, известный всем, вопрос про робота:
image

В примере реализована регистрация пользователей с помощью логина/пароля ис использованием Google Account.
  • При регистрации по логин/пароль пароли в базе данных, разумеется, не хранятся в виде текста, а хранится только хеш. Для его генерации используется библиотекаbcrypt, которая реализует алгоритм хеширванияBlowfish. Сейчас уже есть мнение, что надо использовать более надежные алгоритмы: Password Hashing: Scrypt, Bcrypt and ARGON2.
  • При регистрации с помощью Google Account организация хранения аутентификационных данных проще, т.к. в базу записывается только token, полученный от Google.

Для реализации различных схем аутентификации есть библиотека Passport, которая упрощает работу и скрывает детали. Но чтобы понять как работают алгоритмы, в примере все сделано в соответствии со стандартами, которые я уже описывал в своей статье: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn.
Далее в статье разберем практическую реализацию.

Аутентификация по логину/паролю (Basic Authentication)
Тут все достаточно просто. При получении запроса на логин сервер проверяет, что пользователь с присланнымemail существует и что хеш полученного пароля совпадает с хешом из базы. Далее:
  • Сервер формирует SessionId в формате UUID и Token в формате JWT (JSON Web Token)
  • Токену присваиватся время жизни, которое определяется бизнес задачами. Например, Google устанавливет своим токенам время жизни 1 час.
  • Для формирования токена используется пакетjsonwebtoken
  • В JWT помещается email пользователя, который подписывается ключом сервера. Ключ это текстовая строка, которая хранится, как конфигурационный параметр на сервере.Содержимое токена кодируется Base64
  • SessionId сохраняется в базе и отправляется клиенту в заголовке X-CSRF-Token
  • Token сохраняется в базе и отправляется клиенту в теле HTTP ответа
  • Получив SessionId клиент сохраняетSessionId в sessionStorageбраузера
  • Token записывается в localStorage или Сookiesбраузерадля реализации возможности запомнить меня на этом устройстве
  • В дальнейшем клиент будет присылать SessionId в HTTP заголовкеX-CSRF-Token:

X-CSRF-Token: 69e530cf-b641-445e-9866-b23c492ddbab

  • Token будет присылаться в заголовкеHTTP Authorization с префиксомBearer:

Authorization: BearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

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

  1. Токен подписан сервером;
  2. Пользователь с email существует;
  3. Время жизни токена не истекло. Если же оно истекло, то при выполнении предыдущих условий автоматически формируется новый токен.

В общем случае излишне генерировать и SessionId и Token. В нашем PoC для примера реализованы оба механизма. Посмотрим внимательней на наш JWT. Пример закодированного JWT токена из заголовка:
Authorization:yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

Раскодируем его открытыми средствами. Например самый простой способ загрузить на сайтhttps://jwt.io

image

Мы видим, что информация, помещенная вJWT не зашифрована, а только подписана ключом сервера и туда нельзя помещать чувствительные к разглашению данные!

Аутентификация с помощью Google Account (Token-Based Authentication)
Тут посложнее, поэтому нарисую диаграмму:

image

1. Пользователь заходит на GUI PoC, выбирает Login with Google".
2. Клиент запрашивает у сервера SessionId и настройки Google. Настройки надо предварительно получить с сайта Google, например, по инструкции из моей статьи: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn
3. Клиент сохраняет SessionId в sessionStorageбраузера
4. Из браузера формируется GET запрос на GoogleAuthorization Server (красная стрелка). Для понимания алгоритма, надо обратить внимание, что ответ на этот запрос браузер получит от Google только в самом конце call flow на шаге 13 и это будет 307 (Redirect).

Формат GET запроса:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&scope=openid+email&redirect_uri=http://localhost:8081/redirect&access_type=offline&state=450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58

В этом запросе:
  • accounts.google.com/o/oauth2/v2/auth endpoint для начала аутентификации. У Google есть, кстати, адрес, по которому можно посмотреть актуальный список всех Google API endpoints:https://accounts.google.com/.well-known/openid-configuration
  • response_type=code параметр говорит, что ожидаем получить в ответAuthorization Code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • scope=openid email к каким данным пользователя мы хотим получить доступ
  • redirect_uri = localhost:8081/redirectCallback адрес, заданный при регистрации приложения. В нашем случае это адрес на нашем сервере, который получит запрос от Google
  • state = SessionId, который передается между клиентом, Google и сервером для защиты от вмешательства внешнего злоумышленника

5. Google Authorization Server показывает стандартную Google форму логина.
6. Пользователь вводит свои логин/пароль от Google аккаунта.
7. Google проверяет пользователя и делает GET запрос на адрес Callback с результатом аутентификации, Authorization Code в параметре code и нашем SessionId в параметреstate:
  • state: '450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58',
  • code: '4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ',
  • scope: 'email openid www.googleapis.com/auth/userinfo.email',
  • authuser: '1',
  • prompt: 'consent'

8. Сервер, не отправляя пока ответ на GET, формирует POST запрос с Authorization Code, Client ID и Client Secret:
POSThttps://oauth2.googleapis.com/token?code=4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&client_secret=SUmydv3-7ZDTIh8аНК85chTOt&grant_type=authorization_code&redirect_uri=http://localhost:8081/redirect

  • oauth2.googleapis.com/token это endpoint для получения token
  • code только что присланный code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • client_secret Client Secret, выданный при регистрацииприложения на Google (в примере указан не настоящий)
  • grant_type=authorization_code единственно возможное значение из стандарта

9. Google проверяет присланные данные и формирует access token в формате JWT (JSON Web Token), подписанный своим приватным ключом. В этом же JWT может содержаться и refresh token, c помощью которого возможно продолжение сессии после ее окончания:
  • access_token: 'ya29.a0AfH6SMBH70l6wUe1i_UKfjJ6JCudA_PsIIKXroYvzm_xZjQrCK-7PUPC_U-3sV06g9q7OEWcDWYTFPxoB1StTpqZueraUYVEWisBg46m1kQAtIqhEPodC-USBnKFIztGWxzxXFX47Aag',
  • expires_in: 3599,
  • refresh_token: '1//0cAa_PK6AlemYCgYIARAAGAwSNwF-L9IrdUt1gzglxh5_L4b_PwoseFlQA1XDhqte7VMzDtg',
  • scope: 'openid www.googleapis.com/auth/userinfo.email',
  • token_type: 'Bearer',
  • id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlZGMwMTJkMDdmNTJhZWRmZDVmOTc3ODRlMWJjYmUyM2MxOTcfobDCf2VrxXb6CCxoL_dZq1WnlEjBZx_Sf6Rg_tn3x4gWtusO1oe_bJx_gvlSLxtvSOdO_kPB2uGGQHr3xzF_Evr-S-BiGS8zMuIkslyN6fU7P7BdNVyOYAIYFvHikyIpAoesV2Fd2yBSngBwGmWfrHL7Z2415UrnlCG4H1Nw'

10. Сервер валидирует пришедший Token. Для этого нужен открытый ключ от Google. Актуальные ключи находятся по определенному адресу и периодически меняются:https://www.googleapis.com/oauth2/v1/certs.Поэтому надо или получать их каждый раз по этому адресу, либо хардкодить и следить за изменениями самому.Декодируем пришедший token, для нас самое главное это подтвержденный email пользователя:
decodedIdToken: {iss: 'https://accounts.google.com',azp: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',aud: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',sub: '101987547227421522632',email: 'work.test.mail.222@gmail.com',email_verified: true,at_hash: 'wmbkGMnAKOnfAKtGQFpXQw',iat: 1606220748,exp: 1606224348}

11. Проверяем, что email пользователя существует, или создаём нового пользователя c email из токена. При этомSessionId и полученные от Google токены сохраняются в базе.
12. Только теперь отвечаем Google HTTP кодом 307 (Redirect) и заголовком Location c адресом на клиенте:
HTTP Location:http://localhost:8080/googleLogin

13. И только теперь Google отвечает браузеру с тем же кодом307 (Redirect) и заголовком Location с заданным нами адресом
14. Браузер переходит на адрес, указанный вLocation иклиент определяет, что произошла успешная аутентификация пользователя с помощью Google аккаунта
15. Клиент, по сохраненному в sessionStorage SessionId, получает на сервере токен и данные пользователя
16. Клиент сохраняет токен в localStorage браузера

На этом процедура аутентификации с помощью Google аккаунта завершена и можно переходить к штатной работе приложения.

Прием, обработка и визуализация данных


Сделаем стандартный вид приложения, как рекомендует Google:
  • Drawer (navigation-drawer) в левой стороне
  • Меню сверху (v-app-bar)
  • Footer внизу (v-footer)
  • Для визуализации полученных данных используем карточки (Data Cards)

image

Для создания новых данных используется простая форма ввода:

image

  • При переходах между страницами необходимо проверять, что пользователь залогинен и в зависимости от этого отображать или не отображать определенные формы.
  • В приложениях, в которых используется больше одной страницы, необходимо передавать данные между формами и элементами интерфейса. Для простых случаев можно записывать данные в localStorage. Для более сложных удобнее использовать специализированные библиотеки, реализующие концепции управления состояниями:

  1. Для Vue есть библиотекаVuex, реализующаяstate management pattern.
  2. Есть универсальная библиотека Redux основанная на Flux от Facebook. Она реализует концепцию состояний, используя понятия action, state, view.

  • В примере я использовал Vuex.
  • Также необходимRouter для реализации переходов между страницами.

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

Архитектура


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

Заключение


  • Мы рассмотрели типичной проект PoC в архитектуре клиент-сервер для использования в вебе.
  • Поняли, что даже минимальный проект требует обширных знаний и значительных усилий для реализации.
  • В результате получили заготовку, которую можно использовать для начала любых проектов:

Исходники на Github:https://github.com/AlexeySushkov/common-test-db
Пример запущен в облаке Amazon, можно посмотреть, как он выглядит вживую:http://globalid.tech/

Остается только в канун Нового Года пожелать, чтобы сбылись все ваши самые заветные PoC!

It's only the beginning!
Подробнее..

Пишем full stack монолит с помощью Angular Universal NestJS PostgreSQL

10.08.2020 02:14:34 | Автор: admin
Привет, Хабр!

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


Эта статья будет полезна, если вы:


  • Начинающий fullstack-разработчик;
  • Стартапер, который пишет MVP чтобы проверить гипотезу.

Почему выбрал такой стек:


  • Angular: имею много опыта в нем, люблю строгую архитектуру и Typescript из коробки, выходец из .NET
  • NestJS: тот-же язык, та-же архитектура, быстрое написание REST API, возможность в дальнейшем пересесть на Serverless (дешевле виртуалки)
  • PostgreSQL: Собираюсь хоститься в Яндекс.Облаке, на минималках дешевле на 30% чем MongoDB

Прайс яндекса


Прежде чем написать статью, поискал на хабре статьи про подобный кейс, нашел следующее:



Из этого ничего не описывает "скопировал и вставил" или дает ссылки на то что еще нужно дорабатывать.


Оглавление:


1. Создаем Angular приложение и добавляем библиотеку компонентов ng-zorro
2. Устанавливаем NestJS и решаем проблемы с SSR
3. Делаем API на NestJS и подключаем к фронту
4. Подключаем базу данных PostgreSQL



1. Создаем Angular приложение


Установим Angular-CLI чтобы создавать SPA-сайты на Ангуляре:


npm install -g @angular/cli

Создадим Angular приложение с помощью следующей команды:


ng new angular-habr-nestjs

Далее переходим в папку приложения и запускаем, чтобы проверить работоспособность:


cd angular-habr-nestjsng serve --open

Статическое SPA-приложение на Angular


Приложение создалось. Подключаем библиотеку NG-Zorro:


ng add ng-zorro-antd

Далее выбираем следующие конфигурации библиотеки:


? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] No? Choose your locale code: ru_RU? Choose template to create project: sidemenu

Эта конфигурация заменит содержимое app.component на дизайн с менюшкой слева, футером и хедером и подключит локализацию на русском языке:


Подключили NG-Zorro


В данной статье мы отобразим список данных для наглядности, поэтому добавим простенькую табличку в компоненте src/app/pages/welcome, который сгенерил NG-Zorro:
Пример взят отсюда:
https://ng.ant.design/components/table/en


// welcome.component.html<nz-table #basicTable [nzData]="items$ | async"> <thead> <tr>  <th>Name</th>  <th>Age</th>  <th>Address</th> </tr> </thead> <tbody> <tr *ngFor="let data of basicTable.data">  <td>{{ data.name }}</td>  <td>{{ data.age }}</td>  <td>{{ data.address }}</td> </tr> </tbody></nz-table>

// welcome.module.tsimport { NgModule } from '@angular/core';import { WelcomeRoutingModule } from './welcome-routing.module';import { WelcomeComponent } from './welcome.component';import { NzTableModule } from 'ng-zorro-antd';import { CommonModule } from '@angular/common';@NgModule({ imports: [  WelcomeRoutingModule,  NzTableModule, // Добавили для таблицы  CommonModule // Добавили для пайпа async ], declarations: [WelcomeComponent], exports: [WelcomeComponent]})export class WelcomeModule {}

// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = of([  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]); constructor(private http: HttpClient) { } ngOnInit() { } // Сразу напишем метод к бэку, понадобится позже getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Получилось следующее:


Табличка NG-Zorro



2. Устанавливаем NestJS


Далее установим NestJS таким образом, чтобы он предоставил Angular Universal (Server Side Rendering) из коробки и напишем пару ендпоинтов.


ng add @nestjs/ng-universal

После установки, запускаем наш SSR с помощью команды:


npm run serve

И вот уже первый косяк :) У нас появляется следующая ошибка:


TypeError: Cannot read property 'indexOf' of undefined  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:35:43  at D:\Projects\angular-habr-nestjs\dist\server\main.js:107572:13  at View.engine (D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:30:11)  at View.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\view.js:135:8)  at tryRender (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:640:10)  at Function.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:592:3)  at ServerResponse.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\response.js:1012:7)  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\angular-universal.module.js:60:66  at Layer.handle [as handle_request] (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\layer.js:95:5)  at next (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\route.js:137:13)

Чтобы решить косяк, зайдем в файл server/app.module.ts и поменяем значение liveReload на false:


import { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }) ]})export class ApplicationModule {}

Также подтюним конфиг тайпскрипта, так-как эта конфигурация не взлетает с использованием Ivy рендера:


// tsconfig.server.json{ "extends": "./tsconfig.app.json", "compilerOptions": {  "outDir": "./out-tsc/server",  "target": "es2016",  "types": [   "node"  ] }, "files": [  "src/main.server.ts" ], "angularCompilerOptions": {  "enableIvy": false, // Добавили флажок  "entryModule": "./src/app/app.server.module#AppServerModule" }}

После пересоберем приложение командой ng run serve чтобы SSR заработал.


Angular SSR + NestJS


Ура! SSR подрубился, но как видимо в devtools он приходит с кривыми стилями.


Добавим extractCss: true, который позволит выносить стили не в styles.js, а в styles.css:


// angular.json..."architect": {    "build": {     "builder": "@angular-devkit/build-angular:browser",     "options": {      "outputPath": "dist/browser",      "index": "src/index.html",      "main": "src/main.ts",      "polyfills": "src/polyfills.ts",      "tsConfig": "tsconfig.app.json",      "aot": true,      "assets": [       "src/favicon.ico",       "src/assets",       {        "glob": "**/*",        "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",        "output": "/assets/"       }      ],      "extractCss": true, // Добавили флажок      "styles": [       "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",       "src/styles.scss"      ],      "scripts": []     },...

Также подключим стили библиотеки в app.component.scss:


// app.component.scss@import "~ng-zorro-antd/ng-zorro-antd.min.css"; // Подключили стили:host { display: flex; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.app-layout { height: 100vh;}...

Теперь стили подключены, SSR отдает страничку со стилями, но мы видим что сначала у нас грузится SSR, потом страница моргает и отрисовывается CSR (Client Side Rendering). Это решается следующим способом:


import { NgModule } from '@angular/core';import { Routes, RouterModule } from '@angular/router';const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: '/welcome' }, { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) }];@NgModule({ imports: [RouterModule.forRoot(routes, {initialNavigation: 'enabled', scrollPositionRestoration: 'enabled'})], // Добавили initialNavigation, scrollPositionRestoration exports: [RouterModule]})export class AppRoutingModule { }

  • initialNavigation: 'enabled' дает инструкцию роутингу не отрисовывать страницу, если уже загружена через SSR
  • scrollPositionRestoration: 'enabled' скролит страницу наверх при каждом роутинге.


3. Сделаем пару ендпоинтов на NestJS


Перейдем в папку server и создадим первый контроллер items:


cd servernest g module itemsnest g controller items --no-spec

// items.module.tsimport { Module } from '@nestjs/common';import { ItemsController } from './items.controller';@Module({ controllers: [ItemsController]})export class ItemsModule {}

// items.controller.tsimport { Controller } from '@nestjs/common';@Controller('items')export class ItemsController {}

Контроллер и модуль создались. Создадим метод на получение списка items и на добавление объекта в список:


// server/src/items/items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';class Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { // для простоты данные взял из Angular private items: Item[] = [  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]; @Get() getAll(): Item[] {  return this.items; } @Post() create(@Body() newItem: Item): void {  this.items.push(newItem); }}

Попробуем вызвать GET в Postman:


GET запросы апишки NestJS


Отлично, работает! Обратите внимание, вызываем метод GET items с префиксом api, который ставится автоматически в файле server/main.ts при установке NestJS:


// server/main.tsimport { NestFactory } from '@nestjs/core';import { ApplicationModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(ApplicationModule); app.setGlobalPrefix('api'); // Это префикс await app.listen(4200);}bootstrap();

Теперь прикрутим бэк к фронту. Возвращаемся к файлу welcome.component.ts и делаем запрос списка к бэку:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Можно увидеть что апиха на фронте дергается, но также дергается и в SSR, причем с ошибкой:


Дергание апихи в SSR


Ошибка при запросе в SSR решается следующим способом:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('http://localhost:4200/api/items').pipe(share()); // Прописали полный путь к апихе чтобы SSR не ругался }}interface Item { name: string; age: number; address: string;}

Чтобы исключить двойной запрос к апихе (один на SSR, другой на фронте), нужно проделать следующее:


  • Установим библиотеку @nguniversal/common:

npm i @nguniversal/common

  • В файле app/app.module.ts добавим модуль для запросов из SSR:

// app.module.tsimport { BrowserModule } from '@angular/platform-browser';import { NgModule } from '@angular/core';import { AppRoutingModule } from './app-routing.module';import { AppComponent } from './app.component';import { IconsProviderModule } from './icons-provider.module';import { NzLayoutModule } from 'ng-zorro-antd/layout';import { NzMenuModule } from 'ng-zorro-antd/menu';import { FormsModule } from '@angular/forms';import { HttpClientModule } from '@angular/common/http';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';import { NZ_I18N } from 'ng-zorro-antd/i18n';import { ru_RU } from 'ng-zorro-antd/i18n';import { registerLocaleData } from '@angular/common';import ru from '@angular/common/locales/ru';import {TransferHttpCacheModule} from '@nguniversal/common';registerLocaleData(ru);@NgModule({ declarations: [  AppComponent ], imports: [  BrowserModule.withServerTransition({ appId: 'serverApp' }),  TransferHttpCacheModule, // Добавили  AppRoutingModule,  IconsProviderModule,  NzLayoutModule,  NzMenuModule,  FormsModule,  HttpClientModule,  BrowserAnimationsModule ], providers: [{ provide: NZ_I18N, useValue: ru_RU }], bootstrap: [AppComponent]})export class AppModule { }

Схожую операцию проделаем с app.server.module.ts:


// app.server.module.tsimport { NgModule } from '@angular/core';import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';import { AppModule } from './app.module';import { AppComponent } from './app.component';@NgModule({ imports: [  AppModule,  ServerModule,  ServerTransferStateModule, // Добавили ], bootstrap: [AppComponent],})export class AppServerModule {}

Хорошо. Теперь получаем данные из апи в SSR, отрисовываем на форме, отдаем на фронт и тот не делает повторных запросов.


Запроса нет, данные есть!



4. Подключим базу PostgreSQL


Подключим библиотеки для работы с PostgreSQL, также будем использовать TypeORM для работы с базой:


npm i pg typeorm @nestjs/typeorm

Внимание: у вас уже должна быть установлена PostgreSQL с базой внутри.


Описываем конфиг подключения к базе в server/app.module.ts:


// server/app.module.tsimport { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';import { ItemsController } from './src/items/items.controller';import { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }),  TypeOrmModule.forRoot({ // Конфиг подключения к базе   type: 'postgres',   host: 'localhost',   port: 5432,   username: 'postgres',   password: 'admin',   database: 'postgres',   entities: ['dist/**/*.entity{.ts,.js}'],   synchronize: true  }) ], controllers: [ItemsController]})export class ApplicationModule {}

Немного про поля конфига:


  • type: указываем название типа базы данных, к которой подключаемся
  • host и port: место где база хостится
  • username и password: аккаунт для этой базы
  • database: название базы
  • entities: путь, откуда будем брать сущности для схемы нашей базы

По последнему пункту, нужно создать сущность Item для мапинга полей в базу:


// server/src/items/item.entity.tsimport { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm/index';@Entity()export class ItemEntity { @PrimaryGeneratedColumn() id: number; @CreateDateColumn() createDate: string; @Column() name: string; @Column() age: number; @Column() address: string;}

Далее свяжем эту сущность с нашей базой.


// items.module.tsimport { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { ItemEntity } from './item.entity';import { ItemsController } from './items.controller';@Module({ imports: [  TypeOrmModule.forFeature([ItemEntity]) // Подключаем фича-модуль и указываем сущности базы ], controllers: [ItemsController]})export class ItemsModule {}

Теперь укажем в контроллере, что хотим работать с базой, а не кешем:


// items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';import { ItemEntity } from './item.entity';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm/index';interface Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { constructor(@InjectRepository(ItemEntity)       private readonly itemsRepository: Repository<ItemEntity>) { // Подключили репозиторий } @Get() getAll(): Promise<Item[]> {  return this.itemsRepository.find(); } @Post() create(@Body() newItem: Item): Promise<Item> {  const item = this.itemsRepository.create(newItem);  return this.itemsRepository.save(item); }}

Проверим работу апихи в Postman:


POST к апихе с базой


Работает. Потыкали несколько раз постман, посмотрим что записалось в базе с помощью DBeaver:


Записи в базе


Отлично! В базе есть, посмотрим как выглядит на фронте:


Рабочее fullstack приложение


Готово! Мы сделали fullstack приложение, с которым можно работать дальше.


P.S. Сразу поясню следующее:


  • Вместо Ng-Zorro вы можете использовать любую другую библиотеку, например Angular Material. Мне она лично не зашла из-за сложности разработки;
  • Я знаю, что нужно на бэке использовать сервисы, а не напрямую дергать базу в контроллерах. Эта статья о том, как решив проблемы "влоб" получить MVP с которым можно работать, а не про архитектуру и паттерны;
  • Вместо вписывания на фронте http://localhost:4200/api возможно лучше написать интерсептор и проверять откуда мы стучимся

Полезные ссылки:


Подробнее..

Как работать в команде, которая пишет на 5 языках

23.04.2021 12:19:27 | Автор: admin

Привет, Хабр! Меня зовут Евгений Сальников, я тимлид одной из команд доставки в компании Lamoda. В нашей команде используются сразу пять языков программирования: PHP, Go, Vue, Typescript, Java и Kotlin. Когда я впервые услышал об этом на собеседовании, подумал, что так работать невозможно все слишком разное. Но спустя год мое мнение кардинально изменилось, и я нашел много преимуществ в таком подходе.


В этой статье я расскажу:


  • о нашем направлении и системах, обслуживающих доставку;
  • как мы живем с таким разнообразием технологий;
  • как планируем работу над разноплановыми задачами;
  • как растим экспертизу в fullstack-команде и расширяем направление.


Почему fullstack?


Мы часто слышим про fullstack это устоявшееся понятие, даже избитое. Оно подразумевает, что один человек может пилить и бекенд, и фронтенд все в одном флаконе. Сейчас в нашем направлении уже четыре команды из 27 человек, пять языков и четыре системы.


  1. DataMatrix это система для маркировки товаров. В нашей стране существует закон 487-ФЗ, по которому мы обязаны помечать ряд товаров QR-кодом. Маркировка содержит информацию о том, кто произвел этот товар, кто ввез в страну, когда он поступил в продажу. Это помогает понять, насколько легальная вещь лежит перед нами. Подробнее о DataMatrix рассказывали в отдельной статье.
  2. Система Express стоит в каждом пункте выдачи заказов и во всех транзитных складах. Она целиком написана с нуля внутри Lamoda, поэтому там учтены все наши процессы и потребности. На текущий момент вся система доставки построена на Express.
  3. Система XDC взаимодействует с внешними службами доставки с Почтой России, DPD и остальными.
  4. Также в зоне ответственности нашего направления мобильное приложение для торговых представителей, у которых есть планшет или телефон с ПО нашей разработки.

Мы стараемся стандартизировать все системы. Например, если что-то пишем на РНР, то делаем это одинаково: используем одинаковые фреймворки и библиотеки. Но есть задачи, которые нужно решать иными способами. И в таком случае я не разделяю подход, что все можно написать на одном языке программирования. Как правило, это не всегда оправдано и может приводить к неожиданностям. Для разных задач уже существуют готовые наработки или другие технологии. На мой взгляд, это нормально.


Мой любимый пример Apache Camel. Это интеграционный фреймворк на Java, который, грубо говоря, перекладывает данные из одного места в другое. У нас в компании эта технология обеспечивает взаимодействие с внешними курьерскими службами: мы получаем данные о заказе из API, преобразовываем их и отправляем в курьерскую службу. Написать эту задачу на РНР возможно, но будет неоправданно, потому что Apache Camel и так уже создан для этих целей. Такой подход позволяет легче адаптировать новые службы и новые API, тратить меньше времени на преобразование запроса из Json в XML. В Lamoda это адаптированная технология: если одна команда научилась ее готовить, мы делимся знаниями с коллегами. Сейчас Apache Camel используется уже в четырех командах.


Распределение ролей, покер-планирование и рост экспертизы


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



Например, у нас есть задача сделать удобную форму возврата. Для контроля технической стороны этого проекта мы назначаем техлида, которым может стать любой инженер из команды. Уже заранее он понимает, что для поставленной цели придется затронуть две системы: Express и XDC.


На схеме видно, что у Express и XDC также есть техлиды. У Express их даже два это очень большая система. Таким образом, техлид проекта сообщает техлидам систем о необходимых доработках, составляет и декомпозирует задачи, следит за тем, чтобы работа шла консистентно.


В то же время на всем направлении работает системный архитектор. Это серьезный специалист, который помогает всей разработке двигаться в едином направлении.


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



Как я уже упоминал ранее, любой инженер может дорабатывать каждую из наших систем. Допустим, человек начинал работать только с одной из них чаще всего с Express, так как все наши системы и процессы так или иначе связаны с доставкой. Новичку мало понимать ее технически, требуется знать предметную область и разбираться в процессах, которые там происходят. Таким образом, человеку, который изучил одну систему, будет намного проще работать над задачками по доработке другой. Порог вхождения во что-то новое становится гораздо ниже.


Приведу несколько примеров нашего подхода к работе:


  • Раньше я часто сталкивался с тем, что есть отдельная команда для бэкенда и API мобильного приложения, а другая команда дорабатывает само приложение. Это требует формализации всех процессов и задач. У нас же один и тот же человек в одном спринте дорабатывает бэкенд и API мобильного приложения, да и само мобильное приложение. И это не разные задачи, а один проект по общим изменениям. На мой взгляд, это позволяет двигаться быстрее.
  • Следующий пример о том, как мы близки к инфраструктуре. В нашем направлении используется K8s и Atlassian. Все скрипты для разворачивания новых серверов или деплоя приложения также создаются внутри команды. Любой из наших инженеров может поправить скрипт деплоя или что-то написать на Ansible, чтобы развернуть новый сервер. Благодаря этому мы быстрее делаем доработки.
  • В нашей команде есть сервисы, написанные на Go, но исключительно утилиты. Часто бывает так, что нам нужно запросить миллион Data matrix у внешнего API. В этом случае писать большие истории на РНР невыгодно, потому что для этих целей создан Go. Это бы усложнило ситуацию, если инженер совсем не знаком со сторонними API и с нашими процессами. Но наши ребята могут написать нужную утилиту на подходящем языке. Go адаптирован к нашей компании. У нас есть экспертиза в этом, мы можем пойти к соседним командам и уточнить у них все вопросы.

Год назад я не верил, что такая работа возможна. Но все мои страхи о том, что ее невозможно планировать, не подтвердились. Практика показывает, что fullstack-работой можно управлять и такой подход значительно развивает экспертизу в команде.


Мои итоги спустя год: как расширять fullstack-команду


Fullstack-программист стоит дороже, чем остальные. В HR-отделе на вас посмотрят с недоумением, когда вы попросите найти специалиста, который умеет Go, Java, Kotlin и отлично знает РНР.


Есть три способа увеличить команду:


1. Брать со стороны. Чтобы попасть в нашу команду, не нужно знать все пять языков. На старте большинство из нас были с ними не знакомы. Достаточно знать РНР и SQL и не бояться работы с остальными технологиями и языками. Мы на каждом интервью сразу же говорим о том, что у нас используется несколько языков.


Также у нас выстроен процесс онбординга он длится 3 месяца. Каждый новичок получает план из набора технических задач, которые в совокупности охватывают все наши системы и технологии. Например, сначала РНР-разработчик начинает знакомиться с системой Express, а потом может выбрать другие задачи, исходя из своих интересов: кому-то больше интересен Kotlin, кто-то уже имеет экспертизу на Go. Также происходит постепенное погружение и в процессы команды: дежурства, знакомство с системой мониторинга, выполнение саппорт-задач.


2. Растить внутри. Процесс, запущенный на онбординге, не заканчивается ровно через 3 месяца. Человек по-прежнему продолжает погружаться в наши процессы, а его технологическая экспертиза растет. Со временем он может занять роль техлида. Для этого инженер знакомится сначала с системой, потом с бизнес-требованиями к ней, а со временем понимает, что бизнес-требования слишком велики для одной задачи. В таком случае он может стать техлидом этого проекта или системы. Погружение в такую работу происходит только через практику.


Новичкам же мы даем исключительно технические задачи, которые не сильно влияют на бизнес-составляющие. Поэтому там нет горящих сроков, и никто не будет торопить человека, который только недавно начал разбираться в новой области знаний. Мы понимаем, что если сотрудник впервые видит систему и эту технологию, погружение займёт какое-то время. С ростом вовлеченности инженера растет бизнес-составляющая этих задач и экспертиза сотрудника.


В таком подходе важно понимать, что разработчик это инженер. Для меня инженер это человек, который продолжает учиться, искать что-то новое и понимает, какие технологии лучше подходят для решения конкретной задачи. Ему важно постоянно развиваться, не зацикливаясь на чем-то одном. Я верю, что смена проекта помогает разнообразить жизнь инженера. Очень здорово, когда в команде есть несколько проектов, между которыми можно переключиться и попробовать разное. Это не даст заскучать.


3. Смена технологий. Это тоже отличный путь развития. Сначала я познакомился с РНР, и казалось, что на нем можно сделать абсолютно все. Но сейчас будет странно писать на РНР постоянно живущие воркеры. Для этого можно использовать другие языки. Или, например, когда я познакомился с Go, это казалось крутым, но чего-то не хватало, зная количество готовых библиотек для Java. Новые инструменты помогают лучше понять те, которыми уже владеешь.


За неполный год в мою команду добавилось еще 10 инженеров. Сейчас это уже целое направление. Но не все готовы работать в таком подходе: кому-то не зашёл фронтенд, кому-то не заходит разнообразие технологий. Это не значит, что ушедшие люди плохие инженеры. Мы работаем в огромной компании, поэтому возможен переход в другие команды внутри Lamoda. Бывают и обратные случаи, когда люди из других команд переходят в наше направление, и это тоже круто.


Новые языки и комплектность команды


Мы уже сталкивались с появлением новых языков: кто-то сразу же на них кидается, кто-то ждет, когда появятся стабильные версии. Мы не отрицаем тот факт, что какие-то задачи можно реализовать легче или быстрее на других технологиях. Да, не нужно зацикливаться на чем-то одном, но лучше здраво подходить к каждому выбору. Если появятся новые языки или новые технологии, которые позволят нам двигаться быстрее, конечно, мы их рассмотрим.


Также иногда встает вопрос о комплектности команды: сколько человек должны знать каждый язык. Такого минимума у нас нет. Приведу пример: человек, который знает один из языков, взял бы переходящую роль техлида и проект, где предполагается использовать новую технологию. У нас есть архитектурные комитеты, где мы доказываем, что такая технология оправдана. Если она уже адаптирована в компании, то мы можем брать ее в работу без архитектурного комитета. Техлид нарезает задачи часть из них будет исключительно техническими. В процессе реализации остальные участники команды познакомятся ближе с нужной технологией. Таким образом, мы взращиваем экспертизу. И постепенно все больше и больше инженеров знакомится с новой технологией. В конечном счете этот проект в команде будет реализовывать точно не один человек.


Подведу итог: расширять fullstack-команду возможно. Практика показывает, что можно брать инженеров со стороны и можно растить их внутри компании. Если у вас есть свой опыт формирования fullstack-команды, поделитесь им в комментариях.

Подробнее..

Сервис на языке Dart flutter web-страница

11.08.2020 12:12:28 | Автор: admin
Оглавление
  1. 1. Введение
  2. 2. Backend
  3. 2.1. Инфраструктура.
  4. 2.2. Доменное имя. SSL
  5. 2.3. Серверное приложение на Дарт
  6. ...
  7. 3. Web
  8. 3.1. FlutterWeb страница (мы находимся здесь)
  9. ...
  10. 4. Mobile
  11. ...


Подготовка


В прошлый раз мы закончили на том, что наш веб-сервер получил доменное имя и научился устанавливать безопасное соединение с клиентом. Однако нам пока совсем нечего показать нашему будущему пользователю. Хотя мы уже можем поделиться идеей стартапа и сообщить дату релиза MVP. Для такой задачи подойдёт информационная web-страница. Напишем её на Dart с использованием фреймворка FlutterWeb. Все наши клиентские приложения сервиса станут расширением именно этой страницы. Постараемся вести разработку максимально адаптивно и структурировано, чтобы развитие и сборки под нужные платформы (web-android-iOS) стали просто рутиной.



Начнём с установки Flutter:

  • Установим git
  • Склонируем репозиторий с beta версией Flutter командой
    git clone https://github.com/flutter/flutter.git -b beta
    
  • Для запуска команд flutter из командной строки необходимо указать в операционной системе путь до его исполняемых файлов. Откроем переменные ОС, для этого начнём вводить изменение переменных среды текущего пользователя в строке поиска



    В окне выберем переменную Path и нажмём Изменить. В открывшемся списке создадим новую строку с адресом до исполняемых файлов flutter в файловой системе, например C:\flutter\bin
  • Установим расширение VScode для flutter
  • Перезапустим VScode (чтобы применились новые переменные ОС) и в терминале проверим состояние flutter командой
    flutter doctor
    



    здесь самое важное, что flutter установлен в beta версии (с поддержкой web разработки)
  • Теперь активируем веб разработку командой
    flutter config --enable-web
    

Создание нового проекта и запуск отладки


Создаём новый проект командой
flutter create <название проекта>

Сразу откроем его в VScode командой
code <название проекта>

Откроем файл main.dart в папке lib и запустим отладку командой F5:



Возможно при первом запуске отладки понадобится выбрать Chrome устройством для отладки:



Удалим содержимое файла main.dart. Добавим пустой метод main и корневой класс приложения, возвращающий в методе build() экземпляр MaterialApp:



Далее создадим следующий набор вложенных папок проекта:



Кратко опишем назначение каждой из них:

  • di механизм для связи между компонентами приложения. Здесь будут создаваться и регистрироваться все необходимые сервисы, репозитории, сетевые клиенты, необходимые для работы приложения. Я буду использовать библиотеку GetIt
  • domain data-объекты. Классы представления данных
  • res цвета, строки, импорты путей к картинкам и шрифтам. Всё, что связано со статическими ресурсами
  • service сервисы для работы с данными
  • ui интерфейс
  • utils вспомогательные классы

В файле pubspec.yaml добавим необходимые зависимости:



Подготовка к масштабированию элементов UI


Предполагается, что наша страница должна адаптироваться в зависимости от размеров экрана клиентского устройства и его расположения (портретный или ландшафтный режим).
Начнём с картинок заднего фона. Их подготовка не входит в тему статьи, поэтому просто оставлю здесь эти две ссылки:
  • Pixabay.com хранилище контентных фотографий
  • Paint.net графический редактор

Готовые картинки разместим в папке /assets/images, в файле pubspec.yaml добавим этот путь к ресурсам:



Я предпочитаю доступ к ресурсам в виде дерева c параметрами. В данном случае путь к картинке заднего фона заглушки:
images.background(bool isPortrait).stub

Для этого в папке res создадим файл images.dart с классами адресов картинок:



Для масштабирования размеров интерфейса и шрифтов мы подключили библиотеку ScreenUtil. Её функциональность сводится к двум вещам:

  • Регистрация базового размера экрана. Здесь необходимо задать ширину и высоту экрана, для которого ведётся основная верстка и необходимость масштабирования шрифтов.
  • Набор расширений, позволяющий для чисел (num) применить масштабирующий коэффициент. Например 100.w означает, что результатом этого выражения будет для экрана шириной 1920dp => 100dp, а для экрана iPhone8 с шириной 414dp => 100х(414/1920) = 21,6dp. То есть в пять раз меньше. Также предусмотрены расширения для параметра высоты и размера шрифтов.

Создадим файл /utils/screen_util_ext.dart и статический метод инициализации в нём:



Вызов метода инициализации масштабирования добавим в метод build() корневого виджета:



Расширим функциональность библиотеки масштабирования несколькими дополнительными расширениями в файле /utils/screen_util_ext.dart:







Инъекция зависимостей


Пришло время внедрить механизм создания и регистрации компонентов приложения с помощью библиотеки GetIt. В папке lib/DI/ создадим файл di_container.dart. В нём напишем статический метод getItInit() и инициализируем экземпляр контейнера GetIt. Зарегистрируем первый компонент экземпляр класса Images:



Вызов метода инициализации добавим в main():



Доступ к компоненту Images будет выглядеть так:



Таким же образом зарегистрируем класс с ресурсами строками.

Страница-заглушка


Теперь в папке UI создадим файл stub.dart с классом страницы заглушки StubScreen, расширим базовый класс StatelessWidget и переопределим его абстрактный метод build(). Наша страница представляет собой картинку на заднем плане и два информационных блока перед ней, размещающихся в зависимости от ориентации экрана.







Репозитории и сервис


Для динамического отображения оставшегося до релиза времени необходимо:

  1. Получить с сервера настройки с датами начала разработки и релиза
  2. Создать поток событий изменения оставшегося времени
  3. Объединить эти данные, передав в выходной поток для отображения на UI

Опишем доменные объекты (POJO) для этих данных:





Репозитории для получения настроек и создания потока событий:





Сервис для логики событий:



Зарегистрируем эти компоненты в DI контейнере:



Виджет оставшегося времени


Оставшееся до релиза время можно представить как 4 числа: дни, часы, минуты, секунды. Представим эти параметры в виде перечисления:



Добавим функциональности параметрам с помощью расширения:



Виджет для отображения круговой шкалы, числа и подписи будет анимированным, для этого расширим класс StatefulWidget. Его особенность в том, что Element (построенное и отображаемое представление) соотносится не с самим виджетом, а с его состоянием (State). Состояние, в отличие от виджета мутабельно. То есть его поля могут быть изменены без полного пересоздания экземпляра.





Здесь необходимо уточнить, что такое Animation, AnimationController и TickerProviderStateMixin. Итак AnimationController обёртка над простым параметром double value. Значение этого параметра меняется линейно в пределах от 0,0 до 1,0 (также его можно менять в обратную сторону или сбрасывать в 0,0). Однако для изменения этого параметра используется специальный объект TickerProviderStateMixin, который является обязательным параметром для AnimationController и сообщает ему, что графический движок готов построить новый кадр. Получив такой сигнал AnimationController рассчитывает сколько времени прошло от предыдущего кадра и вычисляет насколько нужно изменить значение своего value. Объекты Animation подписываются на AnimationController и содержат в себе некоторую функцию зависимости выходного значения от линейного (по времени) изменения значения AnimationController.

Метод инициализации состояния initState() вызывается один раз при создании:



При уничтожении состояния виджета вызывается метод dispose():





Представлением виджета будет стек (Stack), с помещёнными в него AnimatedBuilder для числа и шкалы:



Остаётся реализовать графический примитив в виде дуги:



Добавим 4 таких виджета на экран заглушки:



Сборка и релиз


Перед сборкой приложения необходимо заменить название и описание приложения в файлах ./web/index.html ./web/manifest.json и pubspec.yaml.

Останавливаем отладку и собираем релиз приложения командой
flutter build web

Готовое приложение находится в директории ./build/web/. Обратите внимание, что файлы .last_build_id и main.dart.js.map служебные и могут быть удалены.
Разместим приложение на сервере, подготовленном в предыдущей статье. Для этого достаточно скопировать содержимое директории ./build/web/ в /public/ нашего сервера:
scp -r ./* root@91.230.60.120:/public/


Результат

Исходный код github

Вопросы и комментарии приветствуются. Пообщаться с автором можно в Telegram канале.

Вместо заключения


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

Сервис на языке Dart каркас серверного приложения

18.08.2020 12:17:00 | Автор: admin
Оглавление


Подготовка


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

В этой статье мы напишем приложение с использованием фреймворка Aqueduct, оценим его производительность и потребление ресурсов в разных режимах, напишем инструментарий для компиляции в нативное приложение для Windows и Linux, разберемся с миграциями схемы базы данных для доменных классов приложения и даже опубликуем наш инструментальный docker образ в публичный регистр DockerHub.



полезности
  • все скриншоты с кодом кликабельные
  • полный код здесь
  • консольные команды собраны в README.md проекта
  • комментарии, вопросы, советы приветствуются
  • пообщаться с автором можно в Телеграмм канале


Установка Aqueduct


Начнем с установки dart-sdk набора средств разработки на языке Dart. Установить его можно с использованием пакетного менеджера вашей операционной системы как предложено здесь. Однако, в случае Windows никакого пакетного менеджера в вашей системе по умолчанию не установлено. Поэтому просто:
  • Скачаем архив и распакуем его на диск C:
  • Теперь, чтобы наша операционная система знала, где искать исполняемые файлы, добавим необходимые пути. Откроем переменные ОС. Для этого начнем вводить изменение переменных среды текущего пользователя в строке поиска
  • В открывшемся окне выберем переменную Path и нажмем Изменить. В открывшемся списке создадим новую строку с адресом до исполняемых файлов dart в файловой системе, например, C:\dart-sdk\bin
  • Проверим, что dart и pub (пакетный менеджер dart) доступны
    dart --version
    



    pub -v
    


  • Возможно, чтобы новые пути стали доступны, придется перезагрузиться
  • Установим утилиту командной строки aqueduct CLI (command line interface)
    pub global activate aqueduct
    
    Проверим доступность
    aqueduct
    



Теоретически можно установить локально также сервер баз данных PostgreSQL. Однако Docker позволит нам избежать этой необходимости и сделает среду разработки подобной среде выполнения на сервере.

Генерация приложения


Итак, откроем папку нашего сервера в VsCode
code c:/docs/dart_server

Для тех, кто не видел первую и вторую статьи, исходный код можно склонировать из guthub репозитория:
git clone https://github.com/AndX2/dart_server.git
Создадим шаблон приложения:
aqueduct create data_app



Ознакомимся с содержимым шаблона проекта:
  • README.md заметка с описанием, как работать с aqueduct проектом, запускать тесты, генерировать API документацию и пр. Вспомогательный файл.
  • pubspec.yaml спецификция для пакетного менеджера pub. Здесь находятся сведения об используемых пакетах, названии, описании, версии проекта и пр.
  • config.yaml и config.src.yaml конфигурация для отладки и тестирования проекта соответственно. Мы не будем использовать этот способ конфигурирования.
  • analysis_options.yaml правила для линтера (утилиты подсветки ошибок в исходном коде). Вспомогательный файл.
  • .travis.yml конфигурация для системы автоматической сборки и тестирования (continuous Integration). Вспомогательный файл.
  • pubspec.lock и .packages автоматически сгенерированные файлы пакетного менеджера pub. Первый список всех зависимостей проекта, включая транзитивные и их конкретные версии, второй расположение скачанных пакетов зависимостей в файловой системе (кэше).
  • .dart_tool/package_config.json файл отчета о генерации кода нашего проекта, созданный aqueduct CLI. Вспомогательный файл.
  • bin/main.dart точка входа в приложение при локальном запуске (например, для отладки). Мы не будем использовать такой способ запуска (исключая тесты).
  • lib/channel.dart Фактически ApplicationChannel это и есть экземпляр нашего приложения. Aqueduct умеет запускать несколько таких экземпляров для более эффективной утилизации ресурсов CPU и RAM. Такие экземпляры работают в изолированных потоках (в Dart их называют isolate) и никак (почти) не могут взаимодействовать друг с другом.
  • lib/data_app.dart файл инкапсуляции импортов зависимостей. Позволяет объединить необходимые пакеты в условную (library) библиотеку dart_app
  • test/ автотесты. Здесь можно разместить юнит-тесты, поскольку механизм тестирования сетевого слоя рассчитан на локальный запуск приложения и не будет использоваться при разработке. Для тестов будем использовать Postman.


Конфигурация


Первая задача, которую предстоит решить, настройка приложения при запуске. Aqueduct имеет встроенный механизм извлечения параметров из конфигурационных файлов, но этот механизм не очень удобен при запуске в Docker контейнере. Мы поступим иначе:
  • Передадим список переменных в операционную систему контейнера.
  • При запуске приложения внутри контейнера прочитаем переменные окружения операционной системы и используем их для первоначальной настройки.
  • Создадим маршрут для просмотра по сети всех переменных окружения запущенного приложения (это будет полезно при просмотре состояния приложения из панели администратора).

В папке /lib создадим несколько папок и первый репозиторий для доступа к переменным окружения:


EnvironmentRepository в конструкторе считывает переменные окружения из операционной системы в виде словаря Map<String, String> и сохраняет в приватной переменной _env. Добавим метод для получения всех параметров в виде словаря:


lib/service/EnvironmentService логический компонент доступа к данным EnvironmentRepository:


Инъекция зависимостей


Здесь необходимо остановиться и разобраться с зависимостями компонентов:
  • сетевому контроллеру потребуется экземпляр сервиса переменных,
  • сервис должен быть единственным для всего приложения,
  • для создания сервиса необходимо предварительно создать экземпляр репозитория переменных.

Эти задачи решим с помощью библиотеки GetIt. Подключим необходимый пакет в pubspec.yaml:


Создадим экземпляр контейнера инжектора lib/di/di_container.dart и напишем метод с регистрацией репозитория и сервиса:


Метод инициализации контейнера DI вызовем в методе подготовки приложения:


Cетевой слой


lib/controller/ActuatorController сетевой http компонент. Он содержит методы доступа к служебным данным приложения:


Задекларируем обработчики маршрутов для контроллеров в lib/controller/Routes:


Первый запуск


Для запуска необходимо:
  • приложение упаковать в Docker образ,
  • добавить контейнер в сценарий docker-compose,
  • настроить NGINX для проксирования запросов.

В папке приложения создадим Dockerfile. Это скрипт сборки и запуска образа для Docker:


Добавим контейнер приложения в сценарий docker-compose.yaml:


Создадим файл data_app.env с переменными конфигурации для приложения:


Добавим новый location в отладочный конфиг NGINX conf.dev.d/default.conf:


Запускаем отладочный сценарий с флагом предварительной сборки образов:
docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up --build



Сценарий успешно запустился, но настораживают несколько моментов:
  • официальный образ со средой dart от google занимает 290MБ в виде архива. В распакованном виде он займет кратно больше места 754МБ. Посмотреть список образов и их размер:
    docker images
    

  • Время сборки и JIT-компиляции составило 100+ сек. Многовато для запуска приложения на проде
  • Потребление памяти в docker dashboard 300 МБ сразу после запуска
  • В нагрузочном тесте (только сетевые запросы GET /api/actuator/) потребление памяти находится в диапазоне 350390 МБ для приложения, запущенного в одном изоляте

Предположительно ресурсов нашего бюджетного VPS не хватит для работы такого ресурсоемкого приложения. Давайте проверим:
  • Создадим на сервере папку для новой версии приложения и скопируем содержимое проекта
    ssh root@dartservice.ru "mkdir -p /opt/srv_2" && scp -r ./* root@91.230.60.120:/opt/srv_2/
    
  • Теперь необходимо перенести в эту папку проект web-страницы из /opt/srv_1/public/ и все содержимое папки /opt/srv_1/sertbot/ (в ней находятся SSL сертификаты для NGINX и логи Lets encrypt бота), также скопируем ключ из /opt/srv_1/dhparam/
  • Запустим в отдельной консоли монитор ресурсов сервера
    htop
    

  • Выполним docker-compose сценарий в папке /opt/srv_2/
    docker-compose up --build -d
    

  • Так выглядит сборка приложения перед запуском:
  • А так в работе:

    Из доступного 1ГБ оперативной памяти наше приложение потребляет 1,5ГБ заняв недостающее в файле подкачки. Да, приложение запустилось, но ни о какой нагрузочной способности речь не идет.
  • Остановим сценарий
    docker-compose down
    


AOT


Нам предстоит решить три задачи:
  • снизить потребление оперативной памяти dart приложением,
  • уменьшить время запуска,
  • снизить размер docker-контейнера приложения.

Решением станет отказ от dart в рантайме. Начиная с версии 2.6, dart-приложения поддерживают компиляцию в нативный исполняемый код. Aqueduct поддерживает компиляцию начиная с версии 4.0.0-b1.
Начнем с локального удаления aqueduct CLI
pub global deactivate aqueduct

Установим новую версию
pub global activate aqueduct 4.0.0-b1

Поднимем зависимости в pubspec.yaml:


Соберем нативное приложение
aqueduct build

Результатом будет однофайловая сборка data_app.aot размером около 6 МБ. Можно сразу запустить это приложение с параметрами, например,
data_app.aot --port 8080 --isolates 2
Потребление памяти сразу после запуска менее 10 МБ.
Посмотрим под нагрузкой. Параметры теста: сетевые запросы GET /actuator, 100 потоков с максимальной доступной скоростью, 10 минут. Результат:


Итого: средняя скорость 13к запросов в сек для тела ответа JSON 1,4кВ, среднее время ответа 7 мсек, потребление оперативной памяти (на два инстанса) 42 MB. Ошибок нет.
При повторном тесте с шестью инстансами приложения средняя скорость, конечно, повышается до 19к/сек, но и утилизация процессора достигает 45% при потреблении памяти 64 МБ.
Это превосходный результат.

Упаковка в контейнер


Здесь мы столкнемся еще с одной сложностью: скомпилировать dart-приложение в натив мы можем только под текущую ОС. В моем случае это Windows10 x64. В docker-контейнере я, конечно, предпочел бы один из дистрибутивов Linux например, Ubuntu20_10.

Решением здесь станет промежуточный docker-стенд, используемый только для сборки нативных приложений под Ubuntu. Напишем его /dart2native/Dockerfile:


Теперь соберем его в docker-образ с именем aqueduct_builder:4.0.0-b1, перезаписав, если есть, старые версии:
docker build --pull --rm -f "dart2native\Dockerfile" -t aqueduct_builder:4.0.0-b1 "dart2native"


Проверим
docker images



Напишем сценарий сборки нативного приложения docker-compose.dev.build.yaml:


Запустим сценарий сборки
docker-compose -f docker-compose.dev.build.yaml up



Файл скомпилированного под Ubuntu приложения data_app.aot занимает уже 9 МБ. При запуске утилизирует 19 МБ оперативной памяти (для двух инстансов). Проведем локальное нагрузочное тестирование с теми же условиями, но в контейнере с проксированием NGINX (GET, 100 потоков):


В среднем 5,3к запросов в секунду. При этом потребление оперативной памяти не превысило 55 МБ. Размер образа уменьшился по сравнению с установленным dart и aqueduct c 840 МБ до 74 МБ на диске.

Напишем новый сценарий docker-compose.aot.yaml запуска приложения. Для этого заменим блок описания data_app, установив базовым образ пустой Ubuntu:20_10. Смонтируем файл сборки и изменим команду запуска:


Решим еще одну сервисную задачу: фактически сборочный docker-образ c установленными dart и aqueduct вполне себе переиспользуемый инструмент. Имеет смысл выгрузить его в общедоступный регистр и подключать как готовый скачиваемый образ. Для этого необходимо:
  • зарегистрироваться в публичном регистре, например, DockerHub,
  • авторизоваться локально с тем же логином
    docker login 
    
  • переименовать выгружаемый образ по схеме login/title:tag
    docker image tag a365ac7f5bbb andx2/aqueduct:4.0.0-b1
    
  • выгрузить образ в регистр
    docker push andx2/aqueduct:4.0.0-b1
    

    https://hub.docker.com/repository/docker/andx2/aqueduct/general

Теперь можно изменить наш сценарий сборки для использования публичного образа

Подключение базы данных


В aqueduct уже встроен ORM для работы с базой данных PostgreSQL. Для его использования необходимо:
  • Создать доменные объекты, описывающие записи в базе данных.
  • На основе доменных объектов и связей между ними сгенерировать файл миграции. Заметка: для записи в базу данных необходимо, чтобы в БД были подготовлены таблицы, чьи схемы подходят для хранения доменных объектов. Aqueduct предоставляет инструмент миграции, который фактически обходит все классы проекта, являющиеся расширением ManagedObject (управляемые объекты), прочитывает типы их полей и связей с другими управляемыми объектами и создает специальный файл, в котором описано изменение схемы таблиц и связей в базе данных по сравнению со схемой предыдущего файла миграции. Файлы миграции добавляются при каждой перегенерации схемы.
  • Применить файлы миграции к базе данных. Применение происходит последовательно, начиная с версии, которая записана в БД текущей.
  • В файлы миграции, сгенерированные aqueduct, можно вносить свои изменения, например определить реализацию метода seed() для добавления в БД каких-либо начальных данных.
  • Генерация и применение миграций производится aqueduct CLI.

Начнем с подключения нового docker-контейнера с БД PostgreSQL в сценарии docker-compose.aot.yaml. Готовый образ на основе Linux Alpine (компактная версия Linux для встраиваемых применений):


Здесь необходимо обратить внимание на файл переменных окружения data_db.env. Дело в том, что образ заранее настроен на использование этих переменных в качестве имени пользователя, хоста, порта и пароля доступа. Добавим эти переменные в файл:


Значения приведены условно.
Также смонтируем папку хоста ./data_db/ в контейнер для хранения данных БД.
Далее в приложении data_app добавим класс /service/DbHelper для подключения к базе данных, используя переменные окружения:


Создадим доменный объект, управляемый ORM, для получения настроек клиентского приложения:


Добавим репозиторий и сервис для добавления настроек и получения актуальной версии:




Сетевой контроллер:


Зарегистрируем новые компоненты в DI контейнере:


Добавим новый контроллер и эндпойнт в маршрутизатор:


Теперь сгенерируем файл миграции базы данных. Выполним
aqueduct db generate
Результатом будет создание файлов миграции в папке проекта:




Теперь нужно решить сервисную задачу: миграции нужно применять из системы с установленным aqueduct (и dart) к базе данных, работающей в контейнере, и это нужно выполнять как при локальной разработке, так и на сервере. Используем для этого кейса ранее собранный и опубликованный образ для AOT-сборки. Напишем соответствующий docker-compose сценарий миграции БД:


Интересная деталь строка подключения к БД. При запуске сценария можно передать в качестве аргумента файл с переменными окружения, а затем использовать эти переменные для подстановки в сценарии:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env --compatibility up --abort-on-container-exit

Также обратим внимание на флаги запуска:
  • --compatibility совместимость с версиями docker-compose сценариев 2.х. Это позволит использовать параметры deploy для ограничения использования ресурсов контейнером, которые игнорируются в версиях 3.х. Мы ограничили потребление оперативной памяти до 200МБ и использование процессорного времени до 50%
  • --abort-on-container-exit этот флаг устанавливает режим выполнения сценария таким образом, что при остановке одного из контейнеров сценария будут завершены все остальные. Поэтому, когда выполнится команда миграции схемы базы данных и контейнер с aqueduct остановится, docker-compose завершит также и работу контейнера базы данных


Публикация


Для подготовки к публикации приложения необходимо:
  • Изменить переменные окружения в data_app.env и data_db.env. Напомню, что сейчас у нас POSTGRES_PASSWORD=postgres_password
  • Переименовать сценарий запуска нативного приложения docker-compose.aot.yaml в docker-compose.yaml. Команда запуска приложения на сервере не должна иметь аргументов
  • Временно заблокировать маршрут просмотра переменных окружения запущенного приложения /api/actuator. В следующей статье мы реализуем механизм авторизации по ролям и откроем доступ к этому маршруту только для администратора.

Скопируем на сервер папку приложения ./data_app/. Важным моментом здесь будет ключ -p (копировать с сохранением атрибутов файлов) в команде копирования. Напомню, что при сборке нативного приложения мы установили права на исполнение файлу data_app.aot:
scp -rp ./data_app root@dartservice.ru:/opt/srv_1

Скопируем также:
  • Измененную конфигурацию NGINX ./conf.d/default.conf
  • Сценарии запуска и миграции docker-compose.yaml, docker-compose.migrations.yaml
  • Файлы с переменными окружения data_app.env и data_db.env

Добавим на сервере папку /opt/srv_1/data_db. Это том файловой системы хоста для монтирования в контейнер базы данных. Здесь будут сохраняться все данные PostgreSQL.
mkdir /opt/srv_2/data_db

Выполним сценарий миграции схемы базы данных:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env up --abort-on-container-exit

Запустим сценарий приложения
docker-compose up -d


Исходный код github.

Вместо заключения


Каркас для бэкенд приложений готов. В следующей статье на его основе мы напишем новое приложение для авторизации пользователей сервиса. Для этого мы воспользуемся спецификацией oAuth2 и интегрируемся с VK и Github.
Подробнее..

Сервис на языке Dart введение, инфраструктура бэкэнд

21.07.2020 16:21:44 | Автор: admin
Оглавление
1. Введение
2. Backend
2.1. Инфраструктура.
2.2. Доменное имя. SSL.
2.3. Серверное приложение на Dart.

3. Web
3.1. Заглушка Under construction

4. Mobile


Введение


Меня, Flutter-разработчика, знакомые часто спрашивают: Что же такое язык Dart?. Качают головой со словами: А вот Петя серьёзные транспорты на Java пишет, а в Яндексе вообще плюсы в проде.... Ну что ж, пожалуй, действительно, Dart далёк от практик фабрик для создания фабрик из Java. Однако если стоит задача реализовать клиентские приложения сразу для нескольких платформ, не утонув в потоке задач по синхронизации разработчиков разных целевых ОС; создать целостный UI, узнаваемый, но специфичный для Android, iOS и веб и в целом уложиться в адекватные бюджет и сроки, здесь Flutter не имеет конкурентов. И эти вопросы стоят вдвойне если у вас стартап.

Итак, легенда: некий стартап решил создать новый сервис ну, например, для
обмена списками покупок
Такая себе идея для стартапа, я знаю, но если я выпущу ещё один ToDo лист в этот мир, мне будет стыдно :)
между пользователями сервиса. Цель стартапа выпустить MVP за три месяца на трех платформах (плюс четвертая сервер, конечно).

10 лет назад я бы сказал, что этот кейс не имеет решения и постарался бы держаться от него подальше, 3 года назад решением мог стать стек ReactNative/React/NodeJs, в 2020 году для этого есть Dart. Добро пожаловать в атмосферу разработки альфа версии сервиса, я постараюсь наглядно пройти и объяснить весь процесс разработки. Код всех приложений будет выложен в паблик. Комментарии, включая набросы и холивары, приветствуются. Спросить автора по существу или просто посоветоваться можно в Telegram канале нашего отдела.



Инфраструктура бекэнд


Типовым способом размещения серверного приложения является, конечно, VPS (виртуальный приватный сервер). Фактически это часть физического сервера в дата центре, ресурсы которого (ядра процессора и оперативная память) разделены с помощью технологии виртуализации (о наиболее распространённых технологиях аппаратной виртуализации можно почитать здесь XEN, KVM). Внимание: технологии программной виртуализации (OpenVZ, Virtuozzo) для нашей задачи могут не подойти из-за конфликтов с Docker и агрессивным оверселлингом (зачастую, при внимательном прочтении договора аренды такого VPS, можно обнаружить, что провайдеры гарантируют не менее 5% (!) утилизации ядра арендуемого процессора. Это означает, что провайдер планирует продать наше ядро процессора 20 (!) раз).

Итак, приобретём бюджетный VPS со следующими характеристиками: 1 ядро процессора, 1ГБ оперативной памяти, 10ГБ накопителя (в моём случае, это гибридный HDD). В качестве операционной системы выберем Ubuntu, желательно одной из LTS версий. После чего в электронную почту придёт сообщение об активации сервера с логином и паролем доступа по SSH (зашифрованный доступ к консоли операционной системы нашего VPS) в формате SSH:

IP-адрес: 91.230.60.120
Пользователь: root
Пароль: <Пароль>

Проверим подключение, введя в командной строке:

ssh root@91.230.60.120

и, по запросу:

password: <Пароль>

Результатом должен быть вывод сведений о виртуальном сервере и поле ввода внизу:

Server is hosted by хххххххххх

Hostname: 91.230.60.120
Kernel: 3.19.0-22-generic (Ubuntu хх.хх LTS)
Uptime: 09:07:06 up 3 days, 17:17, 1 user, load average: 0.00, 0.01, 0.05
CPU: Intel Xeon CPU 0 @ 2.00GHz (1 cores)
Memory: 989 MB total / 723 MB free

root@91.230.60.120:~$

Поздравляю, наш виртуальный сервер создан и доступен для работы.

Теперь определимся со структурой бекэнд. Нам понадобится HTTP сервер. Мы будем использовать NGINX. Его задачами будут:

  1. Раздача статических файлов (файлы веб приложения).
  2. Раздача служебных ресурсов, например, файлов подтверждения владения доменом для мобильных приложений, сведений о владельце для получения SSL сертификатов Lets encrypt и пр.
  3. Reverse proxy для доступа к серверным приложениям.
  4. Шифрование соединений https.

Два серверных приложения:

  1. Приложение регистрации и авторизации пользователей. Назовём его auth_app.
  2. Приложение с данными. Назовём его app.
  3. Для каждого из приложений п.2 нам понадобится отдельная база данных PostgreSQL.
  4. Приложение для автоматического получения и обновления сертификатов шифрования SSL (в следующей статье).

Очевидно, что такой зоопарк приложений необходимо изолировать друг от друга, а также заблокировать доступ извне. Для этого мы будем использовать технологию контейнеризации Docker и менеджер контейнеров Docker compose. В виде схемы это можно представить так:

image

Разработку будем вести в IDE Visual Studio Code от Microsoft, которая, благодаря множеству доступных плагинов, позволит работать со всеми необходимыми технологиями. Также необходимо установить следующие расширения:


После перезапуска VScode подключимся к нашему VPS. Нажимаем F1 и начнем вводить команду:

Remote-SSH: connect to  Host

далее новое подключение:

+ Add New Ssh Host

затем:

ssh root@<ip-адрес сервера>

Откроем окно терминала VScode (Menu/Terminal/New terminal) и проверим системные ресурсы командой:

top

Готово, доступ к консоли и файловой системе VPS получен:





Утилита top будет использоваться довольно часто, поэтому установим её псевдографическую версию htop:

Ctrl-C #Завершаем выполнение утилиты top

apt-get update #Обновляем установленные пакеты

apt-get install htop #Устанавливаем htop 

htop #Запускаем 

image

Теперь необходимо установить Docker и Docker compose:

Ctrl-C #Завершаем выполнение утилиты htop

Поскольку docker отсутствует в официальном репозитории Ubuntu, установим дополнительный репозиторий

apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common #Устанавливаем необходимые утилиты 

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - #Устанавливаем ключ репозитория docker 

add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" #Добавляем репозиторий 

apt-get install docker-ce docker-ce-cli containerd.io #Устанавливаем

curl -L "https://github.com/docker/compose/releases/download/1.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose #Скачиваем менеджер Docker compose 

chmod +x /usr/local/bin/docker-compose #Устанавливаем разрешение для загруженного файла исполняемый файл

ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose #Добавляем символьную ссылку в директорию исполняемых файлов 

 docker  --version #Проверяем

docker-compose --version

Отлично, сервер готов к тестовому развёртыванию сервиса.

Теперь установим Docker desktop на нашем локальном ПК для разработки. Установщик для Windows 10, версия для MacOS здесь. Будет установлен не только Docker, но и Docker toolbox, в который входят Docker compose и графические утилиты для работы с контейнерами.

Откроем новое окно VScode, Menu/File/Open folder Создадим новую папку нашего проекта, например, Srv и откроем её. В этой папке создадим файл docker-compose.yaml:

image

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

Здесь необходимо остановиться и уточнить разницу между образом и контейнером Docker. Контейнер это приложение + его зависимости (например, пакеты и библиотеки) + операционная система, которую можно с помощью Docker запустить как обычное приложение. А образ это подготовленный к запуску и упакованный в архив контейнер. Таким образом, весь наш бекэнд будет представлять собой набор контейнеров с описанием сценария их запуска.

Первым контейнером, который мы запланировали станет HTTP сервер NGINX. И давайте подготовим необходимый образ или нет? Дело в том, что для очень многих веб приложений и сред исполнения их разработчиками или комьюнити уже собраны необходимые образы и размещены в публичном реестре DockerHub. Разумеется такой широко используемый образ уже собран и ожидает нас по этой ссылке.

Обратите внимание на список это различные версии, они отличаются и версиями самого NGINX и дополнительными инструментами (например, установленным PERL). Для разработки можно использовать тэг latest (последняя стабильная версия на момент запроса образа), но для развёртывания на сервере, конечно, стоит использовать конкретную версию. В данный момент это образ nginx:1.19.0.

Здесь и далее необходимые пояснения к содержимому docker-compose.yaml я буду указывать в комментариях в самом листинге файла:



Сохраним изменения в файле, откроем консоль VScode и выполним команду запуска сценария

docker-compose up

Этой командой был не только запущен сценарий, но и вывод консоли контейнера был направлен в консоль хоста. Теперь, если в адресной строке браузера обратиться к локальному хосту на порте 8081 будет получена стандартный ответ NGINX:



Наш первый контейнер уже работает. Обычно запуск сценария выполняется командой:

docker-compose up -d

это позволяет запустить контейнеры в режиме сервиса (без вывода в консоль). Остановка контейнеров сценария выполняется командой:

docker-compose down

Для тестирования http запросов в VScode есть удобное расширение REST client.

Установим его и напишем первый отладочный тест нашего сервиса. Для этого создадим файл client.http в папке test/http_dev/:





Таким образом можно выполнять тестовые запросы, просматривая подробную информацию об ответах сервера.

Теперь давайте заглянем внутрь контейнера. Остановим выполнение сценария в консоли:

Ctrl-C

и запустим с флагом:

docker-compose up -d

Теперь запросим список выполняемых в данный момент контейнеров (процессов):

docker-compose ps



В списке выполняемых только один контейнер. Давайте откроем его:

docker exec -it srv_web_1 bash

Эта команда выполняет (exec) приложение bash (командная оболочка Linux) в контейнере srv_web_1 и не дает закрываться консоли (флаги -it):



Команда ls покажет стандартную структуру папок Linux:



Нас интересует файл /etc/nginx/conf.d/default.conf настройки NGINX, для просмотра можно использовать утилиту cat

cat /etc/nginx/conf.d/default.conf



В настройках NGINX один блок (так называемый location), в котором включено прослушивание порта 80 и раздача статических файлов из папки контейнера /usr/share/nginx/html. Можно попробовать внести изменения в файл настройки NGINX и перезапустить его, применив изменения, но при перезапуске сценария контейнер будет восстановлен из образа и никакие наши изменения не сохранятся. Это неправильный путь.

Выйдем из консоли контейнера:

Ctrl-D

Мы напишем свой файл настройки и разместим свои статические файлы, а при запуске будем их монтировать в контейнер NGINX. Создадим файл default.conf в папке /conf.d нашего проекта:





Создадим заглушку статического файла /public/index.html:



Теперь в сценарии запуска docker-compose.yaml смонтируем наши папки в файловую систему контейнера:



Обратите внимание, что содержимое папки проекта ./conf.d заменит содержимое контейнера в /etc/nginx/conf.d/, а в корневую папку папку контейнера будет смонтирована папка ./public.

Перезапустим сценарий:

docker-compose restart

Тестовый запрос:



Давайте посмотрим на файл default.conf. Обратите внимание, что мы отключили логирование доступа к статическим файлам access_log off. Это хорошее решение для прода, но очень неудобное при тестировании и разработке. Давайте создадим тестовые файл конфигурации NGINX /conf.dev.d/default.conf и сценарий docker-compose.dev.yaml.







Остановим сценарий:

docker-compose down

и запустим уже с флагами имен файлов:

docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up

При таком запуске сценария сначала будет прочитаны настройки из файла docker-compose.yaml, а затем будут добавлены или заменены совпадающие поля из docker-compose.dev.yaml (ports, volumes). Проверим логирование повторив запрос:





Итак, нам осталось выполнить копирование и запуск на сервере. Создадим на сервере папку /opt/srv_0/ (мы ведь ещё не закрыли окно VScode c SSH соединением к VPS) и скопируем в неё всё содержимое нашего проекта командой:

scp scp -r ./* root@91.230.60.120:/opt/srv_0/ 



Теперь на сервере в папке проекта /opt/srv_0/ выполним команду:

docker-compose up -d

Напишем ещё один http тест, теперь уже для VPS:



Ну или откройте в браузере ссылка.

Исходный код github

Вместо заключения


Итак, сделан первый шаг. Мы успешно раскатили на боевой сервер приложение. Во второй статье мы продолжим настройку сервера: назначим доменное имя и установим сертификат шифрования SSL. В третьей статье напишем flutter web приложение с обратным отсчётом времени до запуска нашего сервиса, соберём его и разместим на нашем сервере. В четвертой статье напишем и соберём нативный сервер под Linux на языке Дарт, который станет основой для приложений авторизации и данных нашего сервиса.

Комментарии и предложения приветствуются. Пообщаться с автором можно в Telegram-канале.
Подробнее..

Сервис на языке Dart доменное имя, SSL

28.07.2020 14:05:45 | Автор: admin
Оглавление
  1. 1. Введение
  2. 2. Backend
  3. 2.1. Инфраструктура.
  4. 2.2. Доменное имя. SSL (мы находимся здесь)
  5. 2.3. Серверное приложение на Дарт.
  6. ...
  7. 3. Web
  8. 3.1. Заглушка Under construction
  9. ...
  10. 4. Mobile
  11. ...


disclaimer (по комментариям к предыдущей статье)
  • Эта статья не является в полной мере самостоятельной и является продолжением серии Сервис на языке Дарт. Начало здесь.
  • Предмет данной статьи только то, что вынесено в заголовок: доменное имя и шифрование соединения.
  • Облаков, оркестрации, масштабирования, K8s, AWS, GKE здесь нет. Автору известно, что данный подход не является современным и модным. Более того, автор признаёт, что общается в окружении ретроградов, многие из которых вообще считают неприемлемым передачу критических данных и сервисов за пределы контролируемого периметра.
  • Автор не может отказаться от использования Дарт на сервере в пользу других языков и технологий, поскольку сама концепция данной серии статей заключается в реализации работоспособного сервиса на языке Дарт для всех уровней приложения: сервера, веб и мобильных клиентов.
  • Список подлежащих рассмотрению в ходе реализации приложений вопросов выбран автором по собственному усмотрению. Список может быть расширен читателем соответствующим комментарием к этой или последующим статьям. Предлагайте, попробуем сделать.
  • Список вопросов
    • Декомпозиция приложения на компоненты и слои
    • Dependency injection (кодогенерация boilerplate)
    • Генерация нативного серверного приложения
    • ORM. Генерация схемы и миграций для БД.
    • oAuth2 + JWT авторизация. Изолированный сервер авторизации.
    • Deeplinks (Universal links/ App links). Бесшовная интеграция web/app
    • Маршрутизация в приложениях
    • Взаимодействие реального времени (websockets)
    • Адаптивная верстка flutter



Доменное имя


В прошлый раз мы закончили на том, что в докер контейнере запустили веб-сервер NGINX, раздающий статический файл index.html. В этот раз мы расширим функциональность веб-сервера, добавив шифрование данных и принудительную переадресацию с http на https.



Для этого понадобится решить организационную задачу: дело в том, что сертификат шифрования можно получить только на доменное имя или группу имён. По этой причине отправляемся к любому из регистраторов доменных имён и выбираем название, соответствующее бренду (назначению, слогану и т.д.) не забывая о назначении доменных имён верхнего уровня. В моём случае отлично подойдёт dartservice.ru. В процессе регистрации необходимо заполнить форму сведений о владельце включая ФИО, почтовый адрес и электронную почту. Затем необходимо в панели управления регистратора перейти к управлению записями DNS и сделать три записи:

  • Не менее двух NS записей (NS records). Это имена серверов доменных имён регистратора и их наименование регистратор сообщает при покупке доменного имени.
  • A запись (A record). Это непосредственно запись связи между доменным именем и IP-адресом сервера.

В моём случае DNS записи выглядят так:



Сделав это не стоит ожидать немедленного результата. Обмен сведениями между DNS серверами занимает обычно от 1 до 12 часов для зоны RU. После чего добавим в проект ещё один тест /test/http/client.http



SSL


Вообще, конечно, протокол SSL устаревшее наименование. Новые версии протокола называются TLS 1.0...1.3, но механизм остался прежним шифрование данных при переходе между протоколом прикладного уровня (в нашем случае HTTP) и протоколом транспортного уровня (TCP/IP). Фактически необходимо:

  • Получить сертификат шифрования от специального удостоверяющего центра, подтвердив владение соответствующим доменом.
  • Передать сертификат серверу NGINX.
  • Настроить конфигурацию веб сервера для шифрования соединения.
  • Принудительно переключать соединения, устанавливаемые по http на https.

Общепринятым на данный момент является использование бесплатных сертификатов, автоматически выдаваемых сервисом Lets encrypt. Одним из ограничений таких сертификатов является срок действия. Всего 90 дней. После чего сертификат необходимо получить вновь. Для автоматического (без участия человека) получения сертификатов был разработан протокол ACME и приложения, периодически выполняющие действия по подтверждению владения доменом. Lets encrypt рекомендует использовать приложение certbot. Оно написанно на python и требует установки собственного репозитория и python3. Поэтому воспользуемся docker контейнером с установленным certbot из регистра DockerHub. Выберем последнюю стабильную версию certbot/certbot:v1.5.0.

Теперь разберёмся с механизмом получения сертификата по протоколу ACME:

  • Certbot при первом запуске генерирует закрытый и открытый ключ, затем создаёт аккаунт администратора домена в сервисе Lets encrypt, передавая открытый ключ и сведения о домене.
  • После этого Lets encrypt передаёт сообщение, которое certbot должен подписать закрытым ключом и вернуть обратно.
  • Сertbot должен разместить на сервере специальный файл, доступный для чтения в dartservice.ru/.well-known/acme-challenge для подтверждения владения этим доменом.
  • Certbot составляет запрос сертификата, отправляет его в Lets encrypt и получает взамен сертификат для домена.

Добавим контейнер приложения в наш сценарий docker-compose.yaml:



Новый параметр здесь comand: тут находится команда, которая будет выполнена после запуска контейнера. В данном случае certonly (получить сертификат). Получение сертификата происходит в интерактивном режиме, то есть необходимо последовательно ответить на несколько вопросов. Передача флагов после команды позволяет сделать это без участия человека: --webroot (способ подтверждения) --webroot-path=/usr/share/nginx/html/letsencrypt (путь, по которому будут размещены файлы подтверждения владения доменом) --email admin@email.com (почта администратора домена) --agree-tos (принимаем условия лицензионного соглашения) --no-eff-email (не сообщать электронную почту разработчикам certbot) -d dartservice.ru (список доменов).

Настроим контейнер NGINX:



Изменения здесь заключаются в открытии порта https (443) и монтировании папок с SSL сертификатом и файлами подтверждения владения доменом.

Важным параметром является папка с ключом Диффи-Хеллмана. Если быть кратким: этот ключ нужен для того, чтобы безопасно обмениваться ключами шифрования между сервером и клиентом при установлении соединения. Более подробно здесь.

Давайте сгенерируем такой ключ, однако мы столкнёмся со следующей проблемой: создание ключа выполняется программой openssl, это Linux консольная утилита, которая вряд ли обнаружится на нашей Windows машине. Самое простое решение запустить наш сценарий, зайти в консоль контейнера web и там создать ключ, передав в выходном пути для файла папку хоста смонтированную в контейнер:

Запускаем сценарий:

docker-compose up -d

Запрашиваем список работающих контейнеров:

docker-compose ps



Открываем консоль контейнера:

docker exec -it srv_web_1 bash

Запускаем генерацию ключа в папку конфигурации NGINX (которая, как мы помним, смонтирована из хоста):

openssl dhparam -out /etc/nginx/conf.d/dhparams.pem 2048



Переместим ключ в ./dhparams/dhparam-2048.pem

Выходим из консоли контейнера Ctrl-D, останавливаем сценарий:

docker-compose down

Теперь изменим конфигурацию NGINX ./conf.d/defaulf.conf:



Добавим новый location ^~ /.well-known/acme-challenge для раздачи статических файлов из папки /usr/share/nginx/html/letsencrypt. Здесь будут размещаться файлы подтверждения certbot. Настроим переадресацию для всех остальных запросов на https.

Всё готово для первого получения SSL сертификата.

Скопируем наш проект на VPS в новую папку /opt/srv_1/ командой:

scp -r ./* root@91.230.60.120:/opt/srv_1/

Подключимся из VScode по SSH к VPS.

Перейдём в папку работающего сервера:

cd /opt/srv_0/

и остановим сценарий:

docker-compose down

Теперь переходим в папку нового сервера cd /opt/srv_1/ и запускаем сценарий:

docker-compose up



В консоли мы видим, что certbot создал файл подтверждения zeS4O87S6AfRQ3Kj4MaBlBFZx3AIiWdPn61DwogDMK4 и сообщил об этом сервису Lets encrypt, который, в свою очередь, из четырёх разных IP адресов запросил данный файл после чего выдал сертификат. Сертификат в виде полной цепочки и приватного ключа были сохранены в соответствующей папке. Срок действия сертификата 90 дней (до 05.10.2020).

Самое время создать второй location для защищенного соединения в конфигурации NGINX на сервере ./conf.d/defaulf.conf:



Перезапустим сценарий
docker-compose restart

Посмотрим, как отреагирует браузер на наш сертификат:



Google Chrome нашим сертификатом доволен. Теперь задачка посложнее протестируем безопасность и доступность для разных браузеров наше SSL соединение https://www.ssllabs.com/ssltest/. Вводим адрес, и получаем результат:



С сертификатом и обменом ключами (спасибо Диффи-Хеллману) всё отлично, однако тестовый робот снизил оценку (В это 4 по-нашему) за поддержку устаревших протоколов TLS1.0 и TLS1.1. Отключить их в конфигурации NGINX несложно, однако, просматривая тестовый отчет дальше, мы видим что, например, браузеры некоторых мобильных устройств в этом случае не смогут подключиться:





Осталось выполнить несколько служебных задач.

Число попыток получения сертификата для домена не должно превышать 5 в течение 7 дней. После этого сервис Lets encrypt может нас заблокировать. Однако запуская сценарий при разработке каждый раз certbot будет делать такую попытку, поэтому в сценарии docker-compose.dev.yaml изменим параметр command контейнера certbot:



Флаг --dry-run это тестовый прогон без получения сертификата.

Напишем тест:



Исходный код github.

Заключение


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

В следующей статье напишем flutter web страницу с обратным отсчетом времени до запуска нашего сервиса, соберём его и разместим на нашем сервере.
Подробнее..

Взгляд с позиции работодателя на fullstack и специализацию

04.10.2020 14:17:49 | Автор: admin

Тема действительно как холиварная, так и актуальная. Что лучше: узкая или узкая специализация? За нее можно получить много минусов в карму и уйти в бан надолго. Но я попробую донести текущую ситуацию, с которой столкнулся, без эмоций и приводить чисто факты. Выводы делайте сами.



Сначала в двух словах о проекте, с позиций которого я сегодня рассматриваю вопрос из заголовка. Не буду называть конкретных названий, чтоб не раздражать администрацию Хабра из-за несанкционированной рекламы, кому интересно, с легкостью найдет всю информацию. Итак, это молодая компания, которая получила контракт на разработку некоей промышленной системы. Промышленная система в данном случае предполагает сбор данных от различного промышленного оборудования, их обработку и выдачу рекомендаций различными способами (от верещания сирены до API). Тема на сегодняшний день очень модная это Интернет вещей (IoT), большие данные, машинное обучение и все такое. На момент моего прихода в проект, команда уже была. За счет естественной текучки, она конечно меняется и я постепенно привожу ее к своим требованиям, но во многом приходится жить с тем, что досталось в наследство.
Причем про наследство я в данном случае говорю не в плане "плохой/хороший чувак", а в плане их привычек, стиля, стека, ожиданий и т.д. И вот с какими проблемами я сразу же столкнулся.


Насущные проблемы


Специализация по коду


За каждым разработчиком были закреплены определенные задачи, в другие он не лез. Вот такой диалог был обычным делом в самом начале:


  • Надо сделать такие-то изменения в модуле X.
  • Это код Пети (Васи, Маши, в общем, другого разработчика).
  • Но Петя в отпуске.
  • Ну дождемся.
  • Мы не можем ждать, у нас система в нерабочем состоянии, у нас спринт заканчивается, у нас сроки горят.

Где-то месяц ушел на то, чтоб объяснить коллегам, что код, который они пишут в рамках своих служебных обязанностей, им уже не принадлежит, а принадлежит компании. И у нас не должно быть "кода Пети", а должен быть только код компании. И любой разработчик должен иметь возможность вносить изменения в любой модуль.


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


Специализация на круге задач


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


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

(Подобрав с полу челюсть) У нас здесь институт благородных девиц или коммерческая организация, у которой вполне бизнесовые конкретные задачи? Где я вам сейчас фронтендера найду под одну страничку?


Производство "велосипедов"


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


  1. Абсолютно все, что они делали, уже давно реализовано и лежит бесплатно под открытой лицензией в интернете. Просто потому что разработчики специализируются на Java-программировании, просто никому не пришло в голову поискать готовое. Что сказали делать, то и делали.
  2. Все, что они делали, содержало серьезные ошибки. Нет, оно работало на разовых запросах, но нагрузочное тестирование приводило к отказу баз данных. Просто, как у Cassandra, так и у ClickHouse есть особенности. Работа с ними в Postgress-стиле для них губительна. Ну это просто следствие. Если бы взяли готовое, то и ошибок бы было меньше.

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


Дефицит компетенций


Мы находимся в боевых условиях. Это не игрушки, это работающий бизнес. Задачи падают такие, что не всегда есть специалисты по ним. Но решать их надо. В общем, определенный класс задач мне просто некому было поручать. В итоге пришлось их решать самому. И это мне очень сильно не нравится, потому что без меня работа по этим направлениям встанет.


Анализируем это


Что хочется сказать в целом по сложившейся ситуации на момент моего прихода в команду.


  1. Специализация внутри команды была доведена до абсурда. Причем мне коллеги как раз и доказывали про то, что есть разделение труда и все такое.
  2. Принятые решения на базе идеи о разделении труда, фактически заблокировали развитие проекта. Фактически была патовая ситуация, когда вся разработка встала.

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


Точнее даже не так. Нам нужно воспитать в разработчиках навык, который на Западе именуется как problem solving. Т.е. из людей, которые просто делают поставленные им сверху задачи в Жире с 9 до 18, нам нужно получить людей, которые нацелены на конечный результат, который получит заказчик и за который компания получит оплату.


Именно problem-solving feature в разработчике и является для компании ценностью. Не способность 5 способами написать цикл в Java 7, а способность быстро и эффективно решать бизнесовые постановки.


Решения и планы


Итого, цель известна, немного опишу те решения, которые были приняты и дальнейшие планы.


Запрет на владение кодом, перераспределение задач


Пришлось долго объяснять людям, что личного кода у нас нет. Вплоть до того, что тоталитарными методами пришлось перераспределять задачи, требуя от людей знакомиться с чужим кодом.


Обучение


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


Парное программирование


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


Инструктаж


Время от времени просто рассказываю и показываю новые и классические малоизвестные инструменты. Например, пришлось объяснять про DDD, а потом на ревью отправлять на доработку код, чтоб добиться разделения моделей по назначению.


Стресс-тестирование


Нет, я не ору и не ругаюсь. Здесь стресс в другом состоит. Дается задача на совершенно незнакомую тему и смотрю как человек реагирует. Зачем я это делаю? Прежде всего это демонстрация что их ждет в дальнейшем. Еще раз напомню: тематика проекта IoT, BigData, ML. По всем не существует готовых специалистов с 20-летним опытом. Всем членам команды придется каждый день обучаться новым вещам. Тут у нас выбора нет: либо коллеги станут problem-solving разработчиками, либо они поймут, что не тянут и выберут себе другое направление карьеры. Тут просто вопрос вопрос выживания компании.


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


Новые люди


Сейчас мы набираем людей в команду. Как вместо ушедших, так и дополнительно. И, признаться, я не исключаю, что кто-то из команды не выдержит и уйдет. Сейчас приходится просматривать много резюме кандидатов и я хочу сразу обозначить критерии, по которым я выбираю с кем готов общаться. Если я вижу у крутого разраба в стеке только классику (Java, Spring, SQL), то резюме сразу идет в отказ. Каким бы он молодцом ни был, если он за годы работы программистом так и не опробовал что-то из модернового стека, то очевидно, что новые темы не для него.


Итого, я смотрю на наличие знаний чего-то из NoSQL, микросервисы, альтернативные языки (Scala, Kotlin) и т.д Если будет присутствовать ультрамодерн в виде NewSQL, Serverless и пр., то это даже лучше. Возможно мы ни с чем из этого стека никогда не будем работать, но знание подобных инструментов демонстрирует способность человека развиваться и осваивать новые инструменты. Также интересен опыт участия в стартапах. Именно в стартапах развиваются problem-solving навыки.


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


Вводится Kotlin вместо Java


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


Еще один гвоздь ...


Да, разделение труда сыграло действительно большую роль в индустриальной революции Человеческой цивилизации. И ведь действительно, если человек всю жизнь профессионально колол дрова, то он колет дрова лучше и быстрее по сравнению с портным или бухгалтером.


Но сейчас у нас новая реальность. Как только возникает однотипная работа, под которую можно заточить профессионализм, сразу возникает возможность АВТОМАТИЗАЦИИ. И, каким бы ни был профессионалом наш дровокол, вместо него проще поставить автомат по колке дров стоимостью от 20 тыс рублей до 2 миллионов. И ни с каким автоматом профессионализм человека конкурировать не способен.


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


Заключение


Я не стану делать максималистских заявлений и не стану утверждать, что узкая специализация умерла. Сейчас она пока еще востребована много где, да и в будущем, возможно, останутся для нее какие-то ниши.


Но в конкретном проекте сейчас я вижу потребность только в специалистах широкого профиля. Ну и при развитии коронокризиса догадайтесь кого из работников будут держать до последнего: того кто 20 лет на Java 5 разрабатывает или того, кто в любой момент может решить любую задачу в компании.


PS: Новые ребята, которые уже приходят в команду, меня очень радуют. Задачу, от которой отказался опытный Java-разработчик (потребовал 3 недели), новичок с опытом работы в 1 год решил за 2 дня.

Подробнее..

Что вообще значит full stack?

29.10.2020 20:09:21 | Автор: admin


Не счесть холиваров о том, стоит ли быть фуллстек-разработчиком. И в таких дискуссиях часто вылезает ещё один спорный вопрос: а что это понятие означает-то? Фронтбэкендер? Многорукий Шива, мастер всего от инфраструктуры до тестирования? Человек, освоивший столько технологий, что воспарил над ними в мир общих концепций?


Я захотел разобраться, как это понятие появилось и что люди в него вкладывали изначально. Было ли какое-то каноническое определение? Пока разбирался, увидел прямо-таки эволюцию представлений о нём и решил изложить её для Хабра.


Когда понятие возникло? Как можно увидеть по графику Google Trends, в широкий обиход оно вошло с 2014-го. А на Хабре первое упоминание произошло в 2013-м. Это был перевод англоязычного блог-поста, где упоминается, что Facebook нанимает только Full Stack. То есть в Фейсбуке это уже тогда было устоявшимся понятием? Я стал гуглить дальше и в техническом блоге Facebook нашёл пост 2010 года The Full Stack, Part I с тысячей лайков. А он, в свою очередь, ссылается на пост разработчика Рэнди Шмидта 2008 года. И, судя по прочей найденной мной информации, вот у Шмидта и было первое использование понятия, из которого выросло всё остальное. Теперь, когда мы дошли до начала начал, давайте пойдём по этим же постам в обратном направлении (по хронологии) и посмотрим, что в них говорилось.




2008: Full Stack Web Developers (Randy Schmidt)


Эта страница личного блога уже даже не открывается, но Internet Archive заботливо сохранил для нас Первый Пост. Автор поста восхищается людьми, которых он называет Full Stack Web Developers. И вот какое определение им он даёт:


A full stack web developer is someone that does design, markup, styling, behavior, and programming.

Вот это сейчас внезапно было: первым пунктом идёт дизайн. Д И З А Й Н. (Судя по контексту, это не про design как проектирование, а именно про графический дизайн.) А programming упомянуто мимоходом как единый последний пункт хотя сегодня обсуждения строятся как раз на том, что у него есть подпункты.


Ну, с programming понятно: в 2008-м ещё не произошёл JS-взрыв, поэтому в тексте браузерная часть проходит как markup, styling (читай: HTML, CSS). Но даже если мысленно заменить markup, styling и programming на фронт и бэк, всё равно не получится нынешних дискуссий: это разделение Шмидта как раз не сильно волновало. По-настоящему его волновало, что он не разбирается в дизайне, и ему надо вот с этим справиться, чтобы стать настоящим full stack web developer. Так что получается, что мы сейчас под фуллстеком понимаем вообще не то, что закладывал автор.


По сути, содержание его поста сегодня в какой-то степени актуально внутри фронтенда, где есть место и JS-программированию, и более дизайнерским вещам. Так что, оставаясь в пределах фронтенда, можно специализироваться на чём-то, а можно быть многостаночником в прошлом году нашумел текст The Great Divide, посвящённый как раз разнице специализаций.




2010: The Full Stack, Part I (Carlos Bueno)


Следующие два года слова full stack не получали большого распространения, но затем Карлос Буэно из Фейсбука написал текст с таким заголовком, ссылающийся на Рэнди Шмидта. Поскольку у инженерного блога Facebook аудитория заметно больше, чем у небольшого личного блога, похоже, что вот отсюда понятие начало расходиться шире. Но Карлос не просто пересказал малоизвестную чужую идею, а дал своё определение:


A "full-stack programmer" is a generalist, someone who can create a non-trivial application by themselves.

Вот такое звучит применимо и сегодня: человек, который может сам создать приложение в одиночку. Про дизайн тут не слова ни сказано, и непонятно, что думает Карлос по его поводу. И ещё, кстати, тут нет слова web, которое было в оригинале получается, что можно и где-нибудь в геймдеве быть фуллстеком.


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


Он сравнивает это с химией и физикой, которые действуют на разных уровнях реальности: понимание нижнего уровня помогает человеку лучше ухватывать происходящее на верхнем. То есть, если исходный пост был только про широту охвата (и дизайнер, и кодер, и на дуде игрец), то вот тут впервые зашла речь о том, что важна глубина.




2012: What is a Full Stack developer? (Laurence Gellert)


Ещё один резонансный ранний пост тот самый, благодаря которому словосочетание full stack впервые появилось на Хабре (его тут переводили аж три раза, причём в третий раз вчера, спустя восемь лет после публикации оригинала).


Здесь определение звучит так:


For me, a Full Stack Developer is someone with familiarity in each layer, if not mastery in many and a genuine interest in all software technology.

Знаком со всеми слоями, даже если и не мастер в них вот это интересный нюанс. Если изначальное определение предполагало умеешь всё делать хорошо, то здесь этого уже требуют. Можно быть T-shaped: чем-то конкретным заниматься глубоко, а с другими вещами достаточно быть знакомым.


И здесь приводится новая причина чем это хорошо, уже не про оптимизацию производительности: Хорошие разработчики, знакомые со всем стеком, знают, как улучшить жизнь окружающим их людям. Про это сейчас можно услышать в связи с тестированием или девопсом: давайте не просто перекидываться кодом через стену, а вместе понимать, как всё работает.


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


Разумеется, этот список породил споры, где к нему пытались что-то добавить или убрать. Но интересно обсуждать не конкретные пункты, а общую идею: тут получается, что для фуллстека важнее не уметь накодить и на фронте, и на бэке, а понимать и ниже своего кода (как работает то, что накодил), и выше (зачем вообще нужно то, что накодил). По сути, тут обращаются к исходному значению слова stack: вертикальная стопка вещей, опирающихся друг на друга (недаром тексты про фуллстек часто иллюстрируют стопкой блинов на тарелке).




2014: Full-stack developers (Mike Loukides)


Наконец, наткнулся в процессе гугления на популярный текст 2014 года в блоге на сайте издательства O'Reilly. Здесь уже не просто отдельное мнение, а попытка осмыслить различные предыдущие выступления по теме и добавить к ним что-то своё.


Одно из добавлений: вот здесь говорится, что JavaScript к 2014-му страшно разросся и усложнился, так что от фуллстек-разработчиков теперь требуется и понимание всяких ангуляров.


А ещё Майк предлагает к списку знаний фуллстека предлагает добавить CVS (ну, сегодня бы уже даже не стал упоминать, наверное), облака, распределённые вычисления Как он сам признаёт, результат в таком случае получается не вертикальным стеком, где всё опирается друг на друга, а разветвлённым деревом, где много вещей в сторону.


И ещё он признаёт, что если добавить это всё к списку от Лоренса, получается совсем уж длиннющий перечень. Поэтому не требует магического абсолютного мастерства во всём сразу. А вместо этого впрямую произносит слово T-shaped, призывая специализироваться на чём-то, но в то же время и заглядывать в другие сферы: я не требую от разработчиков разбираться в сетевых вопросах на уровне сетевиков, но уметь обсуждать эти вопросы надо.


Из забавного: в тексте есть фраза I sincerely hope that full stack doesnt appear in job titles anywhere. Майк, пишем тебе из будущего, не хочется расстраивать, но тут такое дело





И что в итоге?


Какие выводы мы можем извлечь из этих четырёх текстов? Своими выводами делитесь в комментариях, а у меня получились такие:


  • Сегодняшние обсуждения full stack страшно отдалились от того, что вкладывал в это словосочетание его автор. Наверное, он офигевает, глядя на то, во что всё превратилось :)
  • Это понятие с самого начала было довольно размытым: с первых же постов разные люди вкладывали в него разное. Четыре приведённых текста это четыре ощутимо разных позиции.

Так что нет и никогда не было какого-то одного правильного понимания. Это значит, что если у вас есть определённое видение, вы имеете на него полное право. Но это также значит, что у вашего собеседника может быть ощутимо другое видение, на которое он имеет такое же право. Так что перед тем, как спорить нужно ли становиться фуллстеком, стоит проверить, не говорите ли вы о разных вещах.


А ещё я увидел в этих определениях идею, которую считаю очень интересной. Но надо сделать оговорку: у меня профдеформация, и я тут лицо заинтересованное.


Смотрите: мы привыкли воспринимать фуллстек как фронт+бэк, но сразу два из четырёх описаний совершенно не требуют быть сениором в обеих сферах, а вместо этого идут в сторону T-shaped. Они предлагают не отказываться от специализации и быть гением-многостаночником, а изучать разное вокруг своего основного.


И это откликается во мне по следующей причине: я работаю в JUG Ru Group, мы делаем конференции для разных IT-специалистов, и среди их зрителей встречаю как раз таких людей. Типичный случай Java-разработчик посещает конференции не только по Java, но и по тестированию или DevOps. А ещё может посмотреть видеозапись доклада про GraphQL с JS-конференции или про архитектуру с .NET-мероприятия.


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


Стоит ли называть это full stack? Спорный вопрос. Но для меня как раз это звучит как фуллстек здорового человека. Потому что если про совмещение разных ролей часто пишут не ходите сюда, здесь боль и страдание, то вот про вылазки на смежные территории такого негатива никогда не слышал только хорошее.


И если вы хотите быть фуллстеком вот в таком значении, то для вас напоследок сделаю минутку рекламы: мы придумали конференционный вариант как раз для таких людей. Для нашего сезона из 8 онлайн-конференций сделали Full Pass билет-абонемент, дающий доступ ко всем сразу. Смысл в том, чтобы конференцию по своему профилю смотреть внимательно, а на других точечно подключаться к отдельным докладам, актуальным для вас. Если звучит интересно переходите на сайт Full Pass, там все подробности.
Подробнее..

Категории

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

  • Имя: Макс
    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