Что будет, если соединить любовь к музыке и к разработке? В моем случае получился pet-проект по созданию музыкального стримингового веб-приложения. Меня зовут Андрей, я работаю frontend-разработчиком в компании «КОРУС Консалтинг». И сейчас я расскажу, как стоит подходить к разработке pet-проектов, чтобы учиться эффективнее.
Понятно, что не все навыки можно освоить через рабочие задачи, в таком случае нам на помощь и приходят pet-проекты. Они дают возможность не только попробовать новое, но и экспериментировать с идеями и технологиями без ограничений.
Если смотреть на pet-проект не как на лабораторную работу, а как на будущий продукт, начинаешь лучше понимать весь цикл разработки. При таком подходе ты сперва выступаешь в роли заказчика: определяешь, какую задачу пользователя решаешь, как это сделать наилучшим образом, а потом подбираешь технологии, которыми легче всего реализовать функционал или которые хочется попробовать.
Продуктовый подход позволяет взглянуть на процессы в разработке в миниатюре: понять на собственном примере, откуда появляются таски, как их приоритизировать. Начинаешь задаваться вопросами, как построить архитектуру и какой технологический стек выбрать, чтобы было легко добавлять запланированный функционал (учитывая, что с ростом приложения растет и желание прикручивать новый функционал).
Далее расскажу о развитии моего pet-проекта и чему удалось научиться, используя данный подход.
О проекте
На разработку я вдохновился музыкальными стриминговыми приложениями вроде Spotify, Яндекс Музыки и т.д. Решил попробовать реализовать свой аналог, т.к. тема музыки мне близка и хотелось поработать с музыкальными файлами, используя возможности браузера. Хотелось создать свой zaycev.net.
На данном этапе важно сформировать требования к проекту так, будто у вас есть заказчик (даже если это вы сами). Требования помогут сформировать план работы, сфокусироваться на реализации функционала, который интересен. Таким образом вы создаете обязательство с самим собой завершить проект.
Так как мы смотрим на pet-проект как на прототип продукта, хорошо бы подумать, какие люди могли бы им воспользоваться. Мне хотелось, чтобы потенциальным пользователем приложения мог стать любой человек, и сообщество пользователей могло расширять общую музыкальную библиотеку. Также приложение целится на инди-артистов, которые потенциально смогут найти новых слушателей и бесплатно продвигать свои треки.

Для своего проекта я сформировал следующие требования:
Юзабилити
-
Реализация интерфейса с использованием собственной библиотеки UI компонентов (далее UI kit).
-
Реализовать функциональный плеер с возможностью drag and drop управления громкостью, процессом воспроизведения, кнопками переключения на предыдущий или следующий трек и паузы.
-
Добавить возможность кастомизации интерфейса под желания пользователя, наличие смены языка, темной/cветлой темы.
Технические
-
Проигрывание музыкальных треков и радио на собственном музыкальном плеере, реализованном на базе Web Audio API.
-
Проверка пользователя на «человечность» при регистрации путем отправки e-mail с кодом подтверждения.
-
Настройка CI/CD так, чтобы при добавлении новых фич ранние пользователи сразу могли их видеть.
Пользовательские
-
Дать возможность пользователям прослушивать радио со всего мира.
-
Предоставить возможность сообществу пользователей самостоятельно расширять библиотеку треков.
-
Добавить возможность делиться приложением в соцсетях и мессенджерах.
-
Возможность искать треки по исполнителю, названию, радио по названию, стране, жанрам, поисковик должен подсказывать при вводе.
-
Возможность добавлять треки и радио в избранные.
-
Добавить возможность скачивать музыкальные треки на компьютер.
-
Возможность сброса пароля с отправкой e-mail с кодом сброса.
Проектируем архитектуру
После описания концепции и сбора требований, описываем архитектуру будущего приложения. В моем случае она выглядит так:

Выбор технологического стека
Frontend: для реализации клиентской стороны приложения выбрал наиболее знакомый стек технологий React, Redux Tool Kit, Typesript, Webpack.
Затем решил поэкспериментировать и вместо Webpack использовать Vite, т.к. HMR ускорит разработку, а конфигурацию сборщика можно написать за пару минут. Для добавления мультиязычности использовал i18next. Для работы со стилями выбрал SCSS, также рассматривал Tailwind, но т.к. я хотел описать общую дизайн-систему приложения, глобальные переменные и удобная работа со вложенностью определили мой выбор.
UI kit: в случае с библиотекой компонентов решил остановиться на Storybook, React, Typesript, SCSS, Webpack. Ключевым решением был именно выбор сборщика, т.к. до этого библиотеки я ранее не разрабатывал, а с Webpack нашел много материалов и примеров кода.
Backend: Тут я долго выбирал между Express.js и Nest.js. В Nest.js мне нравился ОПП подход, а Express.js тем, что можно быстро написать простенький сервер, и у меня уже был небольшой опыт работы с ним. В итоге решил остановиться на Express.js. Также было очевидно, что для хранения треков понадобится облако (с которым я ранее не работал). В результате небольшого поиска наткнулся на библиотеку easy-yandex-s3, которая дает возможность легкого взаимодействия с облаком. Для авторизации решил использовать подход sessions+cookie. Для работы с файлами использовал multer, мне понравилась простота документации и возможностью хранить файл в оперативной памяти перед отправкой на облако.
Общение между клиентом и сервером должно было проходить через REST API.
Проектирование технического дизайна
Описываем взаимодействие компонентов интерфейса и таблицы в базе данных.


