Панель управления услугами компании «Миран».
Часть 1: Введение

Авдеев Максим, 10 октября, 2017

Вступление

Меня зовут Максим и я разработчик фронтенда панели управления услугами компании «Миран». В данном цикле статей я расскажу вам, как эволюционирует система и что нового стоит ждать в ближайшем будущем. Добро пожаловать под кат.

Эпоха динозавров

Эпоха динозавров

Старая панель, или panel_v1, базируется на Django со стороны бэкэнда + TastyPie в роли API + AngularJS во фронтенде. Выбор основывался на хорошей документации и возможности обойтись небольшим напильником для доведения фреймворка до необходимого нам состояния. Связка Django и Tastypie предоставляли все необходимые инструменты для наших запросов, а AngularJS был достаточно удобен для отображения данных.

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

Муки выбора

Муки выбора

Главным вопросом стал выбор архитектуры. Учитывая существующий опыт из panel_v1 и возможности tastypie мы решили развить идею и разбить всю панель на части, связать через api и управлять всеми частями индивидуально. Поэтому выбором стали микросервисы. Ведь крайне удобно иметь небольшие модули, которые выполняют строго свою задачу и при для изменения их работы? изменении в их работе достаточно обновить интерфейсы для остальных микросервисов.

Решение принято, супер. Для бэкэнда будет достаточно легковесного, но мощного Flask, фронтенд переедет на новый Angular2. Вроде всё выглядит хорошо, но как же им общаться? Тут необходим API, который будет достаточно гибким и подходить для бэкэнда и фронтенда с минимальными изменениями. Тем более его необходимо будет открыть клиентам, а значит без подробной документации с примерами использования никуда/не обойтись. Вот тут-то мы и призадумались.

Озарение

Озарение

Решением нашей проблемы стала спецификация OpenAPI в реализации Swagger. Вот краткий список возможностей Swagger:

  • Единый файл с описанием API для бэкэнда и фронтенда;
  • Работа с форматами yaml и json;
  • Пакет connexion для питона, который запускается с файлом спецификации API и выстраивает маршруты, основываясь на нем;
  • С помощью swagger ui вся документация хранится в описании методов самого API, поэтому необходимость в каких-то дополнительных инструментах отпадает. Есть вопрос: Что делает метод и какие данные требует на вход? — Добро пожаловать в ui! Там же можно выполнить запрос и взглянуть на результат. Как говорится, лучше один раз увидеть, чем 100 раз услышать;
  • Строгий контроль входящих данных. Для примера возьмем стандартную ситуацию – когда пользователь заполняет какую-то форму, то происходит целый ряд проверок: — сначала простая валидация на стороне фронтенда; — далее connexion в соответствии с файлом спецификации проверяет пришедшие данные из фронтенда; — и только потом бэкэнд может дополнительно выполнить какую-либо специфичную проверку. Именно такой многоэтапный контроль за вводимыми данным страхует пользователя от ошибки;
  • Конкретно для фронтенда: утилита swagger-codegen, которая берет на вход файл спецификации и отдает все необходимые сервисы для Angular2. Причем как в виде Javascrip;
  • Нескучные обои для UI: Нескучные обои для UI

Учитывая все эти моменты мы получили единый источник для всех изменений. Необходимо добавить функциональность? Отлично! Сначала описание вносится в файл спецификации, затем логика описывается в бэкэнде, после чего api пересобирается с новыми методами для фронтенда и остается использовать эти методы. Есть потребность изменить возвращаемые данные из метода? Запросто! Зафиксировали изменения в файл спецификации, в соответствии с ним изменили бэкэнд, пересобрали api для фронтенда и минимально исправили затронутые методы. В итоге вся маршрутизация, все запросы и ответы на них описаны в одном файле. А бэкэнд и фронтенд работают согласованно.

Все не так однозначно

Все не так однозначно

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

Файл спецификации

По умолчанию файл спецификации можно разбивать и хранить, например, definitions (объект, содержащий описание данных, циркулирующих в операциях) отдельно и обращаться к нему через ссылку. Но такой вариант не заработал из-за connexion. Текущая версия пакета не предусматривает этой возможности (искренне надеюсь, что в следующий версиях её добавят). А пока пришлось выкручиваться следующим образом: все объекты сущностей (definitions, paths, responses, etc.) выделены в файлы и собираются с помощью grunt. В итоге получился набор задач, которые можно легко добавлять и менять. На мой взгляд такой вариант лучше, чем поиск нужной строки среди ~2500 строк итогового файла.

