- PVSM.RU - https://www.pvsm.ru -
Доброго времени суток, уважаемые читатели!
Сегодня DevOps находится на волне успеха. Практически на любой конференции, посвященной автоматизации, можно услышать от спикера мол “мы внедрили DevOps и тут и там, применили это и то, вести проекты стало значительно проще и т. д. и т. п.”. И это похвально. Но, как правило, внедрение DevOps во многих компаниях заканчивается на этапе автоматизации IT Operations, и очень мало кто говорит о внедрении DevOps непосредственно в сам процесс разработки.
Мне бы хотелось исправить это маленькое недоразумение. DevOps в разработку может прийти через формализацию кодовой базы, например, при написании GUI для REST API.
В этой статье хотелось бы поделиться с вами решением нестандартного кейса, с которым столкнулась наша компания – нам довелось автоматизировать формирование интерфейса веб-приложения. Я вам расскажу о том, как мы пришли к данной задачей и что использовали для ее решения. Мы не считаем, что наш подход является единственно верным, но нам он очень даже нравится.
Надеюсь данный материал будет вам интересен и полезен.
Ну что ж, начнем!
Эта история началась примерно год назад: был прекрасный летний день и наш отдел разработки занимался созданием очередного веб-приложения. На повестке дня стояла задача по внедрению в приложение новой фичи – необходимо было добавить возможность создавать пользовательские хуки.
На тот момент архитектура нашего веб-приложения была построена таким образом, что для реализации новой фичи нам необходимо было сделать следующее:
Выходит, что нам одновременно сразу в двух местах, необходимо было сделать очень похожие изменения в коде, так или иначе, “дублирующие” друг друга. А это, как известно, не есть хорошо, поскольку при дальнейших изменениях, разработчикам нужно было бы вносить правки так же в двух местах одновременно.
Допустим, нам нужно будет поменять тип поля “name” cо “string” на “textarea”. Для этого нам нужно будет внести данную правку в код модели на сервере, а затем сделать аналогичные изменения в коде представления на клиенте.
Не слишком ли всё сложно?
Ранее мы мирились с данным фактом, поскольку многие приложения были не очень большими и подход с “дублированием” кода на сервере и на клиенте имел место быть. Но в тот самый летний день, перед началом внедрения новой фичи, внутри нас что-то щелкнуло, и мы поняли, что дальше так работать нельзя. Текущий подход являлся весьма неразумным и требовал больших временных и трудовых затрат. К тому же, “дублирование” кода на back-end’е и на front-end’e могло в будущем привести к неожиданным багам: разработчики могли бы внести изменения на сервере и забыть внести аналогичные изменения на клиенте, и тогда все пошло бы не по плану.
Мы стали задумываться, как нам можно оптимизировать процесс внедрения новых фич.
Мы задали сами себе вопрос: «Можем ли мы прямо сейчас избежать дублирования изменений в представлении модели на front-end’e, после любого изменения в ее структуре на back-end’e?»
Мы подумали и ответили: «Нет, не можем».
Тогда мы задали себе еще один вопрос: «Окей, в чем тогда заключается причина подобного дублирования кода?»
И тут нас осенило: проблема, по сути, в том, что наш front-end не получает данных о текущей структуре API. Front-end ничего не знает о моделях, существующих в API, до тех пор, пока мы сами ему об этом не сообщим.
И тогда у нас появилась идея: что если построить архитектуру приложения таким образом, чтобы:
Внедрение новой фичи будет занимать гораздо меньше времени, поскольку будет требовать внесения изменений только на стороне back-end’a, а front-end автоматически все подхватит и представит пользователю должным образом.
И тогда, мы решили подумать еще несколько шире: является ли новая архитектура пригодной только для нашего текущего приложения, или мы можем использовать ее где-то еще?
Ведь, так или иначе, почти все приложения имеют часть схожего функционала:
И поскольку наша компания часто выполняет разработку веб-приложений на заказ, мы подумали: зачем нам каждый раз изобретать велосипед и каждый раз разрабатывать схожий функционал с нуля, если можно один раз написать фреймворк, в котором были бы уже описаны все базовые, общие для многих приложений, вещи, и затем, создавая новый проект, использовать готовые наработки в качестве зависимостей, и при необходимости, декларативно их изменять в новом проекте.
Таким образом, в ходе долгих рассуждений у нас появилась идея о создании VSTUtils – фреймворка, который бы:
Ну что ж, надо делать, подумали мы. Некий back-end у нас уже был, некий front-end тоже, но ни на сервере, ни на клиенте не было инструмента, который мог бы сообщить или получить данные о структуре API.
В ходе поисков решения данной задачи наш глаз пал на спецификацию OpenAPI [1], которая на основе описания моделей и взаимосвязей между ними генерирует огромный JSON, содержащий всю эту информацию.
И мы подумали, что, по идее, при инициализации приложения на клиенте front-end может получать от API данный JSON и на его основе строить все необходимые представления. Остается только научить наш front-end все это делать.
И спустя некоторое время мы его таки научили.
Архитектура фрейворка VSTUtils первых версий состояла из 3 условных частей и выглядела примерно так:
{
// объект, хранящий в себе пары (ключ, значение),
// где ключ - имя модели,
// значение - объект с описанием полей модели.
definitions: {
// описание структуры модели Hook.
Hook: {
// объект, хранящий в себе пары (ключ, зачение),
// где ключ - имя поля модели,
// значение - объект с описанием свойств данного поля (заголовок, тип поля и т.д.).
properties: {
id: {
title: "Id",
type: "integer",
readOnly: true,
},
name: {
title: "Name",
type: "string",
minLength:1,
maxLength: 512,
},
type: {
title: "Type",
type: "string",
enum: ["HTTP","SCRIPT"],
},
when: {
title: "When",
type: "string",
enum: ["on_object_add","on_object_upd","on_object_del"],
},
enable: {
title:"Enable",
type:"boolean",
},
recipients: {
title: "Recipients",
type: "string",
minLength: 1,
}
},
// массив, хранящий в себе имена полей, являющихся обязательными для заполнения.
required: ["type","recipients"],
}
},
// объект, хранящий в себе пары (ключ, значение),
// где ключ - путь предсталения (шаблонный URL),
// значение - объект с описанием свойств представления.
paths: {
// описание структуры представлений по пути '/hook/'.
'/hook/': {
// схема представления для get запроса по пути /hook/.
// схема представления, соответствующей странице просмотра списка объектов модели Hook.
get: {
operationId: "hook_list",
description: "Return all hooks.",
// массив, хранящий в себе объекты со свойствами фильтров, доступных для данного списка объектов.
parameters: [
{
name: "id",
in: "query",
description: "A unique integer value (or comma separated list) identifying this instance.",
required: false,
type: "string",
},
{
name: "name",
in: "query",
description: "A name string value (or comma separated list) of instance.",
required: false,
type: "string",
},
{
name: "type",
in: "query",
description: "Instance type.",
required: false,
type: "string",
},
],
// объект, хранящий в себе пары (ключ, значение),
// где ключ - код ответа сервера;
// значение - схема ответа сервера.
responses: {
200: {
description: "Action accepted.",
schema: {
properties: {
results: {
type: "array",
items: {
// ссылка на модель, данные которой пришли в ответе от сервера.
$ref: "#/definitions/Hook",
},
},
},
},
},
400: {
description: "Validation error or some data error.",
schema: {
$ref: "#/definitions/Error",
},
},
401: {
// ...
},
403: {
// ...
},
404: {
// ...
},
},
tags: ["hook"],
},
// схема представления для post запроса по пути /hook/.
// схема представления, соответствующей странице создания нового объекта модели Hook.
post: {
operationId: "hook_add",
description: "Create a new hook.",
parameters: [
{
name: "data",
in: "body",
required: true,
schema: {
$ref: "#/definitions/Hook",
},
},
],
responses: {
201: {
description: "Action accepted.",
schema: {
$ref: "#/definitions/Hook",
},
},
400: {
description: "Validation error or some data error.",
schema: {
$ref: "#/definitions/Error",
},
},
401: {
// ...
},
403: {
// ...
},
404: {
// ...
},
},
tags: ["hook"],
},
}
}
}
Таким образом, что мы имеем: у нас есть back-end, на котором описана вся логика, связанная с моделями. Затем в игру вступает OpenAPI, который на основе описания моделей формирует JSON с описанием структуры API. Далее эстафетная палочка передается клиенту, который анализируя сформированный OpenAPI JSON автоматически генерирует веб-интерфейс.
Помните задачу про добавление пользовательских хуков? Вот как бы мы ее реализовали в приложении на базе VSTUtils:
Теперь благодаря VSTUtils нам не нужно ничего писать с нуля. Вот, что мы делаем, чтобы добавить возможность создавать пользовательские хуки:
В итоге, у нас получилось довольно неплохое решение, мы добились своей цели, наш front-end стал автогенерируемым. Процесс внедрения новых фич в существующие проекты заметно ускорился: релизы стали выходить раз в 2 недели, тогда как ранее мы выпускали релизы раз в 2-3 месяца с гораздо меньшим количеством новых фич. Хотелось бы заметить, что команда разработчиков осталась прежней, такие плоды нам дала именно новая архитектура приложения.
Но, как известно, нет предела совершенству, и VSTUtils не стал исключением.
Не смотря на то, что нам удалось автоматизировать формирование front-end’а, получилось не прям то решение, которое мы изначально хотели.
Архитектура приложения на стороне клиента не была досконально продумана, и получилась не столь гибкой, какой могла бы быть:
И поскольку в нашей компании мы придерживаемся DevOps подхода и стараемся наш код максимально стандартизировать и формализовать, то в феврале этого года мы решили провести глобальный рефакторинг front-end’a фреймворка VSTUtils. У нас было несколько задач:
Среди наиболее популярных JS фреймворков: Angular, React, Vue, наш выбор пал на Vue поскольку:
Сравнительная таблица [2] размеров фреймворков Gzipped версии
Фреймворк | Размер, kb |
---|---|
Angular 2 | 111 |
Angular 2 + RX | 143 |
Angular 1.4.5 | 51 |
React 0.14.5 + React DOM | 40 |
React 0.14.5 + React DOM + Redux | 42 |
React 15.3.0 + React DOM | 43 |
Vue 2.4.2 | 21 |
Процесс глобального рефакторинга front-end’а VSTUtils занял около 4 месяцев и вот что у нас в итоге вышло:
Front-end фреймворка VSTUtils по-прежнему состоит из двух больших блоков: первый занимается парсингом схемы OpenAPI, второй – рендерингом представлений и маршрутизацией между ними, но оба этих блоков перенесли ряд существенных изменений.
Был полностью переписан механизм, парсящий схему OpenAPI. Изменился подход к парсингу этой схемы. Мы постарались сделать архитектуру front-end’а максимально похожей на архитектуру back-end’a. Теперь на стороне клиента у нас есть не просто единая абстракция в виде представлений, теперь у нас есть еще абстракции в виде моделей и QuerySet’ов:
Блок, отвечающий за рендеринг и маршрутизацию, тоже заметно преобразился. Мы отказались от самописных JS SPA библиотек в пользу фреймворка Vue.js. Мы разработали собственные Vue компоненты, из которых строятся все страницы нашего веб-приложения. Маршрутизация между представлениями осуществляется с помощью библиотеки vue-router, а в качестве реактивного хранилища состояния приложения мы используем vuex.
Хотелось бы также отметить, что на стороне front-end’а реализация классов Model, QuerySet и View не зависит от средств реализации рендеринга и маршрутизации, то есть если мы вдруг захотим перейти от Vue к какому-то другому фреймворку, например на React или на что-то новое, то все что нам нужно будет сделать, это переписать компоненты Vue на компоненты нового фреймворка, переписать роутер, хранилище, и все – фреймворк VSTUtils снова будет работоспособен. Реализация классов Model, QuerySet и View останется прежней, поскольку никак не зависит от Vue.js. Мы считаем, что это является весьма неплохим подспорьем для возможных будущих изменений.
Таким образом, нежелание писать “дублирующий” код вылилось в задачу по автоматизации формирования front-end’a веб-приложния, которая была решена с помощью создания фреймворка VSTUtils. Нам удалось построить архитектуру веб-приложения так, что back-end и front-end гармонично дополняют друг друга и любое изменение в структуре API автоматически подхватывается и отображается должным образом на клиенте.
Преимущества, которые мы получили от формализации архитектуры веб-приложения:
К недостаткам подобной формализации архитектуры веб-приложения можно отнести следующее:
На этом мой рассказ подходит к концу, спасибо за внимание!
Автор: akhmadullin_roman
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/320878
Ссылки в тексте:
[1] OpenAPI: https://swagger.io/docs/specification/about/
[2] Сравнительная таблица: https://gist.github.com/Restuta/cda69e50a853aa64912d
[3] меньше: https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.1dpzl7fwb)
[4] Репозиторий VSTUtils: https://github.com/vstconsulting/vstutils
[5] Пример проекта, созданного на базе VSTUtils: https://github.com/vstconsulting/polemarch
[6] Источник: https://habr.com/ru/post/456146/?utm_campaign=456146&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.