Прошел GTA – запили плеер!
Глава семнадцатая, в которой фронтэнд-разработчики Uprock переходят от GTA к альтернативным путям применения YouTube и практическому применению модели акторов.
Что мы любим в GTA? Возможность избить невинного человека угнать машину, ограбить банк, полетать на вертолете, самолете или реактивном ранце, и, конечно, послушать радио с хорошей музыкой. В общем-то большинство людей не против устроить это и в реальной жизни, но из всего списка легальна только хорошая музыка и то не всегда, по мнению звукозаписывающих компаний.
И да, мы, как настоящие фанаты, пройдя GTA 5 запустили GTA Radio с набором всех радиостанций из всех частей этой замечательной игры. Наслаждайтесь все для вас, а тем кому интересно сделать аналогичный плеер, прошу под кат.
В случае с GTA нужно было увидеть пару спорных моментов с авторским правом и хранением музыки. Да, было бы удобно сделать просто плеер на mp3, но нужно было бы во-первых решать, как сделать быструю загрузку, а во-вторых — что делать, если правообладатели захотят написать нам. На помощь пришел YouTube: он дает и хранение музыки, и разруливает все вопросы с авторским правом. Нужно было просто аккуратно спрятать плеер, чтобы он не болтался перед глазами. Естественно, это дало небольшое количество проблем: стало немного больше трафика для конечных пользователей, перестали поддерживаться мобильные устройства, и, к сожалению, safari 7 с его хитрыми настройками энергосбережения.
У плеера YouTube довольно неудобное, но вполне функциональное API: кроме управления громкостью, позицией в треке, стартом-паузой, оказалось, что есть еще возможность замены текущего воспроизводимого трека через loadVideoByUrl. Вначале мы просто пересоздавали инстанс под каждый новый трек, но от этого ощутимо текла память, и только потом сообразили проверить документацию и найти способ не перезапускать флеш под каждый новый трек.
В интернете уже не одна сотня руководств о том, как использовать плеер youTube, так что на самом деле нет смысла пересказывать, насколько различается подход их плеера в отношении событий и нативных событий браузера, как обрабатывать начало и конец трека, как ловить, когда плеер реально готов и может играть и так далее – это все уже сто раз обсуждалось людьми, которые использовали YouTube для создания собственных видеосайтов.
Важно другое: что мало кто использовал его для аудиоплеера. Хотя возможность-то действительно хорошая: у YT действительно быстрая подгрузка (спасибо за CDN по всему миру), неплохие вроде бы кодеки, отличное сжатие (если вы пускаете не видео, а статичное изображение — песня умещается в несколько МБ без серьезных потерь в качестве), а еще он дает возможность снять кучу вопросов от правообладателей точнее, переложить их на гугл.
Мы в Uprock не любим делать простой бэкэнд, если и делать его, то что-то глобальное, солидное, весомое, хайлоад там, или интеграция с реальным миром, а просто хранить данные в действительности куда проще в JSON. Поэтому – да, там есть админ-панель, но она редактирует JSON, который подключается прямо через browserify для более быстрой загрузки страницы, так что формально — бэкэнда на нем нет. Зато есть nginx, который на любую несуществующую страницу отдает index.html, в котором и начинается магия.
На фронтэнде у нас часто царит честный Stateless, вот и сейчас: абсолютно все на сайте разруливается через рутинг (спасибо page.js). Почему это круто? Да потому что вместо того, чтобы отлавливать события, разработчик просто проставляет обычные ссылки, например переход по этой:
<a href="/GTA-San-Andreas">GTA San Andreas</a> Включит случайное радио из GTA San Andreas, а заодно сменит фон. Хотя на самом деле, нет. Единственное, что сделает переход по этой ссылке — это выцепит названия всех станций в списке радио для GTA San Andreas, а потом перекинет пользователя на рандомную странцию, например, Radio X Вы думаете, там начнет играть музыка? Нет, не угадали. Контроллер для радио тоже просто делает редирект, уже на, скажем, Faith No More и уже тогда и фон сменится, и музыка начнет играть.
Зачем это нужно? Во-первых, это хорошо изолирует код. Часть кода, которая занимается рандомным выбором станции и трека, не взаимодействует с частью кода, связанной с рендером. Они не могут влиять друг на друга, каждая часть занимается своим делом. Во-вторых, это делает код более логичным. Если вырезать некоторые детали, связанные с рендером, оповещениями, запоминанием последнего игравшего трека и так далее — весь код выглядит так (переход между страницами изменен на document.location.hash для читаемости):
page('/:game/:station/:track', function (ctx, next) { var track = trackDetector(ctx.params); if (ctx.path != getTrackUrl(track)) return next(); //защита от несуществующего трека player.onComplete = function () { document.location.hash = getTrackUrl(track.station.getNextTrack()); }; player.play(track); });
'/:game/:station/:track' .split('/') .map(function (record, id, array) { return array.slice(0, id + 1).join('/'); }) .reverse() .forEach(function (path) { page(path, function(){ document.location.hash = getTrackUrl(trackDetector(ctx.params)); }); }); Если кому-то непонятна последняя часть — она может быть переписана как
page('/:game/:station/:track', function(){ document.location.hash = getTrackUrl(trackDetector(ctx.params)); }); page('/:game/:station', function(){ document.location.hash = getTrackUrl(trackDetector(ctx.params)); }); page('/:game', function(){ document.location.hash = getTrackUrl(trackDetector(ctx.params)); }); page('/', function(){ document.location.hash = getTrackUrl(trackDetector(ctx.params)); }); В итоге, мы получаем универсальный логический сценарий: если подходящий трек не найден, мы пытаемся спускаться ниже до станции, до игры, потом до корня, и если подходящего трека не существует, у нас делается переход на любой рандомный подходящий условиям трек.
В действительности то, что тут происходит, называется моделью акторов.
Зачем это реально нужно? Это дает нам возможность полагаться на то, что нужный трек найдется, и делать переход на /GTA-San-Andreas/Radio-X, зная, что то, что трек не будет найден, но ошибка будет перехвачена, и нам найдется какой-нибудь подходящий трек из Radio X.
Как бы это выглядело в обычном коде? Ну, как-то так:
page('/:game', function(ctx){ if (glob.games[ctx.params.game]) { document.location.href = '/'+ctx.params.game+'/'+glob.games[ctx.params.game].stations.getRandom(); } else { document.location.href = '/'+glob.games.getRandom()+'/'; } }) Скажите честно, вам не кажется, что это как-то раздуто и неэффективно?
А теперь давайте немного вдадимся в теорию. Модель акторов придумали в семидесятые для максимальной эффективности парралельных вычислений. Тогда компьютеры были не только большими, но и соединенными в большие сети, раскинутые по стране. Об этом времени есть замечательный, сравнительно короткий рассказ Станислава Лема «137 секунд». Там очень ясно дается понимание того, насколько слабы были отдельные машины, и насколько люди хотели верить в то, что объединив их все в одну большую сеть, они получат что-то невероятное, например, предсказание будущего.
Смысл очень простой: есть автономные сущности, которые могут работать или не работать. Они реагируют на входящие соединения и могут обмениваться сообщениями с теми другими сущностями, о которых им известно.
Нельзя сказать, что это была плохая идея, автономный парралелелизм переживает очередное рождение каждый раз, когда появляется очередная технология для параллельных вычислений.
На акторах строили текстовые квесты: пользовательский фокус перекидывался между акторами, каждый актор соответствовал определенной локации. Все они существовали одновременно, а в Unreal Engine любая полноценная сущность, будь то npc или стена — это актор, автономный элемент.
Но текстовые квесты это на самом деле это частный случай так называемого абстрактного автомата. Акторы же используются в огромном количестве задач, начиная от моделирования поведения групп и заканчивая веб-сервером: каждый из акторов соответствует своему ресурсу, и просто обрабатывает входящие сообщения, в случае необходимости передавая их дальше.
Но чем актор реально отличается от просто еще одного парралельного процесса в системе? Во-первых, он не обязательно должен быть в системе и не обязательно должен быть парралельным: акторы просто обмениваются сообщениями. Находятся ли они на одной машине или нет, обрабатываются ли они последовательно или парралельно – неважно. Более того, это обязательное требование к модели акторов: разработчик не должен ожидать, что сообщения будут приходить поочередно. Допустима (но не обязательна) только реализация, которая гарантирует что те сообщения, которые актор 1 отправляет актору 2, придут в том же порядке, что и были отправлены.
Итак, мы подобрались к самому интересному: зачем это нужно.
Представьте себе, вы фронтэндщик, и вы наконец-то начали чувствовать, что ваше «большое» приложение больше не может оставаться цельным: вы замучались создавать директивы или виджеты, устали упорядочивать структуру всего приложения и причесывать его. Вы наконец-то начали чувствовать, что в вашем приложении должно быть микроядро, автономные компоненты и pubsub модель обмена данными. Что может дать вам еще и то, что вы знаете об акторах?
Представьте, что каждый из контроллеров роутера не имеет прямого доступа к внешним данным. Вообще не имеет: вы можете инициализировать его с каким-то стандартным набором данных, и указать, что в случае необходимости он может делать запросы к каким-то еще акторам (например, БД).
Почему это важно?
Потому что теперь контроллер не может влиять на состояние приложения. Это автономная сущность, поведение которой жестко забито, которую можно гарантированно покрыть тестами, и на которую не может повлиять приложение.
Добро пожаловать в функциональный JavaScript.
Нет, это не хаскелл и не скала: у актора может быть внутреннее состояние. Но нужно ли вам оно в рутинге, например? В большинстве случаев нет: контроллеры по умолчанию должны быть stateless.
Но эта та самая функциональщина, которой так боятся многие, пытаясь построить сложный ООП в javaScript, придя в него из java, например. В чужой монастырь с своим самоваром не ходят, но многие пытаются, да.
Но модель акторов не обязательно полноценно реализовывать (хотя зачастую стоит).
Да, мы можем написать что-то вроде
page.on('*', function(ctx){WebActors.send(WebActors.ANY, ctx.params)}); WebActors.spawnWorker(function(){ WebActors.receive(['GTA-san-andreas'], function(){ //send(...); }) }); Но нужно ли вам оно? Да, оно выглядит круто, воспринимается, пожалуй, еще круче, но в действительности, оно зачастую не очень удобно. Гораздо проще зачастую просто держать в памяти то, что каждый слушатель-коллбэк – это воркер, который обладает каким-то набором данных (объекты, функции, константы), которые забиты в него при инициализации, и внутреннее состояние. И максимум, что он умеет — это работать с одним-единственным интерфейсом: рендер, аудио, канвас, сущность в WebGL. Все остальное он может сделать только обмениваясь «сообщениями» с другими акторами и медиатором — актором, который разруливает все остальные акторы.
Вообще, если говорить формально — веб-компоненты это те же самые акторы: автономные сущности, которые можно дергать за методы и атрибуты. Не правда ли, что-то напоминает?
Фронтэнд медленно движется к тому, чтобы стать все более автономным. Если вы фронтэнд-разработчик, вам совершенно необязательно использовать модель акторов, но вам стоит хотя бы попробовать и почувствовать ее.
Есть старая фраза о том, что любой разработчик должен попробовать ФП, ООП, forth и так далее. Сейчас к этому списку я бы прибавил еще и модель акторов, особенно если вы веб-девелопер.