Проектирование пользовательского дизайна
К сожалению, я имею лишь поверхностные знания о UX/UI дизайне и не могу создать пользовательский интерфейс с нуля, поэтому я нашел готовый подходящий дизайн в интернете и реализовал его.
Реализация функционала
В этой части я бы не хотел досконально описывать все шаги по разворачиванию приложения, поэтому ограничусь описанием, на мой взгляд, более интересного функционала. При этом, чтобы сделать статью более полной и полезной для новичков, в конце я прикрепил ссылки на репозитории с исходным кодом проекта.
Так выглядит, например, реализация контроллера Express для загрузки треков в облако с помощью транзакции Sequelize. Сначала вызываю сервис загрузки в облако, затем сервис записи в БД, затем делаю коммит, чтобы сохранить изменения. В случае возникновения ошибки все изменения откатятся, данные будут консистентны, и операция будет атомарной.
tracksController.post( '/uploadTrack', authChecker, upload.fields([ { name: 'cover', maxCount: 1 }, { name: 'track', maxCount: 1 } ]), async (req, res, next) => { const t = await sequelize.transaction() try { const { trackName, artist } = req.body const cover = req.files?.cover[0]?.buffer const track = req.files?.track[0]?.buffer const { img, mp3 } = await cloudService.upload({ track, cover }) await tracksService.addTrack({ artist, trackName, img, mp3, moderated: false }) await t.commit() res.sendStatus(200) } catch (e) { await t.rollback() next(e) } } )
const EasyYandexS3 = require('easy-yandex-s3').default require('dotenv').config() const s3 = new EasyYandexS3({ auth: { accessKeyId: process.env.ACCESS_KEY_ID, secretAccessKey: process.env.SECRET_ACCESS_KEY }, Bucket: 'nirvana-tracks', debug: false }) async function upload({ track, cover }) { try { let mp3 = await s3.Upload( { buffer: track }, '/mp3/' ) let img = await s3.Upload( { buffer: cover }, '/img/' ) if (mp3 && track) { return { img: img.Location, mp3: mp3.Location } } else { throw new Error('Couldn`t upload') } } catch (e) { console.error(e) throw e } } module.exports = { upload }
Чтобы быть более независимым от библиотеки RadioBrowser как от стороннего API, я решил один раз сделать запрос на все радиостанции библиотеки и наполнить данными свою БД как seeders. Тут же проверял, что ссылка на обложку валидна.
const RadioBrowser = require('radio-browser') const { v4: uuidv4 } = require('uuid') const checkImage = require('../../web/utils/checkImage') module.exports = { async up(queryInterface, Sequelize) { const radios = await RadioBrowser.getStations({ limit: 100 }) const radiosWithUsefullFields = await Promise.all( radios.map(async el => { return { id: uuidv4(), name: el.name, url: el.url, votes: el.votes, country: el.country, favicon: (await checkImage(el.favicon)) ? el.favicon : '', tags: el.tags, lastcheckoktime: el.lastcheckoktime } }) ) await queryInterface.bulkInsert('Radios', radiosWithUsefullFields, {}) }, async down(queryInterface, Sequelize) { await queryInterface.bulkDelete('Radios', null, {}) } }
async function checkImage(url) { return fetch(url) .then(response => { if (response.ok) { return true } else { return false } }) .catch(error => { return false }) } module.exports = checkImage
Опишу реализацию кастомного плеера. По сути, с помощью useRef – взял ссылку на audio элемент и получил доступ к Web Audio API. Дальше связал данные из Audio API с состоянием React, чтобы вызывать рендеры, таким подходом начал расширять функционал, переключение треков, drag & drop перемотку, регулировку звука, паузу и т.д.
return ( <> <audio src={currentTrack?.url} ref={audioElem as LegacyRef<HTMLAudioElement>} onTimeUpdate={() => { onPlaying({ audioElem: audioElem as MutableRefObject<HTMLAudioElement>, setCurrentTrack, currentTrack }) }} />
Пример реализации кастомного хука, который автоматически запускает трек при выборе или переключении трека.
import { useLayoutEffect } from 'react' import { usePlayOnMountArgs } from './types' export function usePlayOnMount({ tracks, setCurrentTrack, position, audioElem, setIsPlaying }: usePlayOnMountArgs) { useLayoutEffect(() => { setCurrentTrack(tracks[position]) const timeoutId = setTimeout(() => { audioElem?.current?.play() setIsPlaying(true) }, 500) return () => { clearTimeout(timeoutId) } }, [tracks, position]) }
Решил чуть-чуть дополнить макет, чтобы пользователь чувствовал погружение в UX при прослушивании, и название приложения (Nirvana) оправдывало себя.
Для этого сделал размытую дымку на фоне плеера в цветах обложки трека, которая переливается. А если обложки по каким-то причинам нет, то дымка в цветах приложения. Такой эффект реализовал так:
<div className={styles.playerBg} style={{ backgroundImage: currentTrack.img && `url(${currentTrack.img})` }} ></div>
@import '../../constants/colors.scss'; .playerBg { z-index: 0; height: 20%; width: 120vw; opacity: 0.85; position: fixed; bottom: -5%; left: -5vw; background-image: linear-gradient( 90deg, rgba(243, 243, 243, 1) 0%, rgba(94, 233, 191, 1) 19%, rgba(47, 105, 255, 1) 54%, rgba(99, 96, 255, 1) 82%, rgba(161, 106, 232, 1) 100% ); filter: blur(30px); background-repeat: no-repeat; background-size: 300% 300%; animation: AnimateBG 100s ease-in-out infinite; } @keyframes AnimateBG { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }

CI/CD
Автотестов для приложения у меня не было. Вместо этого я отправил ссылку на проект всем друзьям и знакомым, чтобы они дали фидбек и помогли отловить баги. Чтобы ускорить этот процесс я решил написать простой CI pipeline с помощью GitHub Actions.
name: onPush run-name: Actions on push on: [push] jobs: init: runs-on: ubuntu-latest strategy: matrix: node-version: [21.x] steps: - uses: actions/checkout@v3 - name: Staring Node.js ${{matrix.node-version}} uses: actions/setup-node@v3 with: node-version: ${{matrix.node-version}} - name: install modules run: npm install - name: install prettier run: npm install --global prettier - name: formatting code run: prettier . -w - name: build project run: npm run build
Деплоить решил на Render, мне понравилось, что там можно легко развернуть БД, бэкенд и клиент, также Render сам будет тянуть изменения из репозитория GitHub.
С какими сложностями я столкнулся
Основная сложность возникла с авторскими правами. Пришлось сделать раздел радио доступным только админу, также загруженные треки добавляются в общую ленту только после подтверждения администратора (пока подтверждать можно только через БД).
Еще было не совсем очевидно, почему иногда при частой смене play и pause браузерное API выкидывало ошибку «The play() request was interrupted by a call to pause()». Но это удалось пофиксить кастомным хуком для debounce и соответствующей функцией из lodash. Ниже мое решение этой проблемы.
import { useEffect } from 'react' import { useDebounceOnPlayPauseArgs } from './types' export function useDebounceOnPlayPause({ audioElem, isPlaying }: useDebounceOnPlayPauseArgs) { useEffect(() => { const timeoutId = setTimeout(() => { if (isPlaying) { audioElem?.current?.play() } else { audioElem?.current?.pause() } }, 500) return () => { clearTimeout(timeoutId) } }, [isPlaying]) }
Плюс достаточно времени ушло на мобильную адаптацию, от части функционала пришлось отказаться, чтобы не перегружать UI. В какой-то момент я решил использовать минимум кнопок и сделать фокус на свайпах. Так, например, ряд треков можно листать свайпами и также взаимодействовать с каруселью. Это я реализовал с помощью react-swipeable. На финальных этапах работы над проектом обращался к друзьям фронтам и бекендерам node js, перед деплоем попросил их поревьюить код и внес правки. Свежий взгляд позволил лучше декомпозировать логику. После деплоя друзья указали мне на баги и дали советы по улучшению UX.
Что с проектом сейчас?
Также проект показывает достаточно хороший performance по метрикам Lighthouse

Исходный код выложен в открытый доступ на GitHub, может посмотреть любой желающий
https://github.com/ABurov30/nirvana-ui
https://github.com/ABurov30/nirvana-client
https://github.com/ABurov30/nirvana-server
UI kit опубликован в npm.
https://www.npmjs.com/package/nirvana-uikit
Делаем выводы
Благодаря этому pet-проекту я побывал одновременно в роли заказчика и исполнителя, попробовал применить продуктовый подход и самостоятельно выстроить процесс разработки приложения с нуля. Научился определять стек технологий, поработал с облаком, Web Audio Api, разработал свою UI библиотеку и попробовал новые технологии.
В будущем я планирую на базе этого проекта пробовать новые технологии, добавляя функционал в приложении. Например, хочу сделать микрофронты с тремя проектами:
-
основное музыкальное приложение Nirvana на React, Vite, Redux
-
главная промо-страница приложения на Next.js.
-
чат внутри приложения на Vue, Socket io, Graph Ql (возможно возьму какой-нибудь неочевидный стейт менеджер типа jotai или zustand).
Ну и прикручивать автоматическую валидацию авторских прав, потому что проверять руками загруженный пользователями контент достаточно сложно, хочется автоматизировать этот процесс.
ссылка на оригинал статьи https://habr.com/ru/articles/803673/
Добавить комментарий