[Из песочницы] SVG индикатор загрузки на Vue.js
Привет! Учусь на front-end, и параллельно, в учебном проекте, разрабатываю SPA на Vue.js для back-end, который собирает данные от поискового бота. Бот нарабатывает от 0 до 500 записей, и я должен их: загрузить, отсортировать по заданным критериям, показать в таблице.
Ни back-end ни бот, сортировать данные не умеют, поэтому мне приходятся загружать все данные и обрабатывать их на стороне браузера. Сортировка происходит очень быстро, а вот скорость загрузки, зависит о коннекта, и указанные 500 записей могут загружаться от 10 до 40 секунд.
Поначалу, при загрузке, я показывал спинер, недостаток которого — пользователь не знает когда закончится загрузка. В моём случае заранее известно количество записей которые отыскал бот, поэтому можно показать сколько % записей загружено.
Чтобы скрасить пользователю ожидание, я решил показать ему процесс загрузки:
Вот анимация результата, к которому я стремился и получил:
… по-моему, получилось забавно.
В статье я покажу как продвигался к результату шаг за шагом. Графики функций в браузере я до селе не рисовал, поэтому разработка индикатора принесла мне простые, но новые знания о применении SVG и Vue.
Canvas я применял в простой игре-змейке на JS, а SVG, в одном проекте, я просто вставлял на страницу в теге object и заметил, что при масштабировании, SVG-картинки всегда сохраняли чёткость (на то он и вектор), а у Canvas наблюдалось размытие изображения. На основании этого наблюдения, я решил рисовать график с помощью SVG, ведь надо когда-то начинать.
С учётом выбранного фрймворка Vue, и выбранного способа формирования изображения с помощью SVG, составил себе следующий план работ:
У меня установлен vue cli. Для создания нового проекта, в командной строке ввожу vue create loadprogresser, настройки проекта выбираю default создаётся новый vue-проект с названием loadprogresser, дальше убираю из него лишнее:
Поиск и изучение информации по теме применения SVG совместно с Vue
Отличный сайт с полезной инфой по HTML, CSS и SVG css.yoksel.ru Хороший пример с SVG размещён в документации самого Vue SVG-график Example и по такой ссылочке. На основе этих материалов родился минимальный шаблон компонента с SVG с которого я и стартую:
Эксперименты с формированием и изменением SVG в контексте Vue
SVG Прямоугольник rect
rect — прямоугольник, самая простая фигура. Создаю svg с размерами 100×100 px, и рисую прямоугольник rect с начальными координатами 25:25 и размерами 50×50 px, по умолчанию цвет заливки чёрный (нет стилизации)
SVG стилизация и псевдоэлемент hover:
Попробую стилизовать прямоугольник rect в svg. Для этого к svg добавляю класс «sample», в секции style vue-файла добавляю стили .sample rect (раскрашиваю прямугольник rect жёлтым цветом) и .sample rect: hover который стилизует элемент rect при наведении на него курсора мыши:
Вывод: svg отлично встраивается в template vue-файла и стилизуется прописанными стилями. Начало положено!
SVG path как основа индикатора
В этом разделе я заменю rect на path, в атрибут d тега path передам из vue строку D с координатами пути. Связь производится через v-bind:d="D", что сокращённо записывается как :d="D"
Строка D=«M 0 0 0 50 50 50 50 0 Z» рисует три линии с координатами 0:0→0:50→50:50→0:50 и замыкает контур по команде Z, образуя квадрат 50×50 px начинающийся из коодинат 0:0. С помощью стиля «path» фигуре придаётся жёлтый цвет заполнения и серая рамка в 1 px.
Создание прототипа индикатора загрузки
В минимальном варианте, я сделал простую диаграмму. В шаблоне вставлен svg-контейнер высотой 100 px, шириной 400 px, внутри размещён тег path, атрибуту d которого я добавляю сгенерированную строку-путь d из данных vue, которая в свою очередь формируется из массива timePoints куда, каждые 10 мс, добавляются одно из 400 (по ширине контейнера)случайное число в диапазоне от 0 до 100. Тут всё просто, в хуке жизненного цикла created, вызывается метод update в котором добавляются новые (случайные) точки в диаграмму через метод addTime, потом метод getSVGTimePoints возвращает строку для передачи в PATH, через setTimeout перезапускается метод update
Подробнее о формировании строки для PATH
Строка для PATH формируется в методе getSVGTimePoints, из массива timePoints который я обрабатываю с помощью reduce. В качестве начального значения reduce использую «M 0 0» (начать с координаты 0:0). Далее в reduce, к строке будут добавляться новые пары относительных координат dX и dY. За то, что координаты будут относительными, отвечает прописная буква «l» (большая «L» сообщает о абсолютных координатах), после «l» размещается dX и потом dY, разделённых пробелами. В этом прототипе, dY = 1 (приращение на 1 px), в дальнейшем, по оси X буду перемещаться с приращением dX вычисленным из ширины контейнера и количества точек которые в нём необходимо разместить. В последней строке формирования PATHpath +=`L ${this.timePoints.length} 0`
я принудительно, от последней точки, достраиваю линию до оси Х. Если потребуется замкнуть контур, можно дописать в конец строки «Z», я поначалу думал, что без замкнутого контура, полученная фигура не будет заполняться (fill), но это оказалось не так, там где не замкнуто, не будет прорисована stroke — обводка.
getSVGTimePoints:function(){
let predY = 0
let path = this.timePoints.reduce((str, item)=>{
let dY = item - predY
predY = item
return str + `l 1 ${dY} `
},'M 0 0 ')
path +=`L ${this.timePoints.length} 0`// Z` контур можно не замыкать
return path
},
Продолжу вносить изменения. Мой индикатор должен масштабироваться по ширине и высоте для того чтобы все переданные точки вписались в заданный контейнер. Для этого надо обратится к DOM и узнать размеры контейнера
- ref — получение информации о элементе DOM
DIV-ву контейнеру (в который вставлен svg) я добавляю класс wrapper чтобы передать ширину и высоту через стили. И чтобы svg занял всё пространство контейнера, задал его высоту и ширину 100%. RECT, в свою очередь, тоже займёт всё пространство контейнера и будет фоном для PATH
Для того чтобы найти мой DIV-контейнер в виртуальном DOM Vue, добавляю атрибут ref и присваиваю ему имя по которому буду осуществлять поиск
ref="loadprogresser". В хуке жизненного циклаmountedя вызову метод getScales (), в котором, строкойconst {width, height} = this.$refs.loadprogresser.getBoundingClientRect()узнаю ширину и высоту DIV-элемента после его появления в DOM.Дальше простые расчёты приращения по оси Х зависящего от ширины контейнера и кол-ва точек которые хотим в него уместить. Масштаб по оси Y пересчитывается каждый раз при нахождении максимума в переданном значении.
- transform — изменение системы координат
На этом этапе я замечаю, что надо бы изменить систему координат так, чтобы координата 0:0 начиналась из нижнего левого угла, и ось Y росла бы вверх, а не вниз. Можно, конечно, сделать рассчёты для каждой точки, но в SVG есть атрибут transform, позволяющий трансформировать координаты.
В моём случае требуется применять к Y координатам масштаб -1 (чтобы значения Y откладывались вверх), и сместить начало координат на минус высоту контейнера. Так как высота контейнера может быть любой (задаётся через стили), то пришлось формировать строку трансформации координат в хуке
mountedтаким кодом:this.transform = `scale( 1, -1) translate(0,${-this.wrapHeight})`Но сама по себе трансформация применённая к PATH не сработает, для этого надо обернуть PATH в группу (тег g) к которой и применить трансформации координат:
В итоге координаты правильно развернулись, индикатор загрузки стал ближе к задуманному дизайну
- SVG text и центрирование текста
Текст нужен для вывода % загрузки. Размещение текста в центре по вертикали и горизонтали в SVG довольно просто организовать (по сравнению с HTML/CSS), на помощь приходят атрибуты (сразу прописываю значения) dominant-baseline=«central» и text-anchor=«middle»
Текст в SVG выводится соответствующим тегом:
{{TextPrc}} где TextPrc привязка к соответствующей переменной, вычисляемой по простому соотношению ожидаемого количества точек к переданному количеству
this.TextPrc = `${((this.Samples * 100)/this.maxSamples) | 0} %`.Координаты начала x=»50%» y=»50%» соответствуют центру контейнера, а за то чтобы текст выровнялся по вертикали и горизонтали, отвечают атрибуты dominant-baseline и text-anchor.
Базовые вещи по теме отработаны, теперь надо выделить прототип индикатора в отдельный компонент.
Выделение индикатора загрузки в отдельный Vue-компонент
Для начала определюсь с данными которые буду передавать в компонет, это будут: maxSamples — кол-во сэмплов в 100%-ах ширины, и Point — единица данных (точка) которая будет внесена в массив точек (на основании которого, после обработки, сформируется график). Данные передаваемые компоненту от родителя, размещаю в секции props
props:{
maxSamples: {//кол-во сэмплов в 100%-ах ширины
type: Number,
default: 400
},
Point:{//новая точка
value:0
}
}
Проблемы с реактивностью
За то, что новая переданная в компонент точка будет обработана, отвечает computed свойство getPath которое зависит от Point (а раз зависит, то и перевычисляется при изменении Point)
//шаблон
...
...
//свойства компонента
props:{
...
Point:{
value:0
}
//вычисляемое свойство
computed:{
getPath(){
this.addValue({value:this.Point.value})
return this.getSVGPoints()//this.d
}
},
Я сначала сделал Point типа Number, что логично, но тогда не все точки попадали в обработку, а только отличающиеся от предыдущих. Например, если из родителя передавать в такой Point только число 10, то на графике отрисуется только одна точка, все последующие будут проигнорированы так как они не отличаются от предыдущих.
Замена типа Point с Number на объект {value:0} привело к желаемому результату — computed свойство getPath () теперь обрабатывает каждаю переданнаю точку, через Point.value передаю значения точек
Вызов из родительского компонента и передача параметров
Для работы с компонентом требуется его импортировать в родительский компонентimport Progresser from "./components/Progresser"
и объявить в секцииcomponents: {Progresser }
В шаблон родительского компонета, компонент-индикатор progresser вставляется следующей конструкцией:
Через класс «progreser» в первую очередь задаются размеры блока у индикатора. В props компонента передаются maxSamples (макс кол-во точек в графике) из переменной родителя SamplesInProgresser, и в props Point передаётся очередная точка (в виде объекта) из переменной-объекта Point родителя. Point родителя расчитывается в функции update, и представляет собой увеличивающиеся случайные числа. Получаю такую картинку:

Применение компонента в SPA
Приступаем к тому, ради чего всё затевалось. И так, у меня есть асирхронные операции по загрузке из базы записей о неких личностях. Время выполнения асинхронной операции заранее неизвестно. Я буду измерять время выполнения банальным способом, с помощью new Date ().getTime () до и после операции, и полученную разность времени буду передавать в компонент. Естественно, индикатор будет встроен в блок, который будет появляться на этапе загрузки, и затенять собой таблицу для которой загружаются данные.
async getCandidatesData(){
...
this.LoadRecords = true //сообщаю что началась загрузка, чтобы поверх контента появился блок с индикатором
...
this.SamplesInProgresser = uris.length //сообщаю компоненту сколько записей буду загружать
...
for (let item of uris) {//в uris массив URL которые надо загрузить
try {
const start = new Date().getTime()//время до операции
candidate = await this.$store.dispatch('GET_CANDIDATE', item)
const stop = new Date().getTime()//время после выполнения
this.Point = {value:(stop-start)}//передаю разность в Point
...
В data компонента-родителя прописываю что касается индикации загрузки:
data (){
return {
...
//Индикатор загрузки
LoadRecords:false,
SamplesInProgresser:400,
Point:{value:0}
}
И в шаблоне:
Как и прогнозировалось, ничего сложного. До какого-то момента можно относится к SVG как обычным HTML-тегам, со своей спецификой. SVG — мощный инструмент который я теперь чаще буду использовать в своей работе для визуализации данных