Вот выдержка из задачи для клиентской части:

  1. Очистка перед сборкой
    								
    clean:
       client: [
        '<%= client_interim_dir %>'
                '<%= client_root %>/definitions.yml'
                '<%= client_root %>/paths.yml'
       ]
    								
    							
  2. Добавление отступов
    								
    indent:
    client_common_definitions:
    src: ['<%= client_common %>/definitions/*.yml']
    dest: '<%= client_interim_dir %>/common/definitions/'
    options:
      style: 'space'
      size: 2
      change: 1
    
    client_dedic_definitions:
    src: ['<%= client_dedic_project %>/definitions/*.yml']
    dest: '<%= client_interim_dir %>/dedic/definitions/'
    options:
      style: 'space'
      size: 2
      change: 1
    
    client_common_paths:
    src: ['<%= client_common %>/paths/*.yml']
    dest: '<%= client_interim_dir %>/common/paths/'
    options:
      style: 'space'
      size: 2
      change: 1
    
    client_dedic_paths:
    src: ['<%= client_dedic_project %>/paths/*.yml']
    dest: '<%= client_interim_dir %>/dedic/paths/'
    options:
      style: 'space'
      size: 2
      change: 1
    
    client_misc:
    src: [
      '<%= client_root %>/projects.yml'
    ]
    dest: '<%= client_interim_dir %>/'
    options:
      style: 'space'
      size: 2
      change: 1
    								
    							
  3. “Склеивание” промежуточных и итоговых файлов
    								
    concat:
    client_common_definitions:
    src: ['<%= client_interim_dir %>/common/definitions/*.yml']
    dest: '<%= client_interim_dir %>/common_definitions.yml'
    
    client_dedic_definitions:
    src: ['<%= client_interim_dir %>/dedic/definitions/*.yml']
    dest: '<%= client_interim_dir %>/dedic_definitions.yml'
    
    client_common_paths:
    src: ['<%= client_interim_dir %>/common/paths/*.yml']
    dest: '<%= client_interim_dir %>/common_paths.yml'
    
    client_dedic_paths:
    src: ['<%= client_interim_dir %>/dedic/paths/*.yml']
    dest: '<%= client_interim_dir %>/dedic_paths.yml'
    
    client_misc_paths:
    src: [
      '<%= client_interim_dir %>/common_paths.yml',
      '<%= client_interim_dir %>/projects.yml'
    ]
    dest: '<%= client_interim_dir %>/common_paths.yml'
    
    client_target_paths:
        src: [
          '<%= client_root %>/paths_header.yml'
          '<%= client_interim_dir %>/common_paths.yml'
          '<%= client_interim_dir %>/dedic_paths.yml'
        ]
        dest: '<%= client_root %>/paths.yml'
    client_target_definitions:
        src: [
          '<%= client_root %>/definitions_header.yml'
          '<%= client_interim_dir %>/common_definitions.yml'
          '<%= client_interim_dir %>/dedic_definitions.yml'
        ]
        dest: '<%= client_root %>/definitions.yml'
    client_swagger:
        src: [
          '<%= client_root %>/info.yml'
          '<%= client_root %>/tags.yml'
          '<%= client_root %>/security.yml'
          '<%= client_root %>/definitions.yml'
          '<%= client_root %>/parameters.yml'
          '<%= client_root %>/responses.yml'
          '<%= client_root %>/paths.yml'
        ]
        dest: '<%= client_output_path %>/client_swagger.yaml'
    								
    							

Сборка api-прослойки для фронтенда

Данная проблема обнаружилась в тот момент, когда я исследовал как swagger-codegen может выполнять сборку. Есть два варианта: предоставить файл спецификации или скачать его. И тут мы решили собирать из файла, так как сервис API может отказать и это повлияет на работу фронтенда. Поэтому выбрали готовый файл. Репозиторий был подключен как git submodule, что обеспечило постоянную актуальность. Алгоритм получился следующий:

  1. Обновить зависимый репозиторий:
    								
    git submodule update
    								
    							
  2. Запустить задачу сборки Grunt:
    								
    grunt compile
    								
    							
  3. Запустить swagger-codegen с полученным файлом

Но всплывает подробность, что на вход swagger-codegen принимает JSON. А у нас YAML. И тут мы поняли, что угадали с Grunt. Подключением плагина YAML to JSON брюки превраща… Тьфу. YAML превращается в JSON. А мы получаем необходимый файл спецификации.

Проблема имен

Тут сказались настройки swagger-codegen при именовании методов и переменных в typescript. Например, имя свойства “total_count”. Без настроек компиляции typescript имя переменной становилось totalCount. При попытке решения данной проблемы обнаружилось, что можно вместе с файлом конфигурации передать файл дополнительных настроек в формате JSON. И эта строка:

								
{
    "modelPropertyNaming": "original"
} 
								
							
оставляет имена без перемен.

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