Как мы уменьшили количество кода для генерации графиков в 10 раз, сделав Vue-компонент
Привет, Хабр! С вами Галя Охотникова, веб-разработчик из Топвизора. Наша компания разрабатывает платформу для SEO-специалистов, которая позволяет анализировать позиции сайта в поисковиках и находить конкурентов в выдаче. Аналитика всегда включает в себя работу с большим количеством данных, и делать это проще, если они представлены визуально. Поэтому графики и диаграммы в нашем сервисе используются везде, и разработчикам часто приходят задачи, связанные с графиками.
В статье я расскажу, как раньше мы работали с графиками, что было в этом процессе не так и как мы решили эти проблемы (попутно встретив новые от библиотеки графиков, которую использовали).
Зачем всё это было нужно
Когда разработчику прилетала задача на график для какого-то нового раздела, он заново писал для этого графика код, а также у всех графиков были свои стили. В админке сайта каждый вставлял графики вообще кто как хочет — непубличное же! А ещё у графиков не было никакого API и документации. При рефакторинге мы насчитали почти 10 (в зависимости от того, как считать) разных разделов с графиками, то есть разных, не связанных между собой участков кода.
Наша платформа росла, в ней прибавлялись инструменты и разделы в админке, а код дублировался. Разбираться в нем становилось экспоненциально сложнее, и время программистов расходовалось неэффективно в колоссальных количествах. Так что мы решили создать универсальный Vue-компонент topHighcharts, который упростил бы процесс разработки новых графиков.
Как мы решили проблему
Стек: Vue3, TypeScrypt, Highcharts, JSDoc
Возможности нового компонента:
определены все типы данных (при разработке понятно, что передавать);
определены стили (ко всем графикам применяются одинаковые стили, не требующие редактирования);
добавлено множество дополнительных опций, для которых не нужно писать код (выравнивание графика по ширине, tooltip с процентами, круговая и столбчатая диаграммы);
реализованы пресеты для фильтров;
в мобильной версии график открывается в модальном окне и не занимает место на экране.
Архитектура компонента
Компонент состоит из:
шапки;
графика;
футера.
Слева и справа от фильтров в шапке и в футере есть Vue-слоты, в которые можно вставить любые элементы (кнопки, выпадающие списки, поля ввода и т. д.).
Компонент принимает следующие параметры в качестве пропсов:
chartId: {
type: String,
required: true,
},
constructorType: 'chart' as 'chart' | 'mapChart' | 'stockChart' | 'ganttChart',
filters: Array>,
loader: Function as PropType,
options: Object as Highcharts.Options,
mobileOpenerSelector: String,
extendType: '' as '' | 'column' | 'pie',
extendTooltipPercent: {
type: Boolean,
default: false,
},
extendAlignChart: {
type: Boolean,
default: true,
},
showToggleLegend: {
type: Boolean,
default: false,
}
Под капотом для графика генерируются дефолтные опции, но для гибкости есть возможность передать дополнительные настройки или изменить дефолтные с помощью options.
Если нужна круговая или столбчатая диаграмма, то не нужно писать много сложного кода для этого. Достаточно передать необходимый extendType (в будущем планируется добавить и другие типы диаграмм, например, картографические).
Ещё можно воспользоваться уже реализованным функционалом: tooltip с процентами, выравниванием графика по ширине, кнопкой, которая показывает легенду графика.
Стили
Для компонента topHighcharts, то есть для всех графиков глобально, определены стили: цвета линий, сетки, текста и фона, стилизован tooltip, сделано кастомное меню для экспорта графиков в различные форматы с использованием элементов из Storybook компании.
Фильтры
Для создания фильтров был написан тип, в котором содержится компонент, выводящий фильтр, имя/имена параметров для запроса к API и дополнительные настройки для каждого фильтра (например, значения для выпадающего списка, значения по умолчанию).
type Filter = {
**component?: T,
modelName: string,
modelName2?: string,
modelName3?: string,
**props?: T['props'],
}
Для фильтров реализованы пресеты, в которых генерируются стандартные фильтры. Функции генерации вызываются извне, в них можно передать объект с дополнительными настройками типа FIlter, если это необходимо.
const genCurrencyFilter = (_filter?: Filter): Filter => {
const filter: Filter = {
component: Currency,
modelName: 'currency',
props: {
modelValue: 'RUB',
propsSelect: {
options: new Map().set('RUB', 'RUB').set('USD', 'USD'),
},
},
};
if (_filter) return merge(filter, _filter);
return filter;
};
Выводятся фильтры с помощью директивы : is, которая позволяет динамически выводить разные компоненты. Это удобно, потому что не нужно внутри topHighcharts импортировать много разных файлов и дописывать импорт новых, когда применяются нестандартные фильтры. Таким образом, логика изолирована и компонент передается извне.
Допустим, нам нужно импортировать генераторы настроек фильтров из ./filters/presets.ts и выполнить генерацию фильтров с нужными настройками.
Тогда получится так: [genCurrencyFilter (), genPeriodFilter ()];
Типы
Стандартный тип библиотеки Highcharts.Options был расширен кастомными полями, которые нужны для более гибкой настройки графиков.
interface TVOptions extends Highcharts.Options {
TV: {
extendType: typeof props['extendType'],
extendTooltipPercent: typeof props['extendTooltipPercent'],
extendAlignChart: typeof props['extendAlignChart']
};
}
Пример: передаем в компонент extendAlignChart = true. Тогда график будет выровнен по ширине, и разработчику не придется самому реализовывать логику работы этой фичи или искать ответ на вопрос, как это сделать — всё уже готово, остается просто передавать параметры.
Аналогично:
interface TVChart extends Highcharts.Chart {
TV?: {
/**
* Объект с результатами, полученными через API
*/
result?: Result
};
innerText?: any,
}
Это нужно для того, чтобы было проще взаимодействовать с нашим API. Стандартный тип расширен параметром, который диктует то, что должно возвращать API. Это важно, ведь от правильного формата переданных данных зависит вся работа графика.
Мобильная версия
Для того чтобы на маленьких устройствах график не занимал место, реализована мобильная версия, открывающая график в модальном окне. Разработчик в пропсах передает селектор, в который будет телепортирована кнопка для открытия окна. Компонент с графиком телепортируется в модальное окно и поэтому сохраняет своё состояние даже при изменении размеров экрана.
Какие мы встретили проблемы и что придумали
Использование npm-пакета для интеграции с фрейморком Vue
В процессе разработки долгое время использовался npm-пакет vue-highcharts. Он позволяет использовать библиотеку как Vue-компонент. Впоследствии hightcharts-vue был удален из проекта по причинам ненужной абстракции, усложняющей логику работы. Данная библиотека создавала глубокие копии всех опций графика при каждом изменении, из-за чего могла возникать бесконечная рекурсия, которую приходилось обходить костылями. Вместо этого была написана собственная прослойка между Vue и библиотекой Highcharts.
Тормоза у всплывающей подсказки
Поначалу кастомный tooltip сильно тормозил, и мы не могли понять, в чем дело, ведь в старом коде он работал, как нужно. Казалось, что это из-за того, что в процессе вывода происходит подсчет процентов, ведь стандартный tooltip работал быстро. Подсчет процентов был вынесен в утилиты, таким образом, проценты считались только один раз: при инициализации компонента. Но это не решило проблему.
Следующим шагом было изменение версии используемой библиотеки. После отката на старую версию (9.0.0) tooltip перестал тормозить. Как оказалось, причина такого явления — чрезмерная генерация узлов AST. С версии highcharts 9.2.0 оптимизация узлов AST была отключена, поэтому, если узлов много, их генерация выполняется долго. Это не критично для данной библиотеки, потому что обычно требование к скорости генерации текста не высокие — генерация происходит один раз в момент отрисовки графика. Однако, если генерация связана, например, с движением мыши, как в данном случае, могут возникать проблемы.
Для решения проблемы при использовании новой версии библиотеки была добавлена функция сжатия строки, удаляющая ненужные пустые узлы AST-дерева. Это ускоряет работу tooltip примерно в 10 раз.
Ошибки при первой отрисовке графика
Есть случаи, когда метод загрузки графика может вызываться извне. При таком сценарии возникала ошибка загрузки. Либо график вообще не отображался, либо данные загружались не полностью. Это было связано с тем, что load вызывался до монтирования Vue-компонента. Чтобы система была устойчива к данной проблеме, была добавлена «заглушка» в методе load, которая останавливает выполнение загрузки до тех пор, пока экземпляр компонента не будет вмонтирован в DOM-дерево. Реализуется с помощью внутреннего метода проекта waitWhile, который принимает на вход условие и, пока это условие не станет ложным, устанавливает временной интервал, тем самым останавливая выполнение программы.
Что в итоге
Мы разработали универсальный, гибкий и удобный Vue-компонент, который:
изолирует сложную логику;
помогает избежать дублирования кода;
диктует разработчикам строгие правила по работе над графиками;
позволяет быстро выполнять задачи и не тратить время на поиск ответов в документации библиотеки Highcharts или на чтение старого кода в попытках понять, где хорошо реализовано, а где — плохо.
Все графики теперь выполнены в едином визуальном стиле, и необходимые изменения можно применять сразу ко всем графикам, которые реализованы с помощью этого компонента.
Часто бизнес воротит нос, когда слышит слово «рефакторинг». С ним нужно говорить на его языке: пусть не прямой выручки, но сокращения time to market, времени выполнения задач и проверки кода, упрощения ТЗ и облегчения будущего процесса разработки. Всё это опосредованно влияет на выручку бизнеса, скорость добавления нужных ему новый фичей и затраты на их разработку. И разработчикам-технарям хорошо, и бизнесу выгодно.