Продуктовый подход к pet-проекту или как я разработал музыкальное веб-приложение

от автора

Что будет, если соединить любовь к музыке и к разработке? В моем случае получился 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *