Создание прокрутки прогресса в стиле Tech Crunch с помощью Vue, SVG и TypeScript

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

Он будет построен с использованием Vue, TypeScript и SVG. Тем не менее, мы полностью отделим бизнес-логику (вычисления, связанные с прокруткой и анимацией) от Vue, чтобы вы могли легко адаптировать ее к выбранной вами структуре.

Скроллер Tech Crunch: Вы можете увидеть скроллер, который мы копируем, в действии в любой статье Tech Crunch, например, в этой. По мере прокрутки вниз зеленый ползунок в правом верхнем углу с X медленно заполняется.

Исходный код: исходный код доступен здесь.

Демо: рабочая демонстрация доступна здесь.

Эта статья не является типичным руководством «вот код, скопируйте и вставьте его» - я сначала напишу базовое доказательство концепции, а затем проведу его рефакторинг, обсудив некоторые передовые практики, которые я изучил при создании приложений Vue и независимых от фреймворков библиотек. с годами.

В частности, я рассмотрю разделение независимой от платформы логики (например, бизнес-логики, которая не использует какие-либо API-интерфейсы Vue) от кода, специфичного для платформы (например, частей приложения, которые ссылаются на this, ссылаясь на экземпляр Vue или использовать систему реактивности Vue, такую ​​как вычисляемые свойства).

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

Я начну с создания нового приложения Vue с использованием vue-cli со следующими параметрами: TyepeScript, Babel, синтаксис без компонентов класса.

Сначала я создам компонент под названием Progress.vue в src/components. Внутри добавьте следующий минимальный код - объяснение следует.

Это будет работать следующим образом: пользователь укажет два маркера. Они начинаются как null. Они указывают, между какими двумя точками мы хотим отслеживать прогресс прокрутки. На данный момент это будут просто элементы HTML, выбранные document.querySelector, но вы также можете разрешить пользователю передавать их как свойства. Мы будем хранить процент прокрутки пользователя в объекте data.

Теперь добавьте следующий код в App.vue, который мы будем использовать для тестирования компонента <Progress />.

Много шаблонов, но пока ничего особенного. Рендерим кучу <div />. Затем <div id=”progress-marker-start”></div> - это момент, когда мы начнем отслеживать прогресс прокрутки. Далее у нас есть еще несколько <div /> и конечная точка <div id=”progress-marker-end”></div>.

Это дает следующее:

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

Расчет смещения маркеров

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

  1. Текущая позиция, к которой прокручены пользователи
  2. Положение progress-marker-start по оси y относительно верха области просмотра
  3. Положение progress-marker-end по оси y относительно верха области просмотра

Получить текущую позицию прокрутки тривиально - мы можем использовать window.scrollY и считать это днем. Две другие позиции y немного сложнее. Часть любого алгоритма для вычисления позиции элемента HTML почти всегда будет включать getBoundingClientRect(). Моя стратегия ничем не отличается. Сначала моя стратегия заключалась в следующем:

Это казалось нормальным, пока вы не добавили отступ к элементу <div id="app" /> - тогда document.body.getBoundingClientRect().top перестанет точно отражать верхнюю часть документа! Следующее изображение иллюстрирует это:

Вы можете видеть, что 25 пикселей не учтены, что обозначено красной стрелкой. Я решил это с помощью document.documentElement.getBoundingClientRect().top. document.documentElement относится к элементу <html />, а getBoundingClientRect().top возвращает 0:

Пока что у меня это отлично работает, но, возможно, есть предостережения и в отношении этого метода (например, если кто-то решил добавить margin в HTML, что я не очень часто видел, если вообще когда-либо).

Обладая этими знаниями, мы можем добавить симпатичный маленький метод, который будет получать смещение по оси Y для элемента. Добавьте methods ключ со следующей функцией в Progress.vue:

Расчет хода прокрутки

Я хочу начать подсчет процента прокрутки не тогда, когда <div id=”progress-marker-start”></div> появляется на экране, а когда он исчезает над верхней частью области просмотра. Итак, все, что нам нужно сделать, это взять различие в позиции y <div id=”progress-marker-start”></div> и <div id=”progress-marker-end”></div> и вычесть window.innerHeight. Это даст нам общее количество пикселей, по которым мы хотим отслеживать прогресс прокрутки.

Используя App.vue, который я определил, получается около 2011 пикселей.

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

Собирая все это вместе, мы получаем следующую функцию. Добавьте его в Progress.vue под methods:

Последнее, что нужно сделать, - это вызвать этот метод, когда пользователь прокручивает страницу. Это не последний код - мы сделаем много улучшений, но самый простой способ проверить это - добавить прослушиватель событий в ловушку mounted в Progress.vue:

Оно работает! Мы внесем множество улучшений, но это отличное подтверждение концепции.

Пока что весь тег <script> для Progress.vue выглядит следующим образом:

Извлеките логику из компонента

Прежде чем мы добавим красивый пользовательский интерфейс, такой как у Tech Crunch, мы можем отделить логику прокрутки от компонента. В настоящее время, если кто-то хочет использовать наш компонент прогресса прокрутки, им нужно задействовать весь Vue - не идеально, если ваш сайт еще не использует Vue. Ни одна из написанных нами логических схем даже не использует API Vue.

Давайте выделим логику и сделаем <Progress /> тонкую оболочку Vue, соединяющую Vue и реальную бизнес-логику. Это дает нам возможность выпустить агонистическую библиотеку прогресса прокрутки, которую авторы затем могут интегрировать в свое приложение Vue / React / Angular / любое другое.

Я создам progress.ts скрипт в каталоге components и добавлю функции, описанные выше, внеся небольшие изменения в их сигнатуры:

Обновите Progress.vue. Раздел <script> в <Progress /> теперь представляет собой лишь тонкую оболочку логики в progress.ts:

Все еще работает! Давайте добавим круговой интерфейс, который заполняется по мере прокрутки пользователей, как в Tech Crunch.

Немного SVG

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

Базовый круг SVG нарисован следующим образом:

Это выглядит так:

Следующее интересующее нас свойство - stroke-dasharray. Вы можете узнать больше об этом MDN, однако основная предпосылка заключается в том, что вы можете рисовать фигуру с помощью тире вместо сплошной границы. Вот некоторые примеры:

Для нашей цели мы хотим, чтобы stroke-dasharray равнялось длине окружности нашего круга. Мы можем вычислить это программно: 2 * Math.PI * radius (который мы указали равным 50 пикселей). Для нашего круга получается 314 пикселей. Следующий фрагмент демонстрирует этот расчет:

Пока ничего не изменилось - по-прежнему красный кружок. Однако теперь мы можем воспользоваться другим свойством stroke-dashoffset. Устанавливает смещение (от того места, где рисуется тире). В настоящее время наш круг состоит из одной большой черточки, установленной с использованием stroke-dasharray, то есть полной окружности круга. Давайте попробуем разные числа для stroke-dasharray и посмотрим, как все изменится:

Чем больше stroke-dashoffset, тем меньше красный круг! По мере увеличения размера красная граница почти полностью исчезает. Итак, когда пользователь прокрутил 0% измеряемой области, мы хотим, чтобы stroke-dashoffset был (окружность * прогресс), где прогресс находится между 0 и 1. Например:

Близко, но не совсем. Это смещает круг на 25%, рисуя оставшиеся 75%. На самом деле мы хотим наоборот - нарисовать только 25%. Таким образом, наш расчет выглядит следующим образом (окружность - (окружность * прогресс)):

Выглядит неплохо!

Интеграция SVG Circle и Logic

Теперь у нас есть все части, которые нужно рисовать кругом при прокрутке: процентное изменение и SVG, который мы можем обновить с помощью JavaScript. Давай сделаем это. Обновите раздел <template> в Progress.vue

Затем обновите тег <style>. Теперь это намного проще - теперь мы делаем большую часть стиля в SVG.

Далее у нас есть обновление до progress.ts. Я добавил функцию updateCircle, которая реализует рассмотренные выше вычисления и охватывает несколько крайних случаев. Я также внес небольшие изменения в getScrollPercentage. Теперь готовый файл progress.ts выглядит так:

Наконец, давайте воспользуемся функцией updateCircle. Обновите раздел <script> в Progress.vue:

Я добавил вызов updateCircle в обратном вызове прослушивателя событий scroll. Я также удаляю прослушиватель событий на destroyed, чтобы избежать потенциальной утечки памяти. Теперь это работает! При прокрутке вверх и вниз вы можете видеть, как обводка круга увеличивается и уменьшается, отражая ваш прогресс между двумя маркерами. Попробуйте демо здесь.

Последнее улучшение - requestAnimationFrame

Чтобы узнать, почему это полезно, изучите requestAnimationFrame и пассивные прослушиватели событий - информации много. Есть и другие улучшения и оптимизации, о которых я мог бы написать в будущей статье.

В этой статье говорилось:

  • некоторые предостережения в отношении getClientBoundingRect
  • использование Vue в качестве тонкой оболочки вокруг вашей бизнес-логики
  • базовый SVG

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

Исходный код доступен здесь, а рабочая демонстрация - здесь.

Изначально опубликовано в моем личном блоге.