Русский
Русский
English
Статистика
Реклама

Javascript

Перевод CSS, JavaScript и блокировка парсинга веб-страниц

12.06.2021 14:07:17 | Автор: admin
Недавно мне попался материал, посвящённый проблеме загрузки CSS-файлов, которая замедляет обработку материалов страниц. Я читал ту статью, стремясь научиться чему-то новому, но мне показалось, что то, о чём там говорилось, не вполне соответствует истине. Поэтому я провёл собственное исследование этой темы и поэкспериментировал с загрузкой CSS и JavaScript.



Может ли загрузка CSS-ресурсов блокировать парсинг страницы?


Прежде всего скажу, что на вопрос из заголовка этого раздела можно, без всякого сомнения, дать положительный ответ. Загрузка CSS-файлов способна не только заблокировать парсинг HTML-кода, но и не дать выполняться JavaScript-коду.

Для начала предлагаю поэкспериментировать. Для этого нам понадобится соответствующим образом настроить браузер. CSS-файл мы будем загружать с CDN, поэтому ограничим скорость работы с сетью в браузере Google Chrome. Для этого, на вкладке инструментов разработчика Performance, поменяем значение параметра Network на Slow 3G. Исследовать будем следующую страницу:

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><link href="http://personeltest.ru/aways/cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet"><script>document.addEventListener('DOMContentLoaded', () => {console.log('DOMContentLoaded');})</script><script>console.log('script');Promise.resolve(1).then(res => {console.log('then');});</script></head><body><h1>hello</h1></body></html>

CSS-файл мы загружаем с CDN, при этом, так как скорость подключения к интернету искусственно ограничена, загрузка стилей займёт некоторое время. В результате до загрузки CSS-файла ничего не попадёт в консоль JavaScript и на экран не будет выведено содержимое страницы. То, что мы видим, указывает на то, что загрузка CSS блокирует загрузку и обработку других материалов страницы.


Вывод данных в JS-консоль

Может ли загрузка и выполнение JS-кода блокировать парсинг страницы?


Загрузка и обработка JS-файлов, безусловно, блокирует парсинг страницы. Но, чтобы исправить эту проблему, при подключении скриптов к странице можно пользоваться атрибутами defer и async тега <script>. Сейчас мы изучим их воздействие на загрузку страницы.

Обычные загрузка и выполнение скрипта


Если в теге <script> не используются атрибуты async или defer процесс загрузки и обработки материалов страницы происходит так, как показано на следующей схеме. Загрузка JS-файлов и выполнение содержащегося в них кода блокирует парсинг HTML-кода.


Использование тега <script> без атрибутов async и defer

Здесь и далее мы будем пользоваться следующими цветовыми обозначениями.


HTML parsing Парсинг HTML; HTML parsing paused Парсинг HTML приостановлен; Script download Загрузка скрипта; Script execution Выполнение скрипта

Использование тега <script> с атрибутом async


Когда браузер обрабатывает тег <script> с атрибутом async, загрузка JavaScript-кода осуществляется в асинхронном режиме. Код скрипта выполняется сразу после загрузки. При этом выполнение JS-кода блокирует парсинг HTML.


Использование тега <script> с атрибутом async

Использование тега <script> с атрибутом defer


Если в теге <script> имеется атрибут defer код скрипта загружается асинхронно. При этом код, после завершения его загрузки, выполняется только тогда, когда будет завершён парсинг HTML-кода.


Использование тега <script> с атрибутом defer

Эксперименты


Давайте поэкспериментируем с атрибутами async и defer. Начнём со следующей страницы:

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DomContentLoaded</title></head><body><script src="http://personeltest.ru/away/code.jquery.com/jquery-1.4.4.min.js"></script><script src="./index.js"/> // 0<script src="./index2.js"/> // 2<script >console.log('inline');Promise.resolve().then(res=>{console.log('then');})</script><div id="hello">hello world</div><script>document.addEventListener('DOMContentLoaded', () => {console.log('DOMContentLoaded');})</script></body></html>

Эта страница, помимо загрузки скрипта jquery-1.4.4.min.js с CDN, загружает пару собственных скриптов index.js и index2.js. Ниже приведён их код.

Файл index.js:

Promise.resolve().then((res) => {console.log('index1');return res;});

Файл index2.js:

Promise.resolve().then((res) => {console.log('index2');return res;});

В ходе загрузки этой страницы в JS-консоль попадает то, что показано ниже.


Вывод данных в JS-консоль

В результате у нас есть доказательство того, что загрузка и обработка JS-файлов блокирует рендеринг HTML-кода. Сообщения, выводимые скриптами, появляются в консоли до сообщения, указывающего на завершение загрузки содержимого DOM.

Теперь посмотрим на то, как ведут себя скрипты, в тегах <script> которых используется атрибут <async>:

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta data-fr-http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DomContentLoaded</title></head><body><script async src="http://personeltest.ru/away/code.jquery.com/jquery-1.4.4.min.js"></script><script src="./index.js"></script><script src="./index2.js"/></script><script>console.log('inline');Promise.resolve().then(res=>{console.log('then');})</script><div id="hello">hello world</div><script>document.addEventListener('DOMContentLoaded', () => {console.log('DOMContentLoaded');})</script></body></html>

Изучим то, что попадёт в консоль.


Вывод данных в JS-консоль

Скрипт библиотеки jQuery загружается асинхронно. То, что попадает в консоль, выводится там до его загрузки. Если скрипт библиотеки загружается слишком медленно это не помешает парсингу HTML-кода. Сообщение DOMContentLoaded может быть выведено и до, и после завершения загрузки и выполнения async-скрипта. А при применении атрибута defer скрипт будет загружен асинхронно, дождётся завершения обработки материалов документа, а потом, но до события DOMContentLoaded, будет выполнен.

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

Сталкивались ли вы с проблемами, связанными с блокировкой обработки материалов веб-страниц?


Подробнее..

Перевод Юмористичный обзор Rust с перспективы JavaScript

16.06.2021 20:20:54 | Автор: admin

В этой статье я в несколько забавном ключе документирую кое-какие размышления о своем знакомстве с Rust с позиции прожженного энтузиаста JavaScript. Здесь вас ждет импровизированная прогулка по феодам Вестероса, встреча с Ланнистерами и даже замаскированный под остров корабль занятные аналогии, которые можно провести с работой в этом языке.

Я по достоинству оценил Rust в качестве средства для написания небольших инструментов. Мой повседневный рабочий процесс под завязку заполнен JavaScript, а так как Rust во многом его напоминает, то и знакомство с ним стало само собой разумеющимся.

Однако проделывание серьезной работы в Rust требует существенного переосмысления процесса построения кода. Компилятор здесь верен своему призванию и беспощаден. Но, скажу честно, это даже доставляет некоторое удовольствие оттачивать код до состояния, когда он, наконец-таки, компилируется.

Хорошие новости


Современный Rust оказывается весьма схож с JavaScript. Переменные объявляются через let, функции выглядят очень похоже, типы уже не чужды, так как мы привыкли к TypeScript, присутствуют async/await, да и в общем формируется весьма знакомое ощущение.

Плохие новости


К сожалению, перечень хорошего завершается быстро. Суть проблемы состоит не в синтаксисе, а в способе, которым Rust рассматривает содержимое вашей программы. В высокоуровневых языках вы получаете удобные абстракции, ограждающие вас от внутренней подоплеки компьютерных процессов. И в этом есть отчетливый смысл если вам требуется добираться на работу, то достаточно уметь водить машину не нужно вдаваться в тонкости устройства и действия ее двигателя. В противоположность этому, низкоуровневые языки загружают вас деталями внутренних процессов, и для того, чтобы добраться до магазина за продуктами, вам уже нужно быть автомехаником.

У каждого из этих подходов есть и достоинства, и недостатки, поэтому они и используются, как правило, каждый для своей области задач. Rust же расположился ровно посередине. Он дает вам доступ ко всем внутренним процессам, попутно предоставляя прозрачность и простоту использования высокоуровневых абстракций. Но всегда есть это но вам, как разработчику, необходимо за это платить. В данном случае мы платим необходимостью научиться по-новому рассматривать построение программы.

Управлению памятью быть!


Компьютерная программа опирается на запись значений в память. Без этого никак не обойтись, также как на кухне без ингредиентов. Следовательно, нам необходимо неким образом добыть эти ингредиенты, нарезать их в нужном количестве, закинуть в сковороду, не перепутав последовательность, и по готовности не забыть прибраться на кухне.

Высокоуровневые языки подобны заботливым родителям. Они все за вас терпеливо уберут, чтобы вы могли творить свое искусство, не пачкая руки. Они благоразумно предоставляют вам в помощь личного Санчо Пансу любезно именуемого Сборщиком мусора поддерживающего вас в те трудные моменты, когда вы отчаянно противостоите агрессивным средам тестирования.

Когда речь заходит об управлении памятью, то Rust как бы говорит: Ничего не знаю реальные шеф-повара сами за собой убирают. И на то есть хорошая причина, потому что сборщик мусора несет в себе собственный набор неочевидных проблем, которые могут навредить в самый неожиданный момент. Хотя в то же время, обогащенный опытом других языков, Rust признает, что заставлять программиста управлять памятью столь же разумно, сколь поручить Дугласу Адамсу написать Звездолет Титаник.

И для того, чтобы найти компромисс между удобством для человека и слишком сложным кодом, Rust предложил новую схему, которую можно образно обобщить как Теперь мы все Ланнистеры.



Как розу ты ни назови, а запах ее столь же сладок


верно сказал Шекспир. Хотя, как быть с не столь ароматными запашками? Сохраняется ли верность его слов в обратном случае? Учитывая многовековую традицию использования людьми эвфемизмов, можно с уверенностью заключить, что нет.


Корабль длиной 55 метров, замаскированный в Тихом океане под вид маленького острова для избежания обнаружения во время Второй мировой войны. Сработало

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

На землях Вестероса (смею я сказать ВестеRust?) есть крохотные, небольшие, а также крупные феоды. Загвоздка в том, что все они оккупированы Ланнистерами. Внутренне феоды занимаются своими собственными делами, а когда им требуются товары извне, то они берут на себя долг, чтобы эти товары получить. В последствии долг необходимо возвращать Богам Вестероса. Rust подобен королеве драконов этого мира он узрит свысока все нюансы и проследит, чтобы все долги перед Богами были уплачены.

В нем каждая область представляет свой собственный феод. Переменные (память), которые получаются из системы и используются в этой области, остаются как секреты в Вегасе только внутри области. Если область свою миссию выполнила и цели больше не имеет, то память возвращается системе. Компилятор гарантирует это, поскольку тайком внедряет в создаваемые вами феоды соответствующий код так что избежать своей участи областям не удастся. Это железное правило Rust (восхвалим уровень иронии).

Может ли возникнуть кризис?


Посмотрим, как это работает.



Здесь у нас две области: внешняя main и внутренняя, будем звать ее inner scope, для демонстрации. В этом случае владение работает так:

  1. main владеет a и b
  2. a хочет поработать в inner scope, поэтому main передает a во владение inner scope
  3. inner scope делает свои дела с a и завершается
  4. Скрытый код Rust отбрасывает a
  5. main делает свои дела с b и тоже завершается
  6. Rust отбрасывает b

Обратите внимание, что долг, обусловленный владением, возвращается системе, а не области, из которой возник. Владение a не возвращается в область main. Так, подождите звучит очень опасно.

Что, если у нас будет такой код?



Здесь область main хочет снова использовать a, но мы сказали, что Rust уже ее отбросил по завершении inner scope.

Не даст ли программа сбой и не сгорит ли, когда достигнет этой точки выполнения?



Да, так и будет. Но, как спартанцы ответили отцу Александра, королю Филиппу II Македонскому: если она этой точки достигнет.

Абсолютный бюрократ


Компилятор Rust является гордым послушником традиции Легизма, настолько верным, что Хан Фей-цзы с восторгом бы объявил вне закона все остальные языки. Ничто не происходит в землях Rust, пока компилятор не скрепит это действие печатью Утверждаю. Он будет тщательно проверять каждую мелочь, оценивая, насколько безопасен запуск программы, и только при удовлетворении всех требований выдаст-таки исполняемый файл.



В этом случае он понял, что наши навыки работы с долгами не соответствуют требованиям, и запрос на создание бинарника отклонил.

Программист сам должен изучить законы Rust и гарантировать их соблюдение, включая все подоплеки, хитрости и допущения. В противном случае компилятор будет сильно ругаться.

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

Rust RPG


В землях Rust переменные это игроки. Игроки обязаны принадлежать некоторому классу маги, священники, структуры. Более того, каждый игрок может иметь индивидуальное снаряжение. И в этом есть смысл. Ведь у вас может быть два священника один с посохом, а второй с жезлом не так ли?

Помните пример с dbg!()? Это макрос, представляющий грубый эквивалент console.log из JS. Давайте создадим собственную типизированную переменную и выведем ее в консоль.



Мы создали struct, которая, по сути, является типом. Затем мы создали объект этого типа. В завершении мы запросили вывод созданного объекта.



Ну дела. Наш игрок до такой степени нуб, что даже не может предоставить отладочную информацию. Правда! Просто удивительно

Ключевой момент здесь в том, что ваши рукотворные переменные все начинают в пешках без всякого снаряжения. А вот где вступает в игру и само снаряжение (на языке Rust называемое типажи (Trait)).

Нажмем F.



На этот раз работает. Единственное отличие в появившейся сверху строке. Здесь мы снаряжаем Noob типажом Debug. Теперь наш игрок готов к выходу в консоль какое достижение!

В Rust представлена уйма всяческого снаряжения разной степени распространенности. При этом вполне ожидаемо, что на основе собственного дизайна с его помощью можно создавать новые вариации.

Некоторые же типажи могут генерироваться компилятором автоматически. Именно это здесь и произошло компилятор смог избавить нас от тривиальной работы. В остальных случаях фактическая реализация ложится на ваши плечи. Хотите для мага графитовую броню? Не проблема сделаем но с вас код, который опишет принцип ее действия.

Типажи являются глубинным принципом работы фабрики Rust. Вернемся еще раз к примеру с владением. Если внимательно прочесть сообщение об ошибке, то мы заметим в нем компилятор объясняет, что владение переменной пришлось переместить, потому что String не реализует типаж Copy. В противном случае компилятор не стал бы ее перемещать, а сделал копию.

Типаж Copy означает, что вы берете раздел памяти и memcpy его в другое место, работая непосредственно с байтами. Хорошо, значит String не имеет типажа Copy, нужно ли нам просто сообщить компилятору, чтобы он его присвоил? К сожалению, нет. В целях безопасного использования Copy является слишком низкоуровневой для применения к String.

Конечно же, Rust бы не был полезен, если бы история на этом завершалась. Есть более явный типаж, который проделывает практически то же самое Clone. String обладает типажом Clone, значит нам просто нужно использовать его вместо Copy.

В качестве примера немного подправим код:



Здесь компилятор видит, что нам нужно использовать a внутри inner scope, но теперь он также видит, что мы научились все делать правильно, задействовав вместо фактической a ее клона. Итак, получается следующее:

  1. a принадлежит main
  2. Создается a.clone и одалживается в inner scope
  3. inner scope делает свои дела и завершается
  4. Rust отбрасывает a.clone
  5. main без проблем использует a, потому что a всегда оставалась в ее владении

Красота. Естественно, это не единственный способ решить конкретно данную проблему, но он четко вписывается в наше небольшое исследование принципа владения и типажей.

Прежде чем завершать пост, считаю необходимым вкратце проговорить еще кое-что. Мы рассмотрели владение и то, как оно реализуется в области, но правда в том, что это было всего лишь упрощение. Для отслеживания владения Rust задействует принцип времени жизни (Lifetimes). Просто, получается, что чаще всего времена жизни и области совпадают. Хотя иногда компилятору все же требуется помощь. Следовательно, мы можем работать напрямую с временами жизни, а в некоторых случаях даже обязаны.

Конец?


Однозначно нет! Если сравнить Rust с айсбергом, то это даже не его вершина. Если сравнить его с тортом, то это даже не его глазировка. Если же представить Rust как криптовалюту, то это даже не сатоши. Но сегодня я явно не фонтанирую вдохновенными метафорами, так что давайте на этом закончим.

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


Подробнее..

Как мы интрегрировали Agora SDK в проект

11.06.2021 18:10:18 | Автор: admin

Всем привет. Меня зовут Дмитрий, и я типичный представитель касты гребцов на галере X. Основной ЯП, который я использую - PHP, но иногда приходится писать на других.

Предыстория

Как-то пришел очередной, немного не доделанный, проект "убийца" продукта Y. Все бы ничего, если бы его не делали изначально индусы. Первоначальный анализ кода и все оценкой фич занимался другой разработчик. В итоге было решено взять наследие индусов, немного подправить и в прод.

Немного подправить и в продакшн, растянулось где-то на пол года. Все уже к этому времени поняли что лучше бы выбрали вариант - переписать заново, но было уже поздно, а отступать назад не хотелось. В итоге проект выехал live, но был чертовски страшный и неудобный со стороны UI/UX. Поэтому начался плавный редизайн всего и вся. Все что описано выше, было до меня, я пришел на проект только через пол года.

Постановка задачи и начальные условия

Новый спринт, новые тикеты. Одна из задач звучит как: "Редизайн текущего механизма видео/аудио звонков". Для данного функционала мы использовали Agora Web SDK 3.4.0v. Почему именно Agora - потому что ее выбрали индусы (скорее всего из-за 10000 бесплатных минут). Возможно еще подкупило то что есть SDK под различные платформы:

Поехали

Первый делом я глянул последнюю версию Web SDK. Как оказалось - уже вышла абсолютно новая мажорная версия Agora Web SDK 4.x. Ну, если мы все равно полностью редизайним - то почему бы и не взять новую версию и использовать ее. Все равно будет полный прогон со стороны QA, в добавок - поменялся сам флоу созвона. Сказано - сделано, только насторожила запись:

Вроде разговор идет только про несовместимость Web SDK (у нас еще используется React Native SDK для мобильных устройств), но осадок остался.

На новый дизайн и сервер ушло где-то 3 - 4 дня (не люблю верстать, но что поделать). Настало время самого интересного - запуск процесса интервью. В итоге была взята Agora Web SDK 4.4.0. В течение следующего дня получилось сделать всю JS часть для созвона по видео и ауди (со всеми плюшками). За основу был взят пример из их же гитхаба: https://github.com/AgoraIO/API-Examples-Web/blob/main/Demo/basicVideoCall/basicVideoCall.js (если что, то в архиве с самой либой лежат похожие примеры интеграции)

Еще через день все было уже на testing энвайронменте. Первые пол дня тестов прошли хорошо, на web портале все более-менее работало. Настало время проверки звонков с разных платформ (один человек с мобильного девайса - второй с сайта).

Первый звоночек

При звонке с мобилы на web, и наоборот - звонок не устанавливается. Проверили на проде тот же кейс - все огонь. Значит что-то пошло не так. Так как код на мобильных девайсах не менялся вообще (на их стороне изначально подумали над дизайном и все продумали) - значит проблема на моей стороне. Первое действие - нужно подписаться на все события от SDK что доступны - https://docs.agora.io/en/Voice/API%20Reference/web_ng/interfaces/iagorartcclient.html и смотреть что, где всплывает. Каково же было мое удивление, когда я увидел пустоту в консоле хромиума. Да это же не может быть, что бы Agora Web SDK 4.4.0 была не совместима с Agora React Native API 3.х!

После многих попыток, хоть как-то это дело завести - пришло "Принятие". Что поделать, придется брать все же Agora Web SDK 3.x.

Новый день начинается со скачивания последней версии (в рамках 3.x) и переделкой существующего функционала под другой SDK. К счастью, дело пошло быстро и за первую половину дня - все было готово. На локальной машине все работало отлично. Решил протестировать с коллегой по офису (он должен открыть ветку с прода и попробовать созвонится). И хоп, мы получаем ту же проблему - звонок не устанавливается, но что обнадеживает - в консоле проскакивают логи, что оппонент по звонку выходит из румы (в терминах агоры - это channel). Ну хоть какой прогресс, по сравнению с 4.x.

После первых двух часов дебага - было решено взять код с прода и попробовать его запустить. К черту текущее решение, просто берем код с прода, вcтавляем в HTML, пробрасываем пару конфигов и запускаем. О чудо, все работает. Все друг друга видят и слышат. Первая хорошая новость за день. Значит минорные версии 3.x совместимы между собой (это вселяет надежду с мобилами). Быстро подчищаем код с HTML и переносим его в JS модули. Запускаем и получаем дырку от бублика. Да что ж за день то сегодня такой. Откатываемся назад на вариант с кодом в HTML - работает. Ладно, теперь это уже личное...

Переносим "строчку за строчкой" из HTML в JS модули и почти каждый раз проверяем локально и с коллегой. Хм... Почему же оно все еще работает? Когда была перенесена последняя строчка, я очень удивился. Код был почти один-в-один как после миграции на 3.x, который я получил пол дня назад и он РАБОТАЛ. А давай-ка я попробую запустить старый свой вариант на 3.x. Оп-па не работает. Истина где-то рядом. Как хорошо что есть гит и можно сравнить. Отбросив различия в кодстайле я был очень удивлен увиденным:

Да не может же быть, что бы я так лоханулся. Быстро открываем документацию по либе (uid используется для подключения к руме). Нас интересует метод join:

Все! Я больше не могу. Поеду я домой, рабочий день уже давно закончен.

Второй звоночек

Новый день - новые силы. Как оказалось - на проде используется number, потому что со стороны мобил (с их слов) было жесткое требование на int и ничего более. Успокаиваемся и работаем... Проверяем локально, потом с коллегой, все хорошо - едем на testing. Проверяем web - хорошо, мобилы - черный экран у того кто остался на вебе, но звук работает отлично. Хоть какой прогресс...

После пары часы дебага и проверки различный гипотез, я был готов сдаться, но здесь произошло чудо. Разраб со стороны мобил говорит что у него все работает. После того, как мы созвонились, через один из мессенджеров, и расшарили экран - я действительно убедился, все условия соблюдены и оно действительно работает. Начали искать отличия. Мы с коллегой в офисе. QA на уделенке дома. Разработчик мобильной версии в Латвии. Просим QA пере-проверить кейс со звонком на разных платформа, только пусть звонит на мне в офис, а кому-то вне офиса. О-да, работает... Мы где-то близко...

Услышав наши "горячие" споры в комнате офиса, глава департамента встает и говорит: "Парни, а вы используете STUN / TURN сервера?". Такие слова я слышал впервые, поэтому пришлось гуглить: https://medium.com/nuances-of-programming/webrtc-%D1%84%D1%80%D0%B5%D0%B9%D0%BC%D0%B2%D0%BE%D1%80%D0%BA-ice-stun-%D0%B8-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%B0-turn-f835b11d9dde

В общем, все сошлись на том что для текущей версии проекта - никто не будет пока подключать STUN / TURN сервера (ибо если бесплатные STUN еще можно найти, то бесплатных TURN нет).

Почему не догадались, что из-за NAT в офисе - ловим проблемы? Да потому что звук работал. И видео на одной стороне работало отлично. А как раз черный экран в видео мы уже получали, когда ловили кейсы, где один клиент инициировал созвон по связке rtc/vp8, а второй live/h264.

Вот так и закончилось мое увлекательное приключение в мир WebRTC.

PS. Если кто-то меня попросит еще раз сделать звонки на проекта - я первым делом пойду искать другую библиотеку.

Подробнее..

История одной интеграции Agora SDK

11.06.2021 20:19:02 | Автор: admin

Всем привет. Меня зовут Дмитрий, и я типичный представитель касты гребцов на галере X. Основной ЯП, который я использую - PHP, но иногда приходится писать на других.

Предыстория

Как-то пришел очередной, немного не доделанный, проект "убийца" продукта Y. Все бы ничего, если бы его не делали изначально индусы. Первоначальный анализ кода и все оценкой фич занимался другой разработчик. В итоге было решено взять наследие индусов, немного подправить и в прод.

Немного подправить и в продакшн, растянулось где-то на пол года. Все уже к этому времени поняли что лучше бы выбрали вариант - переписать заново, но было уже поздно, а отступать назад не хотелось. В итоге проект выехал live, но был чертовски страшный и неудобный со стороны UI/UX. Поэтому начался плавный редизайн всего и вся. Все что описано выше, было до меня, я пришел на проект только через пол года.

Постановка задачи и начальные условия

Новый спринт, новые тикеты. Одна из задач звучит как: "Редизайн текущего механизма видео/аудио звонков". Для данного функционала мы использовали Agora Web SDK 3.4.0v. Почему именно Agora - потому что ее выбрали индусы (скорее всего из-за 10000 бесплатных минут). Возможно еще подкупило то что есть SDK под различные платформы:

Поехали

Первый делом я глянул последнюю версию Web SDK. Как оказалось - уже вышла абсолютно новая мажорная версия Agora Web SDK 4.x. Ну, если мы все равно полностью редизайним - то почему бы и не взять новую версию и использовать ее. Все равно будет полный прогон со стороны QA, в добавок - поменялся сам флоу созвона. Сказано - сделано, только насторожила запись:

Вроде разговор идет только про несовместимость Web SDK (у нас еще используется React Native SDK для мобильных устройств), но осадок остался.

На новый дизайн и сервер ушло где-то 3 - 4 дня (не люблю верстать, но что поделать). Настало время самого интересного - запуск процесса интервью. В итоге была взята Agora Web SDK 4.4.0. В течение следующего дня получилось сделать всю JS часть для созвона по видео и ауди (со всеми плюшками). За основу был взят пример из их же гитхаба: https://github.com/AgoraIO/API-Examples-Web/blob/main/Demo/basicVideoCall/basicVideoCall.js (если что, то в архиве с самой либой лежат похожие примеры интеграции)

Еще через день все было уже на testing энвайронменте. Первые пол дня тестов прошли хорошо, на web портале все более-менее работало. Настало время проверки звонков с разных платформ (один человек с мобильного девайса - второй с сайта).

Первый звоночек

При звонке с мобилы на web, и наоборот - звонок не устанавливается. Проверили на проде тот же кейс - все огонь. Значит что-то пошло не так. Так как код на мобильных девайсах не менялся вообще (на их стороне изначально подумали над дизайном и все продумали) - значит проблема на моей стороне. Первое действие - нужно подписаться на все события от SDK что доступны - https://docs.agora.io/en/Voice/API%20Reference/web_ng/interfaces/iagorartcclient.html и смотреть что, где всплывает. Каково же было мое удивление, когда я увидел пустоту в консоле хромиума. Да это же не может быть, что бы Agora Web SDK 4.4.0 была не совместима с Agora React Native API 3.х!

После многих попыток, хоть как-то это дело завести - пришло "Принятие". Что поделать, придется брать все же Agora Web SDK 3.x.

Новый день начинается со скачивания последней версии (в рамках 3.x) и переделкой существующего функционала под другой SDK. К счастью, дело пошло быстро и за первую половину дня - все было готово. На локальной машине все работало отлично. Решил протестировать с коллегой по офису (он должен открыть ветку с прода и попробовать созвонится). И хоп, мы получаем ту же проблему - звонок не устанавливается, но что обнадеживает - в консоле проскакивают логи, что оппонент по звонку выходит из румы (в терминах агоры - это channel). Ну хоть какой прогресс, по сравнению с 4.x.

После первых двух часов дебага - было решено взять код с прода и попробовать его запустить. К черту текущее решение, просто берем код с прода, вcтавляем в HTML, пробрасываем пару конфигов и запускаем. О чудо, все работает. Все друг друга видят и слышат. Первая хорошая новость за день. Значит минорные версии 3.x совместимы между собой (это вселяет надежду с мобилами). Быстро подчищаем код с HTML и переносим его в JS модули. Запускаем и получаем дырку от бублика. Да что ж за день то сегодня такой. Откатываемся назад на вариант с кодом в HTML - работает. Ладно, теперь это уже личное...

Переносим "строчку за строчкой" из HTML в JS модули и почти каждый раз проверяем локально и с коллегой. Хм... Почему же оно все еще работает? Когда была перенесена последняя строчка, я очень удивился. Код был почти один-в-один как после миграции на 3.x, который я получил пол дня назад и он РАБОТАЛ. А давай-ка я попробую запустить старый свой вариант на 3.x. Оп-па не работает. Истина где-то рядом. Как хорошо что есть гит и можно сравнить. Отбросив различия в кодстайле я был очень удивлен увиденным:

Да не может же быть, что бы я так лоханулся. Быстро открываем документацию по либе (uid используется для подключения к руме). Нас интересует метод join:

Все! Я больше не могу. Поеду я домой, рабочий день уже давно закончен.

Второй звоночек

Новый день - новые силы. Как оказалось - на проде используется number, потому что со стороны мобил (с их слов) было жесткое требование на int и ничего более. Успокаиваемся и работаем... Проверяем локально, потом с коллегой, все хорошо - едем на testing. Проверяем web - хорошо, мобилы - черный экран у того кто остался на вебе, но звук работает отлично. Хоть какой прогресс...

После пары часы дебага и проверки различный гипотез, я был готов сдаться, но здесь произошло чудо. Разраб со стороны мобил говорит что у него все работает. После того, как мы созвонились, через один из мессенджеров, и расшарили экран - я действительно убедился, все условия соблюдены и оно действительно работает. Начали искать отличия. Мы с коллегой в офисе. QA на уделенке дома. Разработчик мобильной версии в Латвии. Просим QA пере-проверить кейс со звонком на разных платформа, только пусть звонит на мне в офис, а кому-то вне офиса. О-да, работает... Мы где-то близко...

Услышав наши "горячие" споры в комнате офиса, глава департамента встает и говорит: "Парни, а вы используете STUN / TURN сервера?". Такие слова я слышал впервые, поэтому пришлось гуглить: https://medium.com/nuances-of-programming/webrtc-%D1%84%D1%80%D0%B5%D0%B9%D0%BC%D0%B2%D0%BE%D1%80%D0%BA-ice-stun-%D0%B8-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%B0-turn-f835b11d9dde

В общем, все сошлись на том что для текущей версии проекта - никто не будет пока подключать STUN / TURN сервера (ибо если бесплатные STUN еще можно найти, то бесплатных TURN нет).

Почему не догадались, что из-за NAT в офисе - ловим проблемы? Да потому что звук работал. И видео на одной стороне работало отлично. А как раз черный экран в видео мы уже получали, когда ловили кейсы, где один клиент инициировал созвон по связке rtc/vp8, а второй live/h264.

Вот так и закончилось мое увлекательное приключение в мир WebRTC.

PS. Если кто-то меня попросит еще раз сделать звонки на проекта - я первым делом пойду искать другую библиотеку.

Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 472 (7 13 июня 2021)

14.06.2021 00:15:08 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: Firefox 89, Safari 15 Beta, Jest 27, цикл статей о работе браузера, разработка базовых компонентов, обзорная статья о тестировании фронтенда и анонс WebExtensions Community Group.
podcast Подкаст Веб-стандарты #285: Бета Chrome92, Firefox89, якоря ирасширения, TeamCity, JSвнутри WASM, TypeScript4.3
podcast Подкаст Фронтенд Юность #190: Как подступиться к старому проекту и не сесть на кулак
podcast Новости 512 от CSSSR: React 18, Vue 3.1, анонс ESLint 8, курсы от CSSSR, :is(), where() и :has(), как прилёг Интернет
podcast Подкаст Callback Hell: Сервисы Google с плохими Web Vitals, шеринг логики между фронтом и бэком, документация на проектах


Веб-разработка


habr Будущее веба: станет ли рендеринг в <canvas> заменой DOM?
en Правильный тег для работы: почему следует использовать семантический HTML
en 5 проблем фронтенда, которые нельзя игнорировать





CSS


habr Выкладка нетрадиционной ориентации
en Полное руководство по CSS Grid с шпаргалкой
en Системные цвета CSS
en CSS определяет значения цвета, соответствующие системным настройкам.
en Media Queries во времена @container
en Давайте узнаем об Aspect Ratio в CSS
en CSS size-adjust для @font-face
en Равные столбцы с Flexbox: это сложнее, чем вы думаете
en Эксперимент с сортируемыми мультиколоночными таблицами
en Знакомьтесь с :has: нативный CSS селектор
en Рог изобилия ContainerQueries
en Создание правил для font-size CSS и создание Fluid Type Scale

JavaScript


habr Как я ускорил движок на 13%
habr Прогнозирование временных рядов на JS: анализ данных для самых маленьких фронтендеров
habr Sparkplug неоптимизирующий компилятор JavaScript в подробностях
en Как создать фулстек-приложение с помощью Supabase и Next.js
en Реализация приватных полей в JavaScript
en Forever Functional: Мемоизация промисов
en Как реализовать принципы SOLID в JavaScript
en Автоматизируйте форматирование и исправление JavaScript кода с помощью Prettier и ESLint
en Современный JavaScript
en Выходя за рамки ESLint: обзор статического анализа в JavaScript
en Доберенные типы API для безопасности JavaScript DOM
en Как создать NFT с помощью JavaScript
en Rust с точки зрения JavaScript





Браузеры


habr Vivaldi 4.0 Первое приближение
Google признал неудачным эксперимент с показом только домена в адресной строке Chrome
en Возможности WebKit в Safari, продемонстрированные на WWDC21


Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Webix Datatable. От простой таблицы к сложному приложению

14.06.2021 10:08:37 | Автор: admin

Эта статья будет интересна для тех, кто привык решать сложные задачи простыми методами. Работа с большими данными, на первый взгляд, может показаться сложной задачей. Но если вы владеете специальными инструментами, то организация и отображение больших наборов данных покажется вам не более чем забавным развлечением. Сегодня мы поговорим об одном из самых неординарных инструментов для работы с данными, который предоставляет нам команда Webix. Речь пойдет о таком простом и одновременно сложном виджете библиотеки Webix UI как DataTable. Давайте разбираться в чем его сила.

Библиотека Webix и виджет DataTable

Webix UI это современная и производительная JavaScript библиотека, которая позволяет создавать дружественный интерфейс на основе собственных ui компонентов. Диапазон ее возможностей представлен виджетами различной сложности, начиная обычной кнопкой и заканчивая комплексными виджетами. Помимо самих компонентов, библиотека предоставляет множество дополнительных инструментов для работы с ними. Здесь стоит упомянуть механизм обработки событий, методы для работы с данными, взаимодействие с сервером, темы для стилизации и многое другое. Обо всем этом и не только, вы можете узнать в документации.

Виджет DataTable - это один из самых функциональных компонентов библиотеки Webix. С его помощью вы можете отображать данные в виде таблиц и очень гибко их настраивать. Этот мощный и одновременно простой в использовании инструмент со стильным дизайном поддерживает различные форматы данных (XML, JSON, CSV, JSArray, HTML tables) и довольно быстро работает с большими объемами информации. Секрет его скорости заключается в так называемом "ленивом подходе отрисовке данных". Это не значит, что ему лень отрисовывать данные. Хотя, без сомнений, крупица правды в этом есть. Суть же подхода заключается в том, что даже если вы загрузите в таблицу 1 000 000 рядов, выджет отрисует только выдимые в окне браузера элементы. Стоит также сказать, что среди своих конкурентов виджет удерживает лидирующее место по скорости отрисовки.

Помимо прочего, DataTable обладает широким арсеналом API для фильтрации, сортировки, пагинации, копирования, изменения размеров, стилизации и многих других полезных фишек. Все это делает DataTable незаменимым помощником при работе с большими данными.

Можно много рассуждать об удобстве работы с таблицами Webix и их обширных возможностях, но я предлагаю оставить патетику ораторам и попробовать разобраться во всем на практике. Давайте создадим небольшое приложение, которое будет отображать таблицу данных об аренде автомобилей. На наглядном примере гораздо проще увидеть все преимущества работы с этим мощным инструментом. Успешное применение этого виджета также описано в статье "Создаем Booking приложение с Webix UI".

С готовым приложением можно ознакомиться тут.

Базовые приготовления

Для того чтобы использовать возможности библиотеки Webix в нашем приложении, необходимо подключить ее в главном файле index.html. Здесь стоит упомянуть о том, что существует 2 версии библиотеки: базовая и Pro-версия. Базовая версия бесплатная и предоставляет ограниченный набор возможностей, по сравнению с Pro-версией. Мы же воспользуемся тестовой лицензией расширенной Pro-версии, чтобы по максимуму реализовать возможности виджета DataTable. Необходимые файлы доступны через CDN по следующим ссылкам:

<script type="text/javascript" src="http://personeltest.ru/away/cdn.webix.com/site/webix.js"></script><link rel="stylesheet" type="text/css" href="http://personeltest.ru/away/cdn.webix.com/site/webix.css">

Нам остается только включить их в файл index.html нашего приложения. Теперь он будет так:

<!DOCTYPE html><html>  <head>    <title>Webix Booking</title>    <meta charset="utf-8">    <!--Webix sources -->    <script type="text/javascript" src="http://personeltest.ru/away/cdn.webix.com/site/webix.js"></script>    <link rel="stylesheet" type="text/css" href="http://personeltest.ru/away/cdn.webix.com/site/webix.css">  </head>  <body>    <script type="text/javascript">      //...    </script>  </body></html>

Внутри кода мы добавим теги <script>...</script>, где и будем собирать наше приложение.

Инициализация

Все основные действия будут разворачиваться внутри конструктора webix.ui(). Нам же нужно удостовериться в том, что код начнет выполняться после полной загрузки HTML страницы. Для этого необходимо обернуть конструктор в webix.ready(function(){}). Выглядит это следующим образом:

webix.ready(function(){  webix.ui({    /*код приложения*/  });});

Мы создали базовый index.html файл и подключили необходимые инструменты. Теперь самое время перейти непосредственно к настройке самого компонента DataTable.

Сила в простоте

Если вы минималист и не горите желанием разбираться во всех тонкостях настройки виджета, то для вас предусмотрен самый ленивый набор конфигураций, который позволяет отобразить нужную информацию в виде таблицы. В файле datatable.js нужно создать конструктор виджета и прописать три следующих строчки кода:

const datatable = {  view:"datatable",  autoConfig:true,  url:"./data/data.json"}

Сам компонент DataTable объявляется с помощью выражения view:"datatable". Через свойство url мы задаем путь, по которому виджет загружает данные. Стоит уточнить, что по умолчанию виджет ожидает получить данные в формате JSON. Если данные приходят в другом формате (xml, jsarray или csv), нужно указать его через свойство datatype. В случае, когда данные находятся на клиенте в виде массива, их можно передать компоненту через свойство data или метод parse().

Особое внимание стоит уделить свойству autoConfig, которое предназначено для максимально ленивых программистов. С его помощью данные автоматически распределяются по соответствующим столбцам и рядам таблицы. Удобнее только вообще ничего не делать.

Теперь давайте отобразим компонент в браузере и посмотрим, что же у нас получилось. Для удобства, мы сохраним конструктор виджета в переменную datatable, которую будем использовать для сборки в файле index.html.

С помощью следующего кода мы подключаем файл с компонентом DataTable в файле index.html:

<!--App sources --><script src="js/datatable.js" type="text/javascript" charset="utf-8"></script>

В конструкторе приложения мы указываем переменную, которая хранит настройки виджета:

<script type="text/javascript"> webix.ready(function(){  webix.ui( datatable ); });</script>

В браузере мы увидим следующий результат:

AвтонастройкаAвтонастройка

Вот так, с помощью 3 строчек кода, можно отобразить таблицу с соответствующими данными. При таком раскладе, структура таблицы будет напрямую зависеть от структуры загружаемых данных. На самом деле, автонастройка мало используется в реальных проектах. Гораздо практичнее настраивать столбцы таблицы вручную. В следующем разделе я расскажу вам как это сделать.

Тонкости настроек таблицы

Если автонастройка кажется вам слишком уж тривиальным вариантом, тогда давайте усложним задачу и зададим индивидуальные настройки для каждого столбца в отдельности. Сделать это можно в массиве свойства columns:[ ]. Для каждого столбца нужно задать объект с соответствующими настройками. Стоить учитывать то, что порядок отображения столбцов в таблице напрямую зависит от порядка объектов с настройками в массиве.

В первую очередь, для каждого столбца нужно задать уникальный id. Он должен соответствовать ключу, под которым данные хранятся в объекте (хранилище). Затем можно указать название для шапки столбца с помощью свойства header.

После этого можно задать ширину каждого столбца. Для этого предусмотрены такие свойства как width, minWidth, maxWidth и fillspace. Если мы хотим, чтобы ширина таблицы подстраивалась под ширину контейнера или под доступное ей место, нужно использовать свойство fillspace по крайней мере для одного столбца. Теперь настройки столбцов будут выглядеть так:

{  view:"datatable",  columns:[    { id:"rank", header:"Rank", width:45 },    //...    { id:"vin_code", header:"VIN", minWidth:50, width:180, maxWidth:300 },    //...    { id:"address", header:"Address", minWidth:200, fillspace:true },    //...  ],  url:"./data/data.json"}

В браузере мы получим следующий результат:

Индивидуальные настройки столбцовИндивидуальные настройки столбцов

В этом примере видно, что столбцы размещены в установленном нами порядке и с заданной шириной, а их названия соответствуют значениям свойства header.

Помимо индивидуальных настроек ширины для каждого столбца, вы можете задать глобальные настройки размеров для всех столбцов в конструкторе компонента. Для этого предусмотрено свойство autowidth, которое подстраивает ширину виджета к суммарной ширине всех столбцов таблицы, а также свойство columnWidth, которое задает фиксированную ширину для всех столбцов. По умолчанию стандартная ширина столбца равна 100px.

Для полноты картины давайте предоставим пользователям возможность изменять размеры столбцов. Для этого необходимо задать свойству resizeColumn значение true, а также установить границы для рядов, столбцов и их хедеров с помощью свойства css:"webix_data_border webix_header_border".

Так как у таблицы есть много столбцов, необходимо предусмотреть горизонтальную и вертикальную прокрутку. Сделать это можно с помощью свойства scroll, которое изначально установлено в значении false. Нам же нужно задать ему значение xy.

Теперь мы можем изменять размер столбцов, перетаскивая их правую границу в нужном направлении, а также скролить таблицу по горизонтали и вертикали.

Настройка содержимого ячеек

Работа с шаблонами

По умолчанию, ячейки таблицы заполняются данными, ключ которых задан в качестве id в настройках столбца. Но виджет позволяет нам управлять их отображением. С помощью свойства template мы можем задать необходимый шаблон, по которому данные будут отображаться в ячейке. Значение можно указать как в виде строки, так и в виде функции. Чтобы использовать в строковом шаблоне входящие данные, их ключ нужно указать как #data_key#. У нас есть несколько столбцов, для которых необходимо задать шаблоны.

Начнем мы пожалуй с первого столбца, в ячейках которого будут размещаться иконки в виде звездочек. У вас может возникнуть резонный вопрос, зачем нам такой столбец со звездочками? А мы сделаем так, чтобы наш потенциальный пользователь смог отмечать понравившиеся ему варианты при клике по звездочке. Давайте это реализуем с помощью темплейт-функции. Код будет выглядеть следующим так:

{  view:"datatable",  id:"car_rental_table",  //...  columns:[    { id:"stared", header:"",     template:function(obj){       return `<span class='webix_icon star mdi mdi-"+(obj.star ? "star" : "star-outline") + "'></span>`;     }, ...,    },     //...  ]}

Свойству template мы присваиваем функцию, которая возвращает элемент span с определенными классами. Классы star и star-outline мы будем менять динамически при клике по иконке. Давайте создадим функцию, которая будет менять классы для иконок этого столбца:

function selectStar(id){  const table = $$("car_rental_table");  const item = table.getItem(id);  const star = item.star?0:1;  item.star = star;}

В качестве аргумента функция принимает id выбранного ряда. Через метод $$("car_rental_table") мы получаем доступ к виджету по его id. С помощью метода таблицы getItem(), который принимает id элемента в качестве параметра, мы получаем объект данных ряда. Затем проверяем наличие ключа star и присваиваем ему значение 0 (если он существует) либо 1 (если его нет).

Чтобы переключение начало работать, необходимо установит эту функцию в качестве обработчика на событие клика по иконке со звездочкой. Для того чтобы установить обработчик на любой элемент таблицы с определенным css классом, у таблицы предусмотрено специальное свойство onClick:

//...url:"./data/data.json",onClick:{  "star":(e,id) => selectStar(id)},//...

Теперь мы можем отмечать нужные нам ряды при клике по звездочке в первом столбце таблицы. В браузере это выглядит следующим образом:

Шаблон для столбца со звездочкамиШаблон для столбца со звездочками

На очереди у нас столбец с названием Available. В его ячейках хранятся значения true и false, которые обозначают доступность автомобиля в текущий момент времени. Давайте зададим шаблон, который будет менять входящее значения ячейки на соответствующий текст Yes или No.

Для этого мы создадим специальную функцию, которая и будет менять нужные нам значения. Выглядит она так:

function customCheckbox(obj, common, value){  if(value){    return "<span class='webix_table_checkbox checked'> YES </span>";  }else{    return "<span class='webix_table_checkbox notchecked'> NO </span>";  }}

Теперь нужно установить эту функцию в качестве шаблона для столбца Available:

columns:[  //...  { id:"active", header:"Available", template:customCheckbox, ...,},]

В браузере результат будет следующим:

Шаблон для столбца "Available"Шаблон для столбца "Available"

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

А сейчас давайте займемся столбцом под названием Color. Его значения отображаются в виде HEX кодов, которые обозначают цвет автомобиля. Для обычного пользователя такие значения ничего не говорят. Давайте добавим визуальное представление цвета для каждой ячейки. И сделаем мы это также с помощью шаблона. Код будет выглядеть так:

columns:[  //...  { id:"color", header:"Color", template:`<span style="background-color:#color#; border-radius:4px; padding-right:10px;">&nbsp</span> #color#`},  //...]

Здесь мы используем строковый шаблон, в котором задаем фон неразрывного пробела (&nbsp) с помощью входящего HEX кода.

В браузере результат будет следующим:

Шаблон для столбца "Color"Шаблон для столбца "Color"

Работа с коллекциями

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

Для примера, давайте рассмотрим столбец с названием "Сar make", в котором должны отображаться марки автомобилей. Данные для его ячеек хранятся в виде чисел от 1 до 24 под ключем "car_make":

//data.json[  { "id":1, "rank":1, "car_make":22, ..., "country":1, "company":1, ..., },  { "id":2, "rank":2, "car_make":10, ..., "country":2, "company":3, ..., },  { "id":3, "rank":3, "car_make":16, ..., "country":1, "company":2, ..., },  //...]

Чтобы заменить числа в столбце на соответствующие названия автопроизводителей, нам нужно получить доступ к коллекции, в которой хранятся эти названия. Выглядит коллекция следующим образом:

//car_make.json[  { "id":22, "value":"Toyota" }, ...,  { "id":10, "value":"GMC" }, ...,  { "id":16, "value":"Mazda" }, ...,  //...]

В настройки столбца необходимо добавить свойство collection и присвоить ему путь к нужному объекту (коллекции):

columns:[  //...  { id:"car_make", header:"Car make", collection:"./data/car_make.json", ...,},  //...]

Вот таким образом, вместо числовых значений, в ячейках столбца Car make будут отображаться названия автопроизводителей. По такому же принципу мы заменяем значения для столбцов Company, Country и Card.

В браузере результат будет следующим:

Коллекции для столбцовКоллекции для столбцов

Работа с форматами данных

При работе с данными, не редко возникает потребность изменить формат их отображения. На этот случай, у таблицы имеется специальное свойство format. С его помощью мы можем задать желаемый формат отображения чисел и дат. Для этого предусмотрены специальные методы. Давайте зададим формат отображения для столбцов с датами и ценой заказа:

columns:[  //...  { id:"date", header:"Date", format:webix.i18n.longDateFormatStr, ..., },  { id:"price", header:"Price", format:webix.i18n.priceFormat, ..., },  //...]

Данные о датах приходят в виде строк 05/26/2021. Нам же нужно получить дату в формате 26 May 2021. Метод webix.i18n.longDateFormatStr, который мы применили в настройках столбца Date, должен получать объект Date и возвращать строку в нужном формате. Но сейчас он получает только строку типа 05/26/2021, поэтому результат может быть неожиданным. Давайте изменим входящие данные и преобразуем строки в соответствующие Date объекты.

Для этого у таблицы предусмотрено свойство scheme. В объекте этого свойства мы меняем строковое значение даты на соответствующий объект с помощью метода webix.i18n.dateFormatDate. Код будет выглядеть следующим образом:

{  view:"datatable",  //...  scheme:{    $init:function(obj){      obj.date = webix.i18n.dateFormatDate(obj.date)    }  },  columns:[...]}

С форматированием даты мы разобрались. Теперь давайте посмотрим как изменить цену в столбце "Price". А здесь все еще проще. Метод webix.i18n.priceFormat получает число (например 199) и возвращает строку со знаком доллара в начале: $199. Вот и вся хитрость.

В браузере результат будет следующим:

Форматирование даты и ценыФорматирование даты и цены

Узнать больше о возможностях форматирования данных библиотеки Webix можно в этой статье.

Сортировка данных

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

В виджете DataTable эта задача решается при помощи всего лишь одного свойства sort. Нам нужно только включить его в настройки необходимого столбца и задать правильный режим. Так как мы будем сортировать столбцы с датами, числами и строками, нам понадобятся следующие типы сортировки:

  • "int" - сравнивает числовые значения

  • "date" - сравнивает даты

  • "string" - сравнивает строковые значения в том виде, в котором они загружаются

  • "text"- сравнивает текст элемента, который отображается в ячейке (включая темплейт)

    columns:[  { id:"car_model", header:"Model", width:120, ..., sort:"string", }, ...,  { id:"car_year", header:"Year", width:85, ..., sort:"int" }, ...,{ id:"country", header:"Country", width:140, ..., sort:"text" }, ...,{ id:"date", header:"Date", width:150, ..., sort:"date" }, ...,]
    

Теперь данные будут сортироваться при клике по хедеру определенного столбца. Более того, мы можем установить режим, который позволяет сортировать данные по нескольким критериям одновременно. Для этого нужно задать свойству sort значение "multi" в конструкторе виджета. Чтобы отсортировать данные по нескольким условиям, нужно нажать клавишу Ctrl/Command и кликнуть по хедерам нескольких столбцов.

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

Если же нужно отсортировать данные таблицы при клике по какой либо внешней кнопке, вы можете воспользоваться методом таблицы sort(). Подробнее о сортировке данных вы можете узнать в этой статье.

Фильтрация данных

Сложно представить себе полноценную таблицу без возможности фильтровать в ней данные. И такая возможность предусмотрена в таблице Webix. С помощью свойства content, мы можем добавить один из нескольких встроенных фильтров или же задать собственные условия фильтрации. Это позволит нам фильтровать данные на клиенте или сервере по одному или нескольким критериям.

В зависимости от типа данных в столбцах, мы будем применять для них разные типы фильтров. Давайте добавим фильтр selectFilter с выпадающим списком опций в хедер столбца с названием Company. Для этого же столбца мы ранее указали свойство collection, которое заменяет числовые значения соответствующими названиями компаний. Фильтр сам подтянет данные этой коллекции и сформирует из них список опций. В настройках столбца это выглядит так:

columns:[  //...  {    id:"company",     header:["Company",{content:"selectFilter"}],     collection:"./data/company.json", ...,  }, ...,]

В браузере результат будет следующим:

Фильтр selectFilterФильтр selectFilter

Для столбца с названием Car make мы добавим фильтр textFilter. Он представляет собой обычное поле для ввода данных. Фильтр будет сравнивать введенные значения с данными столбца. Хочу напомнить, что данные приходят сюда в виде чисел, которые преобразуются в соответствующие названия моделей авто. Дело в том, что фильтр будет сравнивать введенные значения именно с числами, а это нас не совсем устраивает. Давайте изменим поведение фильтра по умолчанию и сделаем так, чтобы введенные значения сравнивались с данными из коллекции. Для этого мы добавим к фильтру специальную функцию для сравнения:

columns:[  //...  { id:"car_make", header:["Car make", {    content:"textFilter", placeholder:"Type car make",    compare:function(item, value, data){       const colValue = cars_make_data.getItem(item).value;      const toFilter = colValue.toLowerCase();      value = value.toString().toLowerCase();      return toFilter.indexOf(value) !== -1;    } }], collection:cars_make_data, ...,  },  //...]

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

columns:[  //...  { id:"car_model", header:["Model", {content:"textFilter", placeholder:"Type model"}, ...,],  //...]

В браузере результат будет следующим:

Фильтр textFilterФильтр textFilter

Теперь давайте перейдем к столбцу с названием Year, в ячейках которого отображается год производства автомобилей. Здесь мы добавим такой фильтр как excelFilter. Особенность его заключается в том, что мы можем вводить данные в поле ввода, задавать дополнительные условия и отмечать нежелательные элементы в специальных чекбоксах. Настройки столбца будут выглядеть так:

columns:[  //...  { id:"car_year", header:[{text:"Year", content:"excelFilter", mode:"number"}], ...,},  //...]

С помощью свойства mode мы указываем, что будем сравнивать числа. Вместо поля ввода, возле хедера появится специальная иконка, при клике по которой откроется окно для ввода условий. В браузере результат будет следующим:

Фильтр excelFilterФильтр excelFilter

Среди наших столбцов есть и такой, в котором отображаются доступные даты аренды автомобилей. Для работы с датами у виджета есть специальный фильтр datepickerFilter. Он позволяет выбрать необходимую дату в компактном всплывающем календаре. Настройки столбца будут выглядеть так:

columns:[  //...  { id:"date", header:["Date", {content:"datepickerFilter"}], ..., },  //...]

В браузере результат будет следующим:

Фильтр datepickerFilterФильтр datepickerFilter

Вот таким образом мы можем добавлять нужные нам фильтры для поиска данных в столбцах. С полным списком фильтров вы можете ознакомиться в этой статье документации.

Редактирование данных

Функционал виджета позволяет редактировать данные непосредственно в ячейках таблицы. Чтобы активировать эту опцию, необходимо задать свойству editable значение true в конструкторе таблицы. Также можно определить действие, по которому будет открываться редактор ячейки. По умолчанию, редактор открывается при клике по ячейке. Можно также определить открытие по двойному клику (dblclick) или указать собственное действие (custom).

Когда режим редактирования активирован, в настройках столбца нужно задать необходимый тип редактора. В зависимости от типа данных в столбце, мы будем применять соответствующий редактор.

А начнем мы из самого часто используемого редактора под названием text. Он превращает ячейку в обычное поле для ввода, куда можно ввести нужные нам данные. Давайте добавим его для нескольких столбцов нашей таблицы. Делается это очень просто: в настройках столбца нужно всего лишь указать свойство editor и присвоить ему соответствующий тип редактора. Настройки столбцов будут выглядеть так:

{  view:"datatable",  //...  editable:true,  editaction:"dblclick",  columns:[    { id:"rank", header:"Rank", editor:"text", ..., },    { id:"car_model", header:"Model", editor:"text", ..., },    { id:"manager", header:"Manager", editor:"text", ..., },    //...  ],  //...}

При двойном клике по любой из ячеек этих столбцов, в браузере мы увидим следующий результат:

Редактор "text"Редактор "text"

Если в ячейке будет находиться большой текст, то редактировать его в маленьком поле будет не очень удобно. Для таких случаев предусмотрен редактор popup. Он позволяет редактировать данные в специальном всплывающем окне. По умолчанию ширина и высота окна равны 250px и 50px соответственно. Давайте добавим этот редактор в настройки столбца Address:

columns:[  { id:"address", header:"Address", editor:"popup", ...,},  //...],//...

При двойном клике по любой ячейке столбца Address, в браузере мы увидим следующий результат:

Редактор "popup"Редактор "popup"

Теперь перейдем к столбцу c названием Available. Как вы помните, для него мы задали шаблон, который превращает значения true и false в соответствующие строки YES и NO. Давайте сделаем так, чтобы пользователь смог переключаться между этими значениями. Для этого мы используем специальный редактор inline-checkbox. Он позволяет менять значения в ячейке при клике по ней. Но для работы этого редактора также необходимо задать свойству checkboxRefresh значение true. Это свойство обновляет данные, полученные из checkbox-редакторов в таблице. Настройки столбца будут выглядеть так:

{  //...  checkboxRefresh:true  columns:[    //...    { id:"active", header:"Available", editor:"inline-checkbox", template:customCheckbox, ..., },  //...  ],  //...}

При клике по любой ячейке этого столбца, его значение изменится на противоположное.

А на очереди у нас столбцы, данные для которых подтягиваются из коллекций. Для них лучше всего подойдет редактор combo. Он позволяет выбирать нужную опцию из выпадающего списка. Список формируется автоматически исходя из данных коллекции столбца. Настройки столбцов будут выглядеть так:

columns:[  { id:"company", header:"Company", editor:"combo",    collection:"./data/company.json", ..., },  { id:"car_make", header:"Car make", editor:"combo",    collection:cars_make_data, ..., },  { id:"country", header:"Country", editor:"combo",   collection:"./data/country.json", ..., },  //...],//...

При двойном клике по ячейкам этих столбцов, в браузере мы увидим следующий результат:

Редактор "combo"Редактор "combo"

Особого внимания заслуживает столбец под названием Color. Хочу напомнить, что входящие данные представляют собой HEX коды различных цветов. У таблицы Webix есть специальный редактор, который позволяет выбрать необходимый цвет в специальном всплывающем окне, а его код отобразится в ячейке столбца. Речь идет о таком редакторе как color. Настройки столбца будут выглядеть так:

columns:[  { id:"color", header:"Color", editor:"color", template:..., },  //...], //...

При двойном клике по любой ячейке столбца Color, в браузере мы увидим следующий результат:

Редактор "color"Редактор "color"

Теперь нам осталось разобраться с редактированием данных в столбце Date. Для работы с датами у таблицы предусмотрен специальный редактор date. Как и в случае с фильтром, возле ячейки появится компактный календарь, где вы сможете выбрать нужную дату, которая сразу отобразится в редактируемой ячейке. Настройки столбца будут выглядеть так:

columns:[  {     id:"date", header:"Date", editor:"date",     format:webix.i18n.longDateFormatStr, ...,   },  //...], //...

При двойном клике по любой ячейке столбца Date, в браузере мы увидим следующий результат:

Редактор "date"Редактор "date"

Более подробно о редактировании таблицы можно узнать в соответствующей статье документации. А мы двигаемся дальше.

Валидация

После редактирования данных, они должны сохраняться на сервере. Здесь хорошим тоном будет проверить измененную информацию перед тем как ее сохранить. Как это сделать спросите вы? Да все донельзя просто. У библиотеки Webix есть специальные правила, которые можно применить для каждого редактируемого поля. В зависимости от типа данных, мы будем применять разные правила.

У нас есть несколько столбцов с числовыми значениями, для которых мы установим правило webix.rules.isNumber. Таблица будет проверять, является ли значение в текстовом поле числом.

Также у нас есть столбец, который содержит адреса электронной почты. Его значения мы будем проверять правилом webix.rules.isEmai.

Для столбца с ценами необходимо задать собственные условия проверки. Давайте сделаем так, чтобы пользователь мог ввести только данные в диапазоне от 20 до 500 долларов. Для этого мы создадим специальную функцию:

function(obj){ return (obj>20 && obj<500) }

Все остальные столбцы мы будем проверять правилом webix.rules.isNotEmpty. Это значит, что в любом случае они должны быть заполнены.

Чтобы применить все эти правила, у таблицы есть специальное свойство rules. Внутри объекта этого свойства необходимо указать id нужных столбцов и присвоить им соответствующие правила. Выглядит это так:

column:[...],rules:{  rank:webix.rules.isNumber,  company:webix.rules.isNotEmpty,  email:webix.rules.isEmail,  price:function(obj){ return(obj>20 && obj<500) },  // правила для других столбцов}

По умолчанию, валидация начнется после закрытия редактора. Если введенные значения не соответствуют правилам, то целый ряд подсветится красным, а в правом верхнем углу невалидной ячейки появится красная метка:

ВалидацияВалидация

Хедеры и футеры

Если вы работаете с большими данными, которые разделяются на множество столбцов, часто возникает необходимость объединить их названия в определенные категории. Такой подход помогает структурировать таблицу и упрощает поиск нужной информации. Виджет DataTable позволяет объединять хедеры с помощью свойств colspan и rowspan, которые немного похожи на настройки обычной HTML таблицы. Для примера, давайте посмотрим как объединить столбцы Price, Card и IBAN в категорию Payment information. Для этого нужно немного изменить свойство header вышеуказанных столбцов:

column:[  //...  { id:"price", header:[{text:"Payment information", colspan:3}, "Price"], ..., },  { id:"credit_card", header:["","Card"], ..., },  { id:"iban", header:["","IBAN"], ..., },  //...]

В браузере мы получим следующий результат:

Объединяем хедерыОбъединяем хедеры

Если хедеры подключены по умолчанию, то футеры нужно активировать отдельно. Для этого необходимо задать свойству footer значение true в конструкторе виджета. Давайте определим название футера для первого столбца и объединим его с футером второго столбца при помощи свойства colspan. А в футере столбца Available, где хранятся данные о доступных автомобилях, мы будем подсчитывать и отображать активные варианты. Настройки столбцов будут выглядеть так:

column:[  //...  { id:"stared", header:[...], ..., footer:{ text:"Available:", colspan:2 } },  //...  { id:"active", header:[...], ..., footer:{content:"summColumn"}, ..., },//...]

Элемент, заданный как {content:"summColumn"} , будет подсчитывать все значения равные true и отобразит их количество в футере. Все изменения в ячейках столбца Available незамедлительно отобразятся в его футере. В браузере мы получим следующий результат:

ФутерыФутеры

Управление видимостью столбцов

Если ваша таблица состоит из множества столбцов, то было бы очень удобно спрятать ненужные в данный момент варианты. Разработчики Webix позаботились о решении этого вопроса и реализовали такую опцию как headerMenu. С ее помощью мы можем управлять видимостью столбцов через специальный интерфейс. По желанию, его можно настроить по своему усмотрению.

Давайте добавим иконку headerMenu в качестве хедера для первых двух столбцов и настроим всплывающее окно меню. Код будет выглядеть так:

//...headermenu:{  width:210,  data:[     { id:"car_year", value:"Year" },    { id:"color", value:"Color" },    { id:"vin_code", value:"VIN" },    { id:"phone_number", value:"Phone" },    //...  ]},column:[  { id:"stared", header:[{ content:"headerMenu", colspan:2, ...,}], ..., },  { id:"rank", header:["",""], ..., },  //...]

В объекте свойства headermenu мы задаем ширину всплывающего окна и массив столбцов, которые можно будет прятать при клике по специальной иконке в виде глаза.

В браузере мы увидим такой результат:

Опция headermenuОпция headermenu

Теперь мы с легкостью можем скрывать и отображать нужные нам столбцы. Все что для этого требуется, просто кликнуть по иконке в левом верхнем углу и выбрать необходимые элементы в выпадающем списке.

Пагинация для таблицы

Если количество рядов таблицы достаточно большое, то использовать очень длинную вертикальную прокрутку не совсем удобно. Для решения этого вопроса можно добавить пагинацию. И как вы наверное уже догадались, виджет DataTable предоставляет нам такую возможность.

Для начала, давайте создадим модуль пагинации в файле pager.js. Его код будет выглядеть так:

//pager.jsconst pager = {  view:"pager",  id:"pager",  size:20,  group:5,  template:`{common.first()} {common.prev()} {common.pages()} {common.next()} {common.last()}`};

С помощью свойств size и group мы устанавливаем количество элементов на странице (20) и число видимых кнопок для пагинатора (5). Вот, в принципе, и все настройки. Также можно задать свойство template, которое определяет кнопки для переключения страниц (помимо кнопок с цифрами).

Теперь давайте подключим модуль с компонентом в файл index.html и добавим переменную с пагинатором в конструктор приложения:

//index.html<!--App sources --><script src="js/datatable.js" type="text/javascript" charset="utf-8"></script><script src="js/pager.js" type="text/javascript" charset="utf-8"></script>//...<script type="text/javascript">  webix.ready(function(){    webix.ui({      rows:[        datatable,        {cols:[          {},pager,{}        ]}      ]    });  });</script>

Ну и в завершение, давайте свяжем пагинатор с таблицей, чтобы при переключении кнопок, данные таблицы менялись в соответствии с настройками (по 20 рядов на странице). Для этого нужно задать свойству pager значение id созданного нами пагинатора в конструкторе таблицы. Вот такими нехитрыми манипуляциями реализуется пагинация для таблицы в библиотеке Webix. В браузере мы увидим следующий результат:

ПагинацияПагинация

Операции с рядами таблицы

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

Стоит отметить, что в библиотеке Webix есть несколько способов добавлять иконки. Можно использовать иконки из встроенного шрифта (<span class='webix_icon wxi-drag'></span>), или специальные встроенные элементы (common.trashIcon()).

Чтобы это реализовать, нужно перейти к массиву свойства columns и добавить следующие настройки:

column:[  //...  {     header:[{text:"<span class='webix_icon wxi-plus-circle'></span>", colspan:2}],     width:50, template:"<span class='webix_icon wxi-drag'></span>"   },  { header:["",""], width:50, template:"{common.trashIcon()}" }]

Теперь в конце таблицы появятся 2 дополнительных столбца, в каждом ряду которых будут находиться иконки для удаления и перетаскивания. Так как столбцов у нас достаточно много, давайте, для удобства, зафиксируем эти 2 столбца с иконками в конце таблицы, чтобы они постоянно были на виду и не прокручивались. Для этого, в конструкторе виджета необходимо задать свойству rightSplit значение 2. В браузере мы увидим следующий результат:

Иконки для операций с рядамиИконки для операций с рядами

Иконки у нас готовы. Теперь давайте установим обработчики на событие клика по этим иконкам. Чтобы поймать событие клика по любому элементу таблицы с определенным css классом, необходимо воспользоваться свойством onClick. В объекте этого свойства нужно указать класс иконки и присвоить ему соответствующий обработчик. В нашем случае, мы ловим клик по иконкам с классами wxi-plus-circle и wxi-trash:

onClick:{  "wxi-plus-circle":() => addNewElement(), //добавляет элемент  "wxi-trash":(e,id) => removeElement(id), //удаляет элемент  //...,}

Теперь давайте создадим эти обработчики. Функция для добавления новых данных будет выглядеть так:

function addNewElement(){  const table = $$("car_rental_table"); //получаем доступ к таблице  //добавляем данные  const id_new_elem = table.add({"active":0,"color":"#1c1919","date":new Date()});   table.showItem(id_new_elem); //показываем новый элемент в таблице}

С помощью метода таблицы add() мы можем добавить в нее новые данные. Этот метод возвращает id новой записи, который мы передаем другому методу таблицы showItem(), чтобы показать (проскролить) этот элемент в таблице.

Функция для удаления записи будет выглядеть так:

function removeElement(id){  $$("car_rental_table").remove(id);}

Метод таблицы remove() получает id выбранного элемента в качестве параметра и удаляет его из таблицы.

Нам осталось решить вопрос с перетаскиванием рядов. Эта опция активируется с помощью свойства drag. В конструкторе таблицы ему нужно задать значение order. После этого, мы можем перетаскивать любой ряд (вверх или вниз) и оставлять его в нужном месте.

Сейчас перетаскивать элемент можно за любую его часть. Давайте ограничим зону перетаскивания на специально созданной иконке с классом wxi-drag .

Для этого мы воспользуемся свойством on, в объекте которого и установим обработчик на событие onBeforeDrag:

on:{  onBeforeDrag:function(data, e){     return (e.target||e.srcElement).className == "webix_icon wxi-drag";  }}

В браузере мы увидим следующий результат:

Перетаскивание рядовПеретаскивание рядов

Теперь мы можем добавлять, удалять и перетаскивать ряды таблицы при помощи специальных иконок.

Тулбар с дополнительными опциями

Давайте добавим нашему приложению возможность сбрасывать фильтры, добавлять новые столбцы и экспортировать данные в формате Excel. Для этого мы создадим тулбар, в котором разместим соответствующие кнопки.

В файле toolbar.js мы создаем компонент toolbar, внутри которого определяем кнопки Reset filters, Add column и Export to Excel. Выглядит это так:

const toolbar = {  view:"toolbar",  css:"webix_dark",  height:50,  //...  cols:[    //...    { view:"button", label:"Reset filters", click:resetFilters },    { view:"button", label:"Add column", click:addColumn },    { view:"button", label:"Export to Excel", click:exportToExcel }  ]};

С помощью свойства click мы устанавливаем обработчики на событие клика по кнопкам. Давайте создадим эти обработчик. А начнем мы из функции, которая будет сбрасывать фильтрацию данных таблицы и сами фильтры. Код будет выглядеть так:

function resetFilters(){  const table = $$("car_rental_table");  table.filter();   table.showItem(table.getFirstId());   table.setState({filter:{}}); }

Метод filter(), вызванный для таблицы без параметров, отображает данные в первоначальном порядке. С помощью метода таблицы setState() мы очищаем значения полей фильтров.

Следующей на очереди у нас функция, которая будет добавлять новые столбцы. Код будет выглядеть так:

function addColumn(){  const table = $$("car_rental_table");  table.config.columns.splice(3,0,{    id:"c"+webix.uid(),    header:`<span class="webix_icon wxi-close-circle" webix_tooltip="Delete column"></span>Extra column`,    editor:"text",    width:120  });  table.refreshColumns();}

С помощью свойства таблицы config.columns мы получаем массив с настройками столбцов и добавляем туда объект с настройками нового столбца в 4 позицию. Для этого используем js метод splice(). Когда данные изменены, нужно обновить представление столбцов с помощью метода таблицы refreshColumns().

И у нас осталась только функция, которая будет экспортировать данные таблицы в формате Excel. Код будет выглядеть так:

function exportToExcel(){  webix.toExcel("car_rental_table", {    filename:"Car Rental Table",    filterHTML:true,    styles:true  });}

Внутри функции мы используем метод webix.toExcel(), которому передаем id таблицы и объект с необходимыми настройками. Вот и вся хитрость.

Когда все уже готово, нужно включить файл toolbar.js в файл index.html и добавить переменную toolbar в конструктор приложения:

webix.ui({  rows:[    toolbar,    datatable,    {cols:[    {},pager,{}    ]}  ]});

В браузере наше приложение будет выглядеть следующим образом:

Тулбар с кнопками Тулбар с кнопками

Теперь мы можем сбрасывать фильтрацию данных, добавлять новые столбцы, а также экспортировать данные в формате Excel.

Остался еще один нюанс. При создании нового столбца, в его хедер мы добавляем иконку с классом wxi-close-circle. Нам нужно установить обработчик на событие клика по этой иконке, который будет удалять столбец. Сделать это можно в объекте свойства onClick:

onClick:{  //...  "wxi-close-circle":(e,id) => deleteColumn(id)}

Теперь давайте создадим этот обработчик:

function deleteColumn(id){  const table = $$("car_rental_table");  table.clearSelection();  table.editStop();  table.refreshColumns(table.config.columns.filter(i=>i.id !== id.column));}

Через свойство config.columns мы получаем массив настроек столбцов, отфильтровываем из него ненужный элемент и передаем обновленный массив методу таблицы refreshColumns().

В браузере мы увидим следующий результат:

Добавляем и удаляем новый столбецДобавляем и удаляем новый столбец

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

Заключение

С кодом готового приложения можно ознакомиться тут.

В этой статье мы детально разобрали, как создать приложение на основе виджета DataTable библиотеки Webix UI, а также научились настраивать таблицу с помощью большого разнообразия свойств и методов. Как вы могли убедиться, все настройки интуитивно понятны и просты в применении. На самом деле, это только малая часть всего, что может предложить нам таблица Webix. Для более детального ознакомления можно перейти к документации, где вы сможете найти подробное описание всех возможностей компонента с наглядными примерами.

Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 473 (14 20 июня 2021)

21.06.2021 00:15:47 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript


Медиа


podcast Подкаст Веб-стандарты 286: Высокопроизводительное хранилище для вашего приложения: Storage Foundation API
podcast Подкаст Callback Hell: Микрофронтенды и Module Federation, почему компании боятся открывать свой код, игровая выставка E3
podcast Новости 512 от CSSSR: Canvas-рендеринг, Lighthouse 8, пропорции в CSS, PHP 8.1 alpha, Next.js 11, Линус и антипрививочник
podcast video Подкаст Ленивый фронтендер #2 Kaiwa Show | Как сохранить любовь к веб-разработке
podcast Подкаст Фронтенд Юность #191: HR'ы немножко осатанели


Веб-разработка


habr <img>. Доклад Яндекса
habr Темизация. История, причины, реализация
habr DIV должен уйти: улучшаем HTML
en Изучение Eleventy с нуля. Бесплатный курс, состоящий из 31 урока
en Как я использовал WAAPI для создания библиотеки анимации
en Десять лет веб-компонентам



CSS


video :has в CSS псевдокласс из будущего на примере карточки новости
en Использование свойства `outline` в качестве схлопывающейся границы
en Идеальные всплывающие подсказки с обрезкой и маскированием CSS
en Оптический размер, скрытая сверхспособность вариативных шрифтов
en Краткое руководство по логическим свойствам CSS
en Застенчивая кнопка стоимостью 8 миллионов долларов
en Создание таблиц с липким верхним и нижним колонтитулами стало немного проще

JavaScript


habr Скрываем номера курьеров и клиентов с помощью key-value хранилища
habr Юмористичный обзор Rust с перспективы JavaScript
en Управление состоянием: двусторонние биндинги и расширенные средства форматирования биндингов
en Что такое букмарклеты? Как использовать JavaScript для создания букмарклета в Chromium и Firefox
en Тестирование использования памяти в JavaScript
en Двойные кавычки против одинарных кавычек против обратных кавычек в JavaScript
en sorting-algos-visualizer Визуализация популярных алгоритмов сортировки: QuickSort, MergeSort, HeapSort, BubbleSort, InsertionSort







Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Автоматизация или смерть как управлять тысячами единиц игрового контента с помощью гугл-таблиц

10.06.2021 18:12:02 | Автор: admin

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

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

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

Баланс в онлайн-шутере крайне важная штука. Нужно, чтобы геймплей нравился всем игрокам не было тех, кто не может вообще никого убить или, наоборот, тех, кто доминирует и фрустрирует всех остальных.

На первых годах жизни Pixel Gun 3D использовалась простая схема: весь контент добавлялся и редактировался вручную. Нужно поменять урон пушке? Заходишь в Unity, открываешь нужный файл и правишь руками. Дело на пару минут.

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

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

Нужно было глобально что-то менять.

Сначала просто хотели сэкономить немного времени на балансировке контента, а в итоге автоматизировали большинство процессов, избавились от лишней ручной работы и разгрузили сотрудников. И все это с помощью гугл-таблиц.

Из гугл-таблиц в Unity

В первую очередь стали разбираться, как упростить работу с данными и можно ли их загружать в Unity из обычных гугл-таблиц. Оказалось, можно. Это был ключевой момент и первый шаг на пути к автоматизации назвали его удаленным контролем.

Для получения данных с таблиц мы используем Google Apps Script. Первое время заводили отдельные скрипты на каждую таблицу, в которых обрабатывали данные в JSON. Затем, получая в редакторе JSON, применяли их по назначению.

Сейчас пришли к тому, что опубликовали один скрипт. С его помощью получаем сырые данные с таблиц и уже в проекте конвертируем их в необходимый формат на С#. Этот подход более быстр и удобен.

Так выглядит наш скрипт:
function doGet(e){ if (e === undefined || e.parameter === undefined) {   return FailWithMessage("nullable parameters"); } var tableId = e.parameter["table"]; var listName = e.parameter["list"]; if (listName !== undefined && listName !== "" && listName !== "null") {   var startRow = parseInt(e.parameter["startRow"]);   var startColumn = parseInt(e.parameter["startColumn"]);   var numRow = parseInt(e.parameter["numRow"]);   var numColumn = parseInt(e.parameter["numCol"]);   return GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn); } else {   return GetAllTable(tableId); }}  function GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn){ var ss = SpreadsheetApp.openById(tableId); if (ss == null) {   return FailWithMessage("table with name: " + tableId + "not found"); } var sheet = ss.getSheetByName(listName); if (sheet == null) {   return FailWithMessage("list with name: " + listName + "not found"); }  if (numRow < 1) numRow = sheet.getLastRow(); if (numColumn < 1) numColumn = sheet.getLastColumn(); var range = sheet.getRange(startRow, startColumn, numRow, numColumn); var data = range.getValues(); var str = JSON.stringify(data);  var resultObject = {   "resultCode": 2,   "message": str }; var result = JSON.stringify(resultObject); return ContentService.createTextOutput(result);}  function GetAllTable(tableId){ var ss = SpreadsheetApp.openById(tableId); if (ss == null) {   return FailWithMessage("table with name: " + tableId + "not found"); }  var result = {};  var listModes = ss.getSheets(); for(var i = 0; i< listModes.length; i++) {   var sheet = listModes[i];   var sheetName = sheet.getSheetName();     var range = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn());   var data = range.getValues();   result[sheetName] = data; }  var str = JSON.stringify(result);  var resultObject = {   "resultCode": 2,   "message": str };  var result = JSON.stringify(resultObject); return ContentService.createTextOutput(result);} function FailWithMessage(message){ var result = {   "resultCode": 1,   "message": message };   var str = JSON.stringify(result);  return ContentService.createTextOutput(str);}

Для его использования необходимо создать новый скрипт, вставить содержимое из примера, опубликовать и в разрешениях указать, что запускать могут все, даже анонимные пользователи.

После публикации получится ссылка такого формата:

https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec

Ее нужно использовать для запуска скрипта. Чтобы скрипт знал, с какой таблицы нужны данные, в get-запрос подставляем ID таблицы. Получить его можно из URL таблицы. Например, в https://docs.google.com/spreadsheets/d/example_habr/edit#gid=0, ID будет example_habr.

Если нужно получить данные с конкретного листа, можно в запрос добавить его название тогда данные вернутся только по нему. То же самое и с диапазоном интересующих ячеек.

Полный запрос будет выглядеть так:

https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec?table=example_habr&list=MyList&startRow=1&startColumn=2&numRow=10&numCol=5

У Google есть ограничения на количество запросов и время выполнения. Поэтому нельзя использовать этот инструмент, например, для подтягивание конфигов напрямую в клиент у пользователей при превышении лимита скрипт начнет выдавать ошибку.

Теперь большинство параметров контента редактируется не внутри проекта, а во вне. Движок просто получает данные из таблицы в реальном времени. Например, нужно всем пушкам добавить 5 урона: вбиваем в ячейку, протягиваем по столбцу и нажимаем загрузить. Готово все данные ушли в клиент. По сути, мы с помощью нескольких гугл-таблиц управляем всей игрой.

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

Но мы пошли дальше.

Из Unity в гугл-таблицы

Когда пользователь играет матч, вся статистика отправляется на наш сервер: по ID игрока в базу данных сохраняется, с какой пушкой он ходил, какие результаты показал и прочее. Таких необработанных данных очень много по каждому пользователю и единице контента: киллрейты, результаты матчей, частота взятие пушек и другое. Их мы давно использовали для аналитики но все было вручную, долго и сложно.

Почему бы не использовать таблицы не просто для загрузки данных в Unity, но и в обратную для обработки собираемой статистики и метрик или поиска проблемных мест? Да и вообще для генерации самого контента.

Начали с простеньких задачек. Например, мы часто превышали лимит в 500 символов на описание апдейта в Google Play. Стор такое отклоняет, нужно переписывать и отправлять заново. Задались вопросом, а есть ли формула для подсчета символов в ячейке? Разумеется, в гугл-таблицах большой перечень базовых формул, которые можно комбинировать как угодно и решать практически любые задачи. Написали в ячейке, чтобы описание апдейта автоматически проверялось на количество символов =ДЛСТР(номер ячейки). Теперь проблемы нет.

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

В гугл-таблицах огромное количество функций, ознакомиться со всеми можно на сайте саппорта Google.

Например, решили составить коллекции для галереи оружия (это такие подборки оружия по схожим признакам: цвет, сеттинг, сезон). Чтобы составить одну коллекцию, нужно оценить визуал, смотрятся ли они вместе, схожий ли у них класс и другие детали. Для этого сделали таблицу, в которую из базы данных выгрузили список всех пушек, написали формулу и подгрузили превьюшки, с которыми можно визуально работать. А уже из таблиц по балансу подтягиваем названия, теги и остальную информацию.

В результате получили таблицу, которую можно подгрузить в любую другую и взаимодействовать с пушками наглядно они автоматом подтягиваются по тегу. Например, по тегу Weapon970 сразу подгружается ее название, превью и так далее. С этим уже можно спокойно работать.

Еще пример. Решили поменять баланс дробовиков (скорострельность, дальность и так далее). Надо понимать, что механик много, а позиций еще больше. Каждую нужно учитывать в формулах в виде отдельных коэффициентов. Геймдизайнер не может зайти и перепечатать параметры руками их придется менять у каждого дробовика, которых может быть несколько десятков.

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

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

Где используем автоматизацию

Мы применяем ее практически везде, где это возможно. И всегда ищем новые способы, чтобы сделать себе жизнь проще и переложить часть работы на формулы.

Балансировка контента

Самые базовый способ применения, о котором уже упоминали выше. Так мы ищем проблемные места, добавляем новый контент, не ломая старый, нерфим или бафаем оружие и так далее.

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

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

Генерация сущностей

Автоматизацию можно использовать не только для баланса, но и для генерации нового контента. Так, в каждом батлпассе у нас есть порядка 140 разных челленджей (например, сыграть 10 игр в определенном режиме или убить 30 противников из пистолета). Каждый раз придумывать это вручную долго и нудно, поэтому сделали генерацию. Собрали подробный список условий и прямо из гугл-таблицы создаем квесты теперь одна формула придумывает нам все задачи на каждый сезон.

Естественно, со сгенерированным пулом работает уже человек и настраивает кривую сложности. Игрок должен плавно войти в ивент, тем самым повысив конвертацию в платеж или в более длинную игровую сессию.

Подобным образом мы генерируем задачи и для клановых войн.

Пример формулы:

=ifs(I2="EndMatch";ifs(AE2<=TasksData!$O$34+TasksData!$N$34;"TeamDuel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34;ЕСЛИ(A2=0;"TeamDuel";"Spleef");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34;"Duel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34;ЕСЛИ(A2=0;"Duel";"BattleRoyale");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34;ЕСЛИ(A2=0;"TeamFight";"DeadlyGames");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34;"CapturePoints";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34;"FlagCapture";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34;"Deathmatch";AE2>TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34;"TeamFight"); I2="killPlayer";ifs(AE2<=TasksData!$N$35;"TeamDuel";AE2<=TasksData!$N$35+TasksData!$L$35;"Duel";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35;ЕСЛИ(A2=0;"TeamFight";"BattleRoyale");AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35;"DeadlyGames";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35;"CapturePoints";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35;"FlagCapture";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35;"Deathmatch";AE2>TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35;"TeamFight"); I2="killPet";ifs(AE2<=TasksData!$G$36;"Deathmatch";AE2>TasksData!$G$36;"TeamFight"); I2="killPlayerThroughWall";ifs(AE2<=TasksData!$I$37;"CapturePoints";AE2<=TasksData!$I$37+TasksData!$H$37;"FlagCapture";AE2<=TasksData!$I$37+TasksData!$H$37+TasksData!$G$37;"Deathmatch";AE2>TasksData!$I$37+TasksData!$H$37+TasksData!$G$37;"TeamFight"); I2="killPlayerFlying";ifs(AE2<=TasksData!$I$38;"CapturePoints";AE2<=TasksData!$I$38+TasksData!$H$38;"FlagCapture";AE2<=TasksData!$I$38+TasksData!$H$38+TasksData!$G$38;"Deathmatch";AE2>TasksData!$I$38+TasksData!$H$38+TasksData!$G$38;"TeamFight");I2="ramEscort";"Siege";I2="escortDestroyGate";"Siege";I2="winBrNoChest";"BattleRoyale";I2="crashChest";"BattleRoyale";I2="winBrInParty";"BattleRoyale";I2="flagCapture";"FlagCapture";I2="pointCapture";"CapturePoints")

Пример таблицы с вводными для генератора:

Пример формулы (в ней представлена часть с генерацией описаний задач, Key_номер это ключи локализаций, из которых составляются задачи):

=IFS(I2="endMatch";(ЕСЛИ(T2=0;"Key_7220";"Key_7234"));I2="killPet";ЕСЛИ(W2="None";"Key_7228";"Key_7224");I2="killPlayer";ЕСЛИ(Q2=1;"Key_7227";(ЕСЛИ(W2="NONE";ЕСЛИ(R2=1;"Key_7232";ЕСЛИ(S2=1;"Key_7233";"Key_7221"));"Key_7216")));I2="killPlayerFlying";"Key_7225";I2="killPlayerThroughWall";"Key_7226";I2="ramEscort";"Key_7235";I2="escortDestroyGate";"Key_7236";I2="winBrNoChest";"Key_7229";I2="crashChest";"Key_7230";I2="winBrInParty";"Key_7231";I2="flagCapture";"Key_7237";I2="pointCapture";"Key_7238";I2="";"")

И еще более эпичная, уже проходящая через ячейки параметров и рандомайзеров:

=ifs(L3="DeadlyGames";0;L3="BattleRoyale";0;L3="TeamDuel";0;1=1;ЕСЛИ(I3="killPlayer";ifs(A3=0;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47)*6;1;0);0);A3=1;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$47)*6;1;0);0);A3=2;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$C$47+TasksData!$C$48+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38+TasksData!$B$47+TasksData!$C$47+TasksData!$C$48+TasksData!$D$47)*6;1;0);0));0))

Симуляция процессов

Другой пример. Добавляем в игру сундук с предметами: игрок открывает и получает приз. У сундука есть множество правил, как и что будет выпадать. Можно написать код и отдать QA-отделу, чтобы это проверить, но есть способ лучше. Прямо в гугл-таблице делаем симулятор дропа: вносим правила, условия, открываем 1000 сундуков и корректируем всю модель под нужный исход. Для этого не нужен ни код, ни программист достаточно таблицы и самой концепции.

Пример части вводных данных:

Пример использованных формул в ячейках:

=СЛУЧМЕЖДУ(1;100)
=СРЗНАЧ(13;15)*6
=СУММ(B4:F4)
=IFS($A32<=G32;"Mythic";$A32<=F32;"Legend";$A32<=E32;"Epic";$A32<=D32;"Rare";$A32<=C32;"Common")

Сами по себе формулы простые, но при правильном построении связей ячеек и порядка выполнения действий получается нужный результат.

Пример результатов дропа по номерам открытий:

Разумеется, в самом проекте все пишет программист, но проверить это можно заранее. По сути все, что есть в игре, можно симулировать в таблице. И если результаты устраивают передавать в разработку.

Воркфлоу создания таблицы

Рассказать, как именно происходит работа с данными и создание таблиц, лучше всего на примере. Допустим, мы решили выяснить, какие пушки сейчас самые популярные в игре. А для этого нужно подтянуть множество дополнительных данных.

Расскажу подробнее по шагам (цифры в примере изменены в рамках конфиденциальности):

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

2. Затем создается гугл-таблица, в которой будет происходить основная работа. И в нее загружается полученный CSV-файл.

3. Для удобства из другой таблицы с помощью формулы подтягиваются картинки пушек. Арты предварительно загружены на сервис для получения прямой ссылки в формате (важно именно расширение в конце ссылки):

http://адрес_сервиса/путь/номер/имя картинки.jpg

Для добавления в таблицу используем простую функцию:

=IMAGE("https://files.fm/u/wdrhemgnk#/view/special_offer_pixelman_reward_big.png")

Пример формулы для подтягивания артов с перебором:

=ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!B:F);5;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!B:F);5;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!C:F);4;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!C:F);4;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!D:F);3;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!D:F);3;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!E:F);2;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!E:F);2;ЛОЖЬ);НИМА ТАКОГО))))

Теперь можно работать не с абстрактными строками, а наглядно. Если пушек 30 штук, можно зайти в игру и посмотреть, но когда их 700 так уже не сделаешь.

Важный момент: после подгрузки данных мы их вырезаем (ctrl+x) и вставляем без привязки к формуле (ctrl+x+v). Формулу затем удаляем, иначе после каждого обновления страницы она будет пересчитывать все строки. В данном случае более 800.

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

4. При желании можно выгрузить название пушки в игровом магазине (а также все необходимые для задачи параметры, например, дополнительные свойства).

5. Также используем форматирование ячеек. Оно может выполнить множество полезных преобразований. Например, покрасить ячейки, в которых значение больше 2 или меньше 1.

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

6. Добавляем столбец с категориями оружия (у нас их 6) и прибегая к тому же условному форматированию красим каждый в свой цвет. Стало видно, что в топе нет пушек категории Special, что уже наталкивает на анализ механик этой категории оружия.

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

Теперь можно сортировать, строить графики, добавлять продажи или другую информацию, чтобы быстро и обоснованно сделать нужные выводы или проверить теории.

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

График популярности оружияГрафик популярности оружия

Жизнь после автоматизации

На самом деле сплошные профиты. Проблемы дисбаланса значительно сократились, стало проще следить за контентом и предугадывать возможные проблемы, потому что общая картина всегда перед глазами, а проблемные места подсвечиваются.

Теперь легче что-то поменять, перезалить или вообще обновить целый класс оружия, в котором 100+ позиций. Достаточно вбить новые параметры все автоматически и с учетом механик пересчитается под норму (а часть параметров пересчитаются сами под новые вводные).

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

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

P.S. Данный подход меняет сам принцип работы с различными инструментами. Например, в том же Slack можно видеть BB-коды наглядно с помощью простой команды #:

Полезные ссылки

Подробнее..

Перевод Кастомные операторы RxJS

10.06.2021 18:12:02 | Автор: admin

Библиотека RxJS благодаря широкому выбору операторов по праву считается крайне мощным инструментом в арсенале разработчика. В этом посте я хочу представить вам концепцию кастомных операторов RxJS с примерами реализации.

Библиотека RxJS благодаря широкому выбору операторов по праву считается крайне мощным инструментом в арсенале разработчика. Недавно я подготовил несколько самописных операторов, чтобы повысить удобство повторного использования некоторых комбинаций операторов. В этом посте я хочу представить вам концепцию кастомных операторов RxJS с примерами реализации.

Оператор идентификации

Оператор RxJS это всего лишь функция, которая берет некие наблюдаемые (observable) данные в качестве входных и возвращает результирующий поток. Следовательно, задача написания кастомного оператора RxJS сводится к написанию обычной функции JavaScript (TypeScript). Начнем с базового оператора идентификации (identity), который просто зеркалирует наблюдаемые исходные данные:

import { interval, Observable } from "rxjs";import { take } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function identity<T>(source$: Observable<T>): Observable<T> {  return source$;}const results$ = source$.pipe(identity);results$.subscribe(console.log);  // console output: 0, 1, 2

Далее напишем кастомный оператор с кое-какой элементарной логикой.

Оператор логирования

Следующий кастомный оператор выполняет побочное действие (логирует значения в консоли) для каждого значения исходного потока:

<>Copyimport { interval, Observable } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function log<T>(source$: Observable<T>): Observable<T> {  return source$.pipe(tap(v => console.log(`log: ${v}`)));}const results$ = source$.pipe(log);results$.subscribe(console.log);  // console output: log: 0, log: 1, log: 2

В основе результирующего потока лежат данные source$, которые видоизменяются посредством применения встроенных операторов в составе метода pipe.

Фабрика оператора

В некоторых сценариях для кастомного оператора полезно указать контекст. Для этого можно определить функцию, возвращающую оператор. Аргументы фабрики входят в лексическую область видимости оператора:

import { interval, Observable } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function logWithTag<T>(tag: string): (source$: Observable<T>) => Observable<T> {  return source$ =>    source$.pipe(tap(v => console.log(`logWithTag(${tag}): ${v}`)));}const results$ = source$.pipe(logWithTag("RxJS"));results$.subscribe(console.log);  // console output: logWithTag(RxJS): 0, logWithTag(RxJS): 1, logWithTag(RxJS): 2

Описание возвращаемого типа можно упростить, воспользовавшись функцией MonoTypeOperatorFunction библиотекиRxJS. Кроме того, с помощью статической функции pipe можно сократить определение оператора:

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function logWithTag<T>(tag: string): MonoTypeOperatorFunction<T> {  return pipe(tap(v => console.log(`logWithTag(${tag}): ${v}`)));}const results$ = source$.pipe(logWithTag("RxJS"));results$.subscribe(console.log);  // console output: logWithTag(RxJS): 0, logWithTag(RxJS): 1, logWithTag(RxJS): 2

Другие полезные советы по RxJS можно почитать здесь.

Уникальная для наблюдателя лексическая область видимости

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

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function tapOnce<T>(job: Function): MonoTypeOperatorFunction<T> {  let isFirst = true;  return pipe(    tap(v => {      if (!isFirst) {        return;      }      job(v);      isFirst = false;    })  );}const results$ = source$.pipe(tapOnce(() => console.log("First value emitted")));results$.subscribe(console.log);results$.subscribe(console.log);  // console output: First value emitted, 0, 0, 1, 1, 2, 2

Чтобы у каждого наблюдателя была уникальная лексическая область видимости, можно применить функцию defer:

import { defer, interval, MonoTypeOperatorFunction } from "rxjs";import { take, tap } from "rxjs/operators";const source$ = interval(1000).pipe(take(3));function tapOnceUnique<T>(job: Function): MonoTypeOperatorFunction<T> {  return source$ =>    defer(() => {      let isFirst = true;      return source$.pipe(        tap(v => {          if (!isFirst) {            return;          }          job(v);          isFirst = false;        })      );    });}const results$ = source$.pipe(tapOnceUnique(() => console.log("First value emitted")));results$.subscribe(console.log);results$.subscribe(console.log);  // console output: First value emitted, 0, First value emitted, 0, 1, 1, 2, 2

Другой способ решения задачи tapOnce рассматривается в одном из моих предыдущих постов.

Практические примеры

Оператор firstTruthy:

import { MonoTypeOperatorFunction, of, pipe } from "rxjs";import { first } from "rxjs/operators";const source1$ = of(0, "", "foo", 69);function firstTruthy<T>(): MonoTypeOperatorFunction<T> {  return pipe(first(v => Boolean(v)));}const result1$ = source1$.pipe(firstTruthy());result1$.subscribe(console.log);// console output: foo

Оператор evenMultiplied:

import { interval, MonoTypeOperatorFunction, pipe } from "rxjs";import { filter, map, take } from "rxjs/operators";const source2$ = interval(10).pipe(take(3));function evenMultiplied(multiplier: number): MonoTypeOperatorFunction<number> {  return pipe(    filter(v => v % 2 === 0),    map(v => v * multiplier)  );}const result2$ = source2$.pipe(evenMultiplied(3));result2$.subscribe(console.log);  // console output: 0, 6

Оператор liveSearch:

import { ObservableInput, of, OperatorFunction, pipe  } from "rxjs";import { debounceTime, delay, distinctUntilChanged, switchMap } from "rxjs/operators";const source3$ = of("politics", "sport");type DataProducer<T> = (q: string) => ObservableInput<T>;function liveSearch<R>(  time: number,  dataProducer: DataProducer<R>): OperatorFunction<string, R> {  return pipe(    debounceTime(time),    distinctUntilChanged(),    switchMap(dataProducer)  );}const newsProducer = (q: string) =>  of(`Data fetched for ${q}`).pipe(delay(2000));const result3$ = source3$.pipe(liveSearch(500, newsProducer));result3$.subscribe(console.log);  // console output: Data fetched for sport

Заключение

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

Живой пример: [смотрите в оригинале]

Надеюсь, вам понравился мой пост и вы узнали что-то новое.


Перевод материала подготовлен в рамках курса "JavaScript Developer. Professional". Если вам интересно узнать о курсе подробнее, приглашаем на день открытых дверей онлайн, где преподаватель расскажет о формате обучения и программе.

Подробнее..

Как написать пассивный доход Пишем качественного трейд бота на JS (часть 1)

12.06.2021 20:19:26 | Автор: admin

Начнем писать трейдинг бота, который будет работать на криптобирже Binance. Бот должен уметь:

  1. торговать самостоятельно, принося какой-то доход

  2. должен быть удобен для создания и обкатывания различных стратегий торговли

  3. тестировать стратегию на исторических данных

Пожалуй, начнем с архитектуры

У нас есть биржа Binance, у которой есть шикарное api. Поэтому архитектура могла бы выглядеть так:

Вызвать пару методов купи дешевле и продай дороже. Но задача для нас написать такого бота, при котором условный программист-трейдер сможет создавать и тестировать на прибыльность новые стратегии. Поэтому, необходимо отделить логику торговли от всего прочего. А также модулю логики должно быть все равно к какой бирже его подключили: к реальному API или к псевдо-API (для тестирования). С учетом всего этого получилась примерно вот такая архитектура:

Базу выбрал PostgreSQL. Тут нет никакого тайного умысла. Вы можете использовать любую.

В связи с тем, что каждый модуль стоит внимания, это все не поместится в одну статью. Поэтому я начинаю мини-сериал: "Пишем качественного трейд бота на JS". Поэтому подписывайтесь, устраивайтесь поудобней - начинаем

Сервис для логов

Простой класс, который принимает на вход префикс для логирования и имеет два метода log и error. Эти методы печатают лог с текущим временем и перфиксом:

class LoggerService {  constructor(prefix) {    this.logPrefix = prefix  }  log(...props) {    console.log(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }  error(...props) {    console.error(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)  }}

Теперь подключим биржу

yarn add node-binance-api

Добавим класс BaseApiService. Сделаем в нем инициализацию Binance SDK, а также применим сервис LoggerService. Учитывая мой опыт с Binance могу сразу сказать, что в зависимости от торговой пары мы должны слать цену и обьем с разным количеством знаков после запятой. Все эти настройки для каждой пары можно взять, сделав запрос futuresExchangeInfo(). И написать методы для получения количества знаков после запятой для цены getAssetPricePrecision и объема getAssetQuantityPrecision.

class BaseApiService {  constructor({ client, secret }) {    const { log, error } = new Logger('BaseApiService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.exchangeInfo = {}  }  async init() {    try {      this.exchangeInfo = await this.api.futuresExchangeInfo()    } catch (e) {      this.error('init error', e)    }  }  getAssetQuantityPrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { quantityPrecision: 3 }    return s.quantityPrecision  }  getAssetPricePrecision(symbol) {    const { symbols = [] } = this.exchangeInfo    const s = symbols.find(s => s.symbol === symbol) || { pricePrecision: 2 }    return s.pricePrecision  }}

Дальше добавляем метод создания ордера, с учетом правильного количества знаков после запятой для цены и обьема:

async futuresOrder(side, symbol, qty, price, params={}) {  try {    qty = Number(qty).toFixed(this.getAssetQuantityPrecision(symbol))    price = Number(price).toFixed(this.getAssetPricePrecision(symbol))    if (!params.type) {      params.type = ORDER.TYPE.MARKET    }    const res = await this.api.futuresOrder(side, symbol, qty, price || false, params)    this.log('futuresOrder', res)    return res  } catch (e) {    console.log('futuresOrder error', e)  }}

Теперь бот умеет создавать ордера. Научим его слушать события из биржы для того, чтоб он мог отлавливать изменения статуса ордеров. Для этого создадим класс TradeService.

class TradeService {  constructor({client, secret}) {    const { log, error } = new LoggerService('TradeService')    this.log = log    this.error = error    this.api = new NodeBinanceApi().options({      APIKEY: client,      APISECRET: secret,      hedgeMode: true,    })    this.events = new EventEmitter()  }  marginCallCallback = (data) => this.log('marginCallCallback', data)  accountUpdateCallback = (data) => this.log('accountUpdateCallback', data)  orderUpdateCallback = (data) => this.emit(data)  subscribedCallback = (data) => this.log('subscribedCallback', data)  accountConfigUpdateCallback = (data) => this.log('accountConfigUpdateCallback', data)  startListening() {    this.api.websockets.userFutureData(      this.marginCallCallback,      this.accountUpdateCallback,      this.orderUpdateCallback,      this.subscribedCallback,      this.accountConfigUpdateCallback,    )  }  subscribe(cb) {    this.events.on('trade', cb)  }  emit = (data) => {    this.events.emit('trade', data)  }}

При помощи метода из SDK this.api.websockets.userFutureData подписываемся на события из биржы. Самой главный колбек для нас this.orderUpdateCallback . Он вызывается каждый раз когда меняется статус у ордера. Ловим это событие и прокидываем через EventEmitter тому, кто на это событие подписался, используя метод subscribe.

Перейдем к базе данных

Для чего она нужна? В базе будем хранить все ордера, а также всю историю торговли бота. Пользователей с их ключами к бирже и балансами. В последствии сможем считать сколько бот принес прибыли/убытка. Тут останавливаться долго не буду. Подключаю sequlize.

yarn add sequelize-cli -Dyarn add sequelizenpx sequelize-cli init

Добавим docker-compose.yml файл для локальной базы:

version: '3.1'services:  db:    image: 'postgres:12'    restart: unless-stopped    volumes:      - ./volumes/postgresql/data:/var/lib/postgresql/data    environment:      POSTGRES_USER: root      POSTGRES_PASSWORD: example      POSTGRES_DB: bot    ports:      - 5432:5432    networks:      - postgresnetworks:  postgres:    driver: bridge

А также добавляю миграции и модели. User, Order

Продолжение следует.

В следующей статье будем писать ядро, которое соединит все эти части и заставит бота торговать.

Подробнее..

Книга JavaScript с нуля

15.06.2021 18:13:51 | Автор: admin
imageПривет, Хаброжители! JavaScript еще никогда не был так прост! Вы узнаете все возможности языка программирования без общих фраз и неясных терминов. Подробные примеры, иллюстрации и схемы будут понятны даже новичку. Легкая подача информации и живой юмор автора превратят нудное заучивание в занимательную практику по написанию кода. Дойдя до последней главы, вы настолько прокачаете свои навыки, что сможете решить практически любую задачу, будь то простое перемещение элементов на странице или даже собственная браузерная игра.

Вот небольшой список того, что вы узнаете:

Как организовать код с помощью переменных.
Как функции делают ваш код повторно используемым.
Как работать с циклами и условиями.
Что такое глобальная и локальная области видимости.
Что такое замыкания.
Как правильно писать комментарии.
Основные типы объектов, с которыми вы столкнетесь в JavaScript.
Как работать с текстом и выполнять стандартные операции со строками.
Как использовать массивы для обработки списков.
Как создавать собственные объекты.

О ПИЦЦЕ, ТИПАХ, ПРИМИТИВАХ И ОБЪЕКТАХ


В ЭТОЙ ГЛАВЕ:

  • разберем суть и особенности объектов;
  • познакомимся с базовыми типами в JavaScript;
  • выясним, что пицца обладает не только прекрасным вкусом, но и образовательной ценностью.

Пора заняться серьезными делами. Суперсерьезными! В последних нескольких главах мы изучили разные значения, в том числе: строки (текст), числа, логические значения (true и false), функции и другие встроенные элементы JavaScript.

Вот некоторые примеры, чтобы освежить память:

let someText = "hello, world!";let count = 50;let isActive = true;

В отличие от других языков, JavaScript упрощает определение и использование этих встроенных элементов. Нам даже не требуется составлять план их будущего использования. Но несмотря на всю простоту, существует множество скрытых деталей. И их знание важно, так как не только облегчает понимание кода, но и ускоряет выявление причин его неисправностей.

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

Поехали!

Сначала поговорим о пицце


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

Если вы давненько ее не ели, то напомню, как она выглядит:

image

Конечно же, пицца не появляется в таком виде из ниоткуда. Она создается из простых и сложных ингредиентов:

image

Простые ингредиенты легко выявить. Это грибы и халапеньо. Причина, по которой мы называет их простыми, в том, что их нельзя разложить на составные части:

image

Они не изготавливаются и не составляются из других компонентов.

К сложным же ингредиентам относятся сыр, соус, основа из теста и пеперони. Сложными их делает то, что они сделаны из других ингредиентов:

image

К сожалению, такие ингредиенты, как сыр и пеперони, не бывают простыми. Для их приготовления нужно смешивать, жарить и добавлять различные компоненты. Кроме того, их получение не ограничивается смешиванием простых ингредиентов, но может также требовать совмещения сложных.

От пиццы к JavaScript


Все, что мы узнали о пицце в предыдущем разделе, было неспроста. Описание простых и сложных ингредиентов вполне применимо к типам в JavaScript. Каждый отдельно взятый ингредиент можно рассматривать как аналог типа, который вы можете использовать (рис. 12.1).

image

Подобно сыру, соусу, пеперони, грибам и бекону в нашей пицце, типами в JavaScript являются string (строка), number (число), boolean (логическое значение), null (пустой), undefined (не определен), bigint (целочисленные значения), symbol (символы) и Object (объект). С некоторыми из этих типов вы уже можете быть знакомы, с некоторыми нет. Подробнее мы будем рассматривать их в дальнейшем, сейчас же в табл. 12.1 вы можете посмотреть краткое описание их назначения.

image

Как мы видим, каждый тип имеет свое уникальное назначение. При этом они, аналогично ингредиентам пиццы, также подразделяются на простые и сложные. Только в терминологии JavaScript простые и сложные типы называются примитивами (примитивные типы) и объектами (объектные типы) соответственно.

К примитивным типам относятся string, number, boolean, null, bigint, symbol и undefined. Любые значения, попадающие в их юрисдикцию, не подлежат делению на части. Они являются халапеньо и грибами в мире JavaScript. Примитивы достаточно легко определять и оформлять в понятные элементы. В них нет глубины, и при встрече с ними мы, как правило, получаем то, что видим изначально.

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

Что такое объект?


Принцип объектов в таких языках программирования, как JavaScript, прекрасно отражает их аналогию из реальной жизни, в которой мы все буквально окружены объектами. К ним относятся ваш компьютер, книга на полке, картошка (спорно), будильник, плакат, заказанный на eBay, и т. д. Продолжать можно бесконечно.

Некоторые объекты вроде пресс-папье малофункциональны и могут долго бездействовать.

image

Другие объекты, вроде телевизора, уже выходят за рамки простого существования и выполняют множество задач:

image

Обычный телевизор получает сигнал, позволяет вам включать его и выключать, щелкать каналы, регулировать громкость и прочее.

Здесь важно понять, что объекты имеют разную форму, размер и назначение. Несмотря на эти отличия, на верхнем уровне они все одинаковы и представляют собой абстракцию. Они дают возможность пользоваться ими, не задаваясь вопросом об их внутреннем устройстве. Даже простейшие объекты скрывают в себе определенный уровень сложности, о котором можно не париться.

Например, не важно, что именно происходит внутри телека, как спаяны провода или какой клей использовался для соединения деталей. Все это не имеет значения. Все, что вас интересует, так это чтобы телевизор выполнял свое предназначение. Он должен исправно переключать каналы, позволять регулировать громкость и пр. Остальное лишние заморочки.

В принципе, объект можно рассматривать как черный ящик. Существует ряд предопределенных/описанных действий, которые он совершает. Увидеть же, как он это делает, достаточно непросто. На деле вас это и не интересует до тех пор, пока он делает все как надо. Мы изменим свое представление об этом позже, когда поучимся создавать внутренности объекта, а пока насладимся простотой этого мира.

Предопределенные объекты в JavaScript


Помимо встроенных типов, перечисленных ранее, в JS также изначально присутствуют предопределенные объекты. Эти объекты позволяют работать с чем угодно, включая наборы данных, даты, текст и числа. В табл. 12.2 приводится аналогичный предыдущему список, описывающий их назначения:

image

Использование встроенных объектов несколько отличается от использования примитивов. Каждый объект в этом плане по-своему особенный. Подробное пояснение всех этих особенностей использования я отложу на потом, а здесь приведу короткий фрагмент кода c комментарием, который покажет возможные варианты:

// массивlet names = ["Jerry", "Elaine", "George", "Kramer"];let alsoNames = new Array("Dennis", "Frank", "Dee", "Mac");// округленное числоlet roundNumber = Math.round("3.14");// текущая датаlet today = new Date();// объект booleanlet booleanObject = new Boolean(true);// бесконечностьlet unquantifiablyBigNumber = Number.POSITIVE_INFINITY;// объект stringlet hello = new String("Hello!");

Вас может несколько озадачить то, что примитивы string, boolean, symbol, bigint и number могут существовать и в форме объектов. Внешне эта объектная форма выглядит очень похожей на примитивную. Вот пример:

let movie = "Pulp Fiction";let movieObj = new String("Pulp Fiction");console.log(movie);console.log(movieObj);

При выводе обоих вариантах вы увидите одинаковый результат. Тем не менее внутренне movie и movieObj весьма различны. Первый буквально является примитивом типа string, а второй имеет тип Object. Это ведет к интересному (а иногда и непонятному) поведению, о котором я постепенно расскажу в процессе изучения встроенных типов.

КОРОТКО О ГЛАВНОМ

Если вам кажется, что все оборвалось на самом интересном месте, то это вполне нормально. Главный вывод здесь в том, что примитивы составляют большинство основных типов, которые вы будете использовать. Объекты несколько сложнее и состоят из примитивов или других объектов. Мы узнаем обо всем этом больше, когда начнем углубляться в тему. Помимо прочего, мы также узнали имена встроенных типов и некоторые присущие им особенности.

В последующих главах мы глубже изучим все эти типы, а также связанные с их использованием нюансы. Рассматривайте эту главу как плавный разгон, после которого вы резко влетите на рельсы безумных американских горок.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок
Электронная версия книги цветная

Для Хаброжителей скидка 25% по купону JavaScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Как работает Middleware в Express?

16.06.2021 00:20:01 | Автор: admin

Статья переведена. Ссылка на оригинал

Эта статья представляет собой адаптированный отрывок из книги "Express API Validation Essentials". Она научит вас полноценной стратегии валидации API, которую вы можете начать применять в своих Express-приложениях уже сегодня.

__________________

Документация Express говорит нам, что "приложение Express - это, по сути, серия вызовов функций middleware". На первый взгляд это звучит просто, но, честно говоря, промежуточное ПО может быть весьма запутанным. Вы, вероятно, задавались вопросом:

  • Где правильное место для добавления этого middleware в мое приложение?

  • Когда я должен вызвать функцию обратного вызова next, и что произойдет, когда я это сделаю?

  • Почему важен порядок использования middleware?

  • Как я могу написать свой собственный код для обработки ошибок?

Паттерн middleware является основополагающим при создании приложений на Express, поэтому вам необходимо хорошо понимать, что такое middleware и как оно работает.

В этой статье мы подробно рассмотрим паттерн промежуточного ПО (middleware). Мы также рассмотрим различные типы middleware Express и то, как эффективно сочетать их при создании приложений.

Шаблон Middleware

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

Когда вы определяете маршрут в Express, функция-обработчик маршрута, которую вы указываете для этого маршрута, является функцией Middleware:

app.get("/user", function routeHandlerMiddleware(request, response, next) {    // execute something});

(Пример 1.1)

Middleware достаточно гибкое. Вы можете указать Express запускать одну и ту же функцию middleware для различных маршрутов, что позволит вам делать такие вещи, как общая проверка для разных конечных точек API.

Помимо написания собственных функций middleware, вы также можете установить стороннее middleware для использования в вашем приложении. В документации Express перечислены некоторые популярные модули middleware. На npm также доступен широкий спектр модулей middleware Express.

Синтаксис Middleware

Вот синтаксис функции middleware:

/** * @param {Object} request - Express request object (commonly named `req`) * @param {Object} response - Express response object (commonly named `res`) * @param {Function} next - Express `next()` function */function middlewareFunction(request, response, next) {    // execute something}

(Пример 1.2)

Примечание: Вы могли заметить, что я называю req как request, а res как response. Вы можете называть параметры своих промежуточных функций как угодно, но я предпочитаю использовать более четкие имена переменных, поскольку считаю, что так другим разработчикам легче понять, что делает ваш код, даже если они не знакомы с фреймворком Express.

Когда Express запускает функцию middleware, ей передаются три аргумента:

  • Объект запроса Express (обычно называемый req) - это расширенный экземпляр встроенного в Node.js класса http.IncomingMessage.

  • Объект ответа Express (обычно называемый res) - это расширенный экземпляр встроенного в Node.js класса http.ServerResponse.

  • Функция Express next() - После того как промежуточная функция выполнит свои задачи, она должна вызвать функцию next(), чтобы передать управление следующей промежуточной программе. Если вы передаете ей аргумент, Express принимает его за ошибку. Он пропустит все оставшиеся функции middleware, не обрабатывающие ошибки, и начнет выполнять middleware, которое обрабатывает ошибки.

  • Функции middleware не должны иметь значение return. Любое значение, возвращаемое промежуточным ПО, не будет использовано Express.

Два типа Middleware

Обычное промежуточное ПО (middleware)

Большинство функций Middleware, с которыми вы будете работать в приложении Express, являются тем, что я называю "простым" промежуточным ПО (в документации Express нет специального термина для них). Они выглядят как функция, определенная в приведенном выше примере синтаксиса middleware (пример 1.2).

Вот пример простой функции middleware:

function plainMiddlewareFunction(request, response, next) {    console.log(`The request method is ${request.method}`);    /**     * Ensure the next middleware function is called.     */    next();}

(Пример 1.3)

Middleware для обработки ошибок

  • Разница между middleware для обработки ошибок и обычным middleware заключается в том, что функции middleware для обработки ошибок задают четыре параметра вместо трех, т.е. (error, request, response, next).

Вот пример функции middleware для обработки ошибок:

function errorHandlingMiddlewareFunction(error, request, response, next) {    console.log(error.message);    /**     * Ensure the next error handling middleware is called.     */    next(error);}

(Пример 1.4)

Эта промежуточная функция обработки ошибок будет выполнена, когда другая промежуточная функция вызовет функцию next() с объектом ошибки, например.

function anotherMiddlewareFunction(request, response, next) {    const error = new Error("Something is wrong");    /**     * This will cause Express to start executing error     * handling middleware.     */    next(error);}

(Пример 1.5)

Использование middleware

Порядок настройки middleware очень важен. Вы можете применить их на трех различных уровнях в вашем приложении:

  • Уровень маршрута

  • Уровень маршрутизатора

  • Уровень приложения

Если вы хотите, чтобы маршрут (или маршруты) обрабатывал ошибки, которые он вызывает, с помощью middleware для обработки ошибок, вы должны добавить его после определения маршрута.

Давайте рассмотрим, как выглядит настройка middleware на каждом уровне.

На уровне маршрута

Это самый конкретный уровень: любое промежуточное ПО, которое вы настроите на уровне маршрута, будет работать только для этого определенного маршрута.

app.get("/", someMiddleware, routeHandlerMiddleware, errorHandlerMiddleware);

(Пример 1.6)

На уровне маршрутизатора

Express позволяет создавать объекты Router. Они позволяют вам ограничить использование middleware определенным набором маршрутов. Если вы хотите, чтобы одно и то же middleware выполнялось для нескольких маршрутов, а не для всех, то такие объекты могут быть очень полезны.

import express from "express";const router = express.Router();router.use(someMiddleware);router.post("/user", createUserRouteHandler);router.get("/user/:user_id", getUserRouteHandler);router.put("/user/:user_id", updateUserRouteHandler);router.delete("/user/:user_id", deleteUserRouteHandler);router.use(errorHandlerMiddleware);

(Пример 1.7)

На уровне приложения

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

app.use(someMiddleware);// define routesapp.use(errorHandlerMiddleware);

(Пример 1.8)

Технически вы можете определить несколько маршрутов, вызвать app.use(someMiddleware), затем определить несколько других маршрутов, для которых вы хотите запустить someMiddleware. Я не рекомендую такой подход, поскольку он приводит к запутанной и трудноотлаживаемой структуре приложения.

Вы должны настраивать middleware на уровне приложения только в случае крайней необходимости, а именно, если его действительно нужно запускать для каждого маршрута в вашем приложении. Каждая функция middleware, независимо от того, насколько она мала, требует определенного времени для выполнения. Чем больше функций middleware необходимо запустить для маршрута, тем медленнее будут выполняться запросы к этому маршруту. Это действительно увеличивается по мере роста вашего приложения и конфигурации с большим количеством middleware. Старайтесь по возможности ограничивать middleware уровнями маршрута или маршрутизатора.

Подведение итогов

В этой статье мы узнали о шаблоне middleware в Express. Мы также узнали о различных типах middleware и о том, как их можно комбинировать при создании приложения на Express.

Если вы хотите прочитать больше о middleware, в документации Express есть несколько руководств:

__________________

Эта статья представляет собой адаптированный отрывок из книги "Express API Validation Essentials". Она научит вас полной стратегии валидации API, которую вы можете начать применять в своих Express-приложениях уже сегодня.

__________________

Все коды на изображениях для копирования доступны здесь.

Статья переведена в преддверии старта курса "Node.js Developer". Всех, кто желает подробнее узнать о курсе и процессе обучения, приглашаем записаться на Demo Day курса, который пройдет 28 июня.

- ЗАПИСАТЬСЯ НА DEMO DAY КУРСА

Статья переведена. Ссылка на оригинал

Подробнее..

React. Не в глубь, а в ширь. Композиция против реальности

16.06.2021 14:10:53 | Автор: admin

Давайте рассмотрим искусственный пример кода, который, как и в жизни, постепенно будет расширяться и усложняться, а наша задача, глядя на это всё, понять: не пора ли рефакторить. План наших действий: задача решение анализ рефакторинг. Приступим.

Задача: в проект нужны тултипы. Сказано сделано.

interface OwnProps {  hint: string}export const Tooltip: FC<OwnProps> = ({ hint, children }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)    useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )

Спустя какое-то время в проекте должен появиться ещё один тултип, он красивее и принимает обработчик клика. Самое простое и кратчайшее решение изменить имеющийся компонент Tooltip.

interface TooltipProps {  hint: string  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (onClick) {    return (      <div ref={ref}>      {children}      <AnotherTooltipComponent config={config} hint={hint} onClick={onClick} />    </div>    )  }  return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Мы модифицировали старый компонент, добавили инструкцию if и всё заработало. Единственное, что несколько смущает на данном этапе, это то, что из интерфейса TooltipProps совсем не очевидно, что обработчик onClick на самом деле не просто опциональное свойство, а ещё и определитель: какой вариант тултипа нужно вернуть. В общем, может и не очевидно, а может и очевидно, ясно одно: Done is better than perfect.

И вот нас снова просят добавить новый тултип DiscountTooltipComponent, который тоже обязательным свойством принимает обработчик onClick. Чтобы отличать два компонента DiscountTooltipComponent от AnotherTooltipComponent мы используем дополнительное свойство type.

interface TooltipProps {  hint: string  type?: 'another' | 'discount'  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ type, hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (type && onClick) {    return (      <div ref={ref}>      {children}      {type === 'another' ? (        <AnotherTooltipComponent config={config} hint={hint} onClick={onClick} />      ) : (        <DiscountTooltipComponent config={config} hint={hint} onClick={onClick} />      }    </div>    )  }  return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Условие в инструкции if усложнилось, внутри появился тернарный оператор, само собой код стало сложнее читать. И вот когда условия становятся сложнее и специфичнее, стоит это дело проанализировать.

Начнём сверху, с интерфейса TooltipProps. Глядя на него, совсем не очевидно, что поля type и onClick связаны между собой. Следовательно, не очевидны и варианты использования компонента Tooltip. Мы можем указать type = "another", но не передать onClick, и тогда typescript не выдаст ошибки.

Самое время обратиться к принципу разделения интерфейсов (Interface Segregation Principle), который на уровне компонентов называется принципом совместного повторного использования. Он гласит:

Не вынуждайте пользователей компонента зависеть от того, чего им не требуется.

Чтобы проблема стала видна отчётливее, представим, что прошло ещё немного времени.

Аналитики просят залогировать нажатие на DiscountTooltipComponent.

interface TooltipProps {  hint: string  type?: 'another' | 'discount'  onClick?: () => void}export const Tooltip: FC<TooltipProps> = ({ type, hint, children, onClick }) => {  // допустим, в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)    useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])    // ЗДЕСЬ М БУДЕМ ЛОГИРОВАТЬ  const handleClick = () => {    if (type === 'discount') {      // произвести логирование    }    if (onClick) {  onClick()    }}  // А ВОТ И НОВЙ ВАРИАНТ ИСПОЛЬЗОВАНИЯ!!!  // в этом компоненте уже обязательно нужен onClick  if (type) {    return (    <div ref={ref}>      {children}      {type === 'another' ? (        <AnotherTooltipComponent config={config} hint={hint} onClick={handleClick} />      ) : (        <DiscountTooltipComponent config={config} hint={hint} onClick={handleClick} />      }    </div>    )  }    return (    <div ref={ref}>      {children}      <TooltipComponent config={config} hint={hint} />    </div>  )}

Теперь все, кто использовал Tooltip в его первозданном виде, получили в нагрузку handleClick, который ими никак не используется, но ресурсы на него расходуются. А те, кто использовал компонент с type='another', получили не нужную обертку handleClick. Что, если мы разделим интерфейсы, например:

interface Tooltip {  hint: string}interface TooltipInteractive extends Tooltip {  onClick: () => void}

Теперь выделим общую логику в компонент TooltipSettings:

interface TooltipSettingsProps {  hint: string  render: (config: any, hint: string) => JSX.Element}export const TooltipSettings: FC<TooltipSettingsProps> = ({ render }) => {  // допустим в зависимости от кол-ва символов и пространства на экране  // производится позиционирование  const [config, setConfig] = useState(null)  const ref = useRef(null)  useLayoutEffect(() => {    // реализация алгоритма позиционирования    // ...    setConfig(someConfig)  }, [hint])  return (    <div ref={ref}>      {children}      {render(config, hint)}    </div>  )}

Реализуем интерфейс Tooltip:

export const Tooltip: FC<Tooltip> = ({ hint }) => (  <TooltipSettings hint={hint} render={(config, hint) => <TooltipComponent config={config} hint={hint} />} />)

Реализуем интерфейс TooltipInteractive:

export const AnotherTooltip: FC<TooltipInteractive> = ({ hint, onClick }) => (  <TooltipSettings    hint={hint}    render={(config, hint) => <AnotherTooltipComponent onClick={onClick} config={config} hint={hint} />}  />)

В частности DiscountTooltipComponent:

export const DiscountTooltip: FC<TooltipInteractive> = ({ hint, onClick }) => {  const handleClick = () => {    // произвести логирование    // вызвать обработчик    onClick()  }  return (    <TooltipSettings      hint={hint}      render={(config, hint) => <DiscountTooltipComponent onClick={handleClick} config={config} hint={hint} />}    />  )}

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

Подробнее..

SpaceShooter на Phaser 3

17.06.2021 14:15:17 | Автор: admin

Здравствуйте! Это перевод курса (ссылка на оригинал в конце статьи), который охватывает создание космического шутера с помощью Phaser 3. Курс будет состоять из 5 частей, в каждой из которых будет решаться определенная задача при создании проекта. Перед началом прохождения курса желательно знать основы JavaScript.

Шаг первый. Настроить веб-сервер

Первое, что необходимо сделать, это настроить веб-сервер. Несмотря на то, что фазерные игры запускаются в браузере, к сожалению нельзя просто запустить локально html-файл непосредственно из файловой системы. При запросе файлов по протоколу http, безопасность сервера позволяет получить доступ только к тем файлам, которые вам разрешены. При загрузке файла из локальной файловой системы (file://) ваш браузер сильно ограничивает его по очевидным причинам безопасности. Из-за этого нам нужно будет разместить нашу игру на локальном веб-сервере. Вы можете использовать любой удобный для вас веб-сервер, будь то OpenServer или какой-либо другой.

Шаг второй. Создать необходимы файлы и папки

Найдите где ваш веб-вервер размещает файлы сайтов и создайте в нём папку с вашим проектом. Назовите его как вам будет удобно. Внутри проекта создайте файл index.html. Наш индексный файл - это место, где объявим местоположение фазерного скрипта и остальных игровых скриптов.

Далее нам нужно создать две новые папки: content (спрайты, аудио и др.) и js (фазерные и игровые скрипты). Теперь внутри папки js нужно создать 4 файла: SceneMainMenu.js, SceneMain.js, SceneGameOver.js, и game.js.

На данный момент структура нашего проекта должна выглядеть следующим образом:

структура проекта в начале работыструктура проекта в начале работы

Теперь нужно загрузить контент для нашего проекта в папку content. Вы можете создать и загрузить свои собственные файла, либо воспользоваться готовыми (здесь).

Необходимый контент:

Sprites (images)

  • sprBtnPlay.png (Кнопка "Play")

  • sprBtnPlayHover.png (Кнопка "Play" когда мышь наведена)

  • sprBtnPlayDown.png (Кнопка "Play" когда кнопка нажата)

  • sprBtnRestart.png (Кнопка "Restart")

  • sprBtnRestartHover.png (Кнопка "Restart" когда мышь наведена)

  • sprBtnRestartDown (Кнопка "Restart" когда кнопка нажата)

  • sprBg0.png (фоновый слой звезд с прозрачностью вокруг звезд)

  • sprBg1.png (еще один фоновый слой звезд)

  • sprEnemy0.png (первый анимированный враг)

  • sprEnemy1.png (второй не анимированный враг)

  • sprEnemy2.png (третий анимированный враг)

  • sprLaserEnemy.png (выстрел лазера врагом)

  • sprLaserPlayer.png (выстрел лазера игроком)

  • sprExplosion.png (анимация взрыва)

  • sprPlayer.png (спрайт игрока)

Audio (.wav files)

  • sndExplode0.wav (первый звук взрыва)

  • sndExplode1.wav (второй звук взрыва)

  • sndLaser.wav (звук выстрела лазера)

  • sndBtnOver.wav (звук наведения мышки на кнопку)

  • sndBtnDown.wav (звук нажатия на кнопку)

Шаг третий. Загрузка фреймворка

Теперь необходимо загрузить актуальную версию самого Phaser. Это можно сделать здесь. В своем проекте вы можете использовать из phaser.js или phaser.min.js файл. Разница в том, что phaser.js имеет более удобный формат для чтения, но он и больше весит. Если вы не собираетесь вносить, какие-либо изменения в исходный код библиотеки, то можно использовать phaser.min.js. Он предназначен для распространения и сжимается для уменьшения размера файла. Загрузите файл в нашу папку js в проекте.

Шаг четвертый. Index.html

Следующее, что нужно сделать, это создать содержимое index.html, который лежит в корне проекта. Для этого используйте любой текстовый редактор или IDE по своему усмотрению.

Откройте index.html и введите следующий код:

<!DOCTYPE html><html>  <head>    <meta charset="utf-8">    <meta lang="en-us">    <title>Space Shooter</title>    <script src="js/phaser.js"></script> <!-- название файла должно соответствовать тому, который вы решили использовать. -->  </head>  <body>    <script src="js/Entities.js"></script>    <script src="js/SceneMainMenu.js"></script>    <script src="js/SceneMain.js"></script>    <script src="js/SceneGameOver.js"></script>    <script src="js/game.js"></script>  </body></html>

Обратите внимание на порядок подключения скриптов. Порядок очень важен, так как JavaScript интерпретируется сверху вниз. Мы будем ссылаться на код из файлов сцен (с приставкой Scene) в файле game.js.

Шаг пятый. Инициализация игры

Откройте game.js и создайте объект следующим образом:

var config = {}

Этот объект будет содержать свойства конфигурации, которые мы будем передавать нашему экземпляру игры phaser. Внутри данного объекта добавьте:

type: Phaser.WEBGL,width: 480,height: 640,backgroundColor: "black",physics: {  default: "arcade",  arcade: {    gravity: { x: 0, y: 0 }  }},scene: [],pixelArt: true,roundPixels: true

Пока что, внутри нашего объекта конфигурации, мы говорим нашей игре, что она должна визуализироваться с помощью WebGL, а не с помощью обычной технологии рендеринга Canvas. Далее, параметры width и height, устанавливают ширину и высоту нашей игры, которые будет занимать наша игра на странице. Свойство backgroundColor устанавливает черный цвет фона. Следующее свойство, physics, определяет физический движок, который будет использоваться, а именно arcade. Аркадная физика хорошо работает, когда нам нужно базовое обнаружение столкновений без каких-либо особенностей. Внутри physics, мы также устанавливаем гравитацию (gravity) для нашего физического мира. Следующее свойство scene, мы определяем массивом, который заполним немного позже. Наконец, мы хотим, чтобы Phaser обеспечивал четкость пикселей (pixelArt и roundPixels) точно так же, как ностальгические видеоигры, которые мы знали и полюбили.

scene: [  SceneMainMenu,  SceneMain,  SceneGameOver],

Теперь объект конфигурации закончен и мы не будем больше к нему прикасаться. Последняя и самая важная строка в этом файле это инициализация игры Phaser, где мы передаем нашу конфигурацию:

var game = new Phaser.Game(config);

Файл game.js завершен! В итоге он должен выглядеть следующим образом:

var config = {    type: Phaser.WEBGL,    width: 480,    height: 640,    backgroundColor: "black",    physics: {      default: "arcade",      arcade: {        gravity: { x: 0, y: 0 }      }    },    scene: [        SceneMainMenu,        SceneMain,        SceneGameOver    ],    pixelArt: true,    roundPixels: true}var game = new Phaser.Game(config);

Шаг шестой. Создание классов сцен

Давайте откроем SceneMainMenu.js и добавим в него следующий код:

class SceneMainMenu extends Phaser.Scene {  constructor() {    super({ key: "SceneMainMenu" });  }  create() {    this.scene.start("SceneMain");  }}

Здесь мы объявляем класс SceneMainMenu, который расширяет Phaser.Scene. Внутри данного класса есть две функции: constructor и create. Конструктор вызывается немедленно, при создании класса (подробнее с этим вы можете ознакомиться изучив принципы ООП). Внутри конструктора мы выполняется одна строчка кода:

super({ key: "SceneMainMenu" });

что фактически означает:

var someScene = new Phaser.Scene({ key: "SceneMainMenu" });

Вместо того, чтобы создавать экземпляр Phaser, мы определяем нашу сцену как класс, в котором мы можем создать пользовательские функции предварительной загрузки, создания и обновления. Функция create будет вызываться сразу же после создания сцены. Внутри функции create выполняется одна строка кода:

this.scene.start("SceneMain");

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

Теперь надо закончить оставшиеся классы в файлах SceneMain.js и SceneGameOver.js.

SceneMain.js:

class SceneMain extends Phaser.Scene {  constructor() {    super({ key: "SceneMain" });  }  create() {}}

SceneGameOver.js:

class SceneGameOver extends Phaser.Scene {  constructor() {    super({ key: "SceneGameOver" });  }  create() {}}

На данный момент, при запуске игры в браузере, у вас должен отобразиться черный прямоугольник:

Шаг седьмой. Загрузка игровых ресурсов

На этом шаге, в классе SceneMain нам нужно добавить новую функцию под названием preload. Эта функция должна быть размещена между функциями constructor и create. Теперь класс должен выглядеть следующим образом:

class SceneMain extends Phaser.Scene {  constructor() {    super({ key: "SceneMain" });  }  preload() {    }  create() {    }}

Внутри нашей новой функции предварительной загрузки нам нужно добавить код для загрузки наших игровых ресурсов. Чтобы загрузить файл изображения, внутри функции preload нужно ввести следующую строку:

this.load.image("sprBg0", "content/sprBg0.png");

Где первый параметр это imageKey. По нему далее в коде мы будем обращаться к загруженной картинке. А второй параметр - это путь к месту, где лежит файл изображения. Давайте загрузим остальные изображения, которые пригодятся нам для создания игры. После загрузки всех изображений, функция preload будет выглядеть следующим образом:

preload() {  this.load.image("sprBg0", "content/sprBg0.png");  this.load.image("sprBg1", "content/sprBg1.png");  this.load.spritesheet("sprExplosion", "content/sprExplosion.png", {    frameWidth: 32,    frameHeight: 32  });  this.load.spritesheet("sprEnemy0", "content/sprEnemy0.png", {    frameWidth: 16,    frameHeight: 16  });  this.load.image("sprEnemy1", "content/sprEnemy1.png");  this.load.spritesheet("sprEnemy2", "content/sprEnemy2.png", {    frameWidth: 16,    frameHeight: 16  });  this.load.image("sprLaserEnemy0", "content/sprLaserEnemy0.png");  this.load.image("sprLaserPlayer", "content/sprLaserPlayer.png");  this.load.spritesheet("sprPlayer", "content/sprPlayer.png", {    frameWidth: 16,    frameHeight: 16  });}

Обратите внимание, что в некоторых местах мы загружаем не image, а spritesheet. Это означает, что мы загружаем анимацию, а не статическое изображение. Spritesheet - это изображение с несколькими кадрами, расположенными бок о бок. Также в spritesheet мы определяем третьим аргументом ширину и высоту кадра в пикселях.

Теперь нам нужно будет загрузить звуки. Это делается очень похоже на то, как мы загружали изображения. Добавьте в конец функции preload следующий код:

this.load.audio("sndExplode0", "content/sndExplode0.wav");this.load.audio("sndExplode1", "content/sndExplode1.wav");this.load.audio("sndLaser", "content/sndLaser.wav");

Шаг восьмой. Немного кода для анимации.

После того, как мы загрузили контент, нам нужно добавить немного больше кода для создания анимации. Теперь в функции create() класса SceneMain создадим анимации:

this.anims.create({  key: "sprEnemy0",  frames: this.anims.generateFrameNumbers("sprEnemy0"),  frameRate: 20,  repeat: -1});this.anims.create({  key: "sprEnemy2",  frames: this.anims.generateFrameNumbers("sprEnemy2"),  frameRate: 20,  repeat: -1});this.anims.create({  key: "sprExplosion",  frames: this.anims.generateFrameNumbers("sprExplosion"),  frameRate: 20,  repeat: 0});this.anims.create({  key: "sprPlayer",  frames: this.anims.generateFrameNumbers("sprPlayer"),  frameRate: 20,  repeat: -1});

Нам также нужно добавить звуки к какой-то переменной или объекту, чтобы мы могли ссылаться на него позже. Если существует более одного звука (скажем, три звука взрыва), я добавляю массив в качестве значения свойства explosions. Давайте добавим объект звукового эффекта:

this.sfx = {  explosions: [    this.sound.add("sndExplode0"),    this.sound.add("sndExplode1")  ],  laser: this.sound.add("sndLaser")};

Позже мы сможем воспроизводить звуковые эффекты с нашего объекта, например:

this.scene.sfx.laser.play();

Нам также придется загрузить некоторые изображения и звуки для главного меню и экрана Game Over. Открывайте SceneMainMenu.js и создайте функцию предварительной загрузки (preload()) внутри класса SceneMainMenu. Внутри новой функции предварительной загрузки добавьте следующее, чтобы добавить наши кнопки и звуки:

Теперь мы можем вернуться к игре в браузере, и черный прямоугольник все еще должен отображаться. Откройте инструменты разработки в браузере, который вы используете. Если вы используете Chrome или Firefox, вы можете просто нажать F12, чтобы открыть его. Посмотрите во вкладке Консоли (Console), чтобы убедиться в отсутствии ошибок (они отображаются красным цветом.) Если вы не видите ошибок, мы можем приступить к добавлению игрока!

Шаг девятый. Создание игрока

Прежде чем добавить космический корабль игрока в игру, мы должны добавить новый файл в нашу папку js под названием Entities.js. Этот файл будет содержать все классы для различных сущностей в нашей игре. Мы будем классифицировать игрока, врагов, лазеры и т. д., как сущности. Обязательно также добавьте ссылку на Entities.js в index.html перед SceneMainMenu.js. После этого откройте файл и объявите новый класс с именем Entity.

class Entity {constructor(scene, x, y, key, type) {}}

Как вы можете увидеть, мы сразу определяем параметры которые будет принимать конструктор класса. Каждый из параметров, которые мы добавили в конструктор, будет важен, потому что мы будем расширять класс для всех сущностей, которые мы создадим. Очень похоже на то, как мы расширили Phaser.Scene когда мы начали эту игру. Теперь мы расширим класс Entity:

class Entity extends Phaser.GameObjects.Sprite

Как всегда при расширении класса, нам нужно будет добавить super в наш конструктор. Поскольку игрок, враги и различные снаряды, которые мы добавляем, будут иметь одни и те же базовые свойства, это помогает нам не добавлять избыточный, дублирующий код. Таким образом, мы унаследуем свойства и функции базового класса Phaser.GameObjects.Sprite для всех наших сущностей.

Все наши сущности будут иметь одинаковые базовые свойства (scene, x, y, key и type, которую мы будем использовать для извлечения определенных сущностей, если нам это понадобится.) Давайте добавим super в наш конструктор, который должен выглядеть следующим образом:

super(scene, x, y, key);

После добавления ключевого слова super в конструктор на следующей строке добавьте строки:

this.scene = scene;this.scene.add.existing(this);this.scene.physics.world.enableBody(this, 0);this.setData("type", type);this.setData("isDead", false);

Этот фрагмент кода назначает сцену сущности, а также добавляет экземпляр сущности в очередь рендеринга сцены. Мы также включаем инстанцированные сущности в качестве физических объектов в физическом мире сцены. Наконец, мы определяем скорость игрока. С этим мы должны закончить добавление в наш класс сущностей. Теперь мы можем перейти к созданию вашего класса игроков.

К настоящему времени вы должны знать, как добавить класс. Добавьте его сразу после класса Entity и назовите его Player и убедитесь, что он расширяет Entity. Добавьте конструктор в класс Player с параметрами: scene, x, y и key. Затем добавьте ключевое слово super в конструктор, предоставив ему следующие параметры:

super(scene, x, y, key, "Player");

Нам также понадобится способ определить скорость, с которой должен двигаться игрок. Добавив пару ключ/значение скорости игрока, мы можем обратиться к ней позже для наших функций движения. Под ключевым словом super добавьте следующее:

this.setData("speed", 200);

Мы также хотим добавить небольшой фрагмент кода для воспроизведения анимации игрока:

this.play("sprPlayer");

Чтобы добавить функции перемещения, добавьте следующие четыре функции после конструктора.

moveUp() {  this.body.velocity.y = -this.getData("speed");}moveDown() {  this.body.velocity.y = this.getData("speed");}moveLeft() {  this.body.velocity.x = -this.getData("speed");}moveRight() {  this.body.velocity.x = this.getData("speed");}

Эти функции позволяют перемещаться игроку с установленной скоростью на экране по координатам x и y.

Эти функции будут вызваны в функции update(). Добавьте функцию update() непосредственно под функцией moveRight. Внутри функции обновления пропишите:

this.body.setVelocity(0, 0);this.x = Phaser.Math.Clamp(this.x, 0, this.scene.game.config.width);this.y = Phaser.Math.Clamp(this.y, 0, this.scene.game.config.height);

Теперь мы закончили с классом игрока! В данном случае скорость игрока будет равна нулю. Если ни одна из клавиш перемещения не нажата, игрок останется неподвижным. Следующие две строки кода обновления игрока гарантируют, что игрок не сможет выйти за пределы экрана. На этом этапе мы можем создать экземпляр игрока в функции create главной сцены. Добавьте следующее в функцию создания главной сцены:

this.player = new Player(  this,  this.game.config.width * 0.5,  this.game.config.height * 0.5,  "sprPlayer");

Именно здесь мы создаем экземпляр игрока. Мы можем обратиться к игроку в любом месте SceneMain. Затем игрок располагается в центре холста. Если вы попытаетесь запустить игру, вы все равно не увидите, как игрок двигается. Это происходит потому, что сначала мы должны добавить функцию обновления в SceneMain и добавить проверки движения. Поскольку this.player теперь добавлен, теперь мы можем добавить функцию обновления. Добавьте функцию обновления прямо под функцией создания главной сцены и добавьте внутрь следующее:

this.player.update();if (this.keyW.isDown) {  this.player.moveUp();}else if (this.keyS.isDown) {  this.player.moveDown();}if (this.keyA.isDown) {  this.player.moveLeft();}else if (this.keyD.isDown) {  this.player.moveRight();}

Напомню, что this.player.update() запустит код обновления, который сохранит игрока неподвижным, а также гарантирует, что он не сможет переместиться за пределы экрана. В функции create() класса SceneMain добавьте следующее для инициализации наших ключевых переменных после инициализации игрока:

this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);this.keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

Если мы запустим наш код сейчас, игрок должен иметь возможность перемещаться с помощью клавиш W, S, A, D. В следующей части мы добавим возможность для игрока стрелять лазерами (которые будут использовать клавишу пробела.)

Шаг десятый. Добавление врагов

Давайте теперь откроем Entities.js и добавим классы врагов. В самом низу Entities.js под классом игрока добавьте три новых класса, называемых ChaserShip, GunShip и CarrierShip:

class ChaserShip extends Entity {  constructor(scene, x, y) {    super(scene, x, y, "sprEnemy1", "ChaserShip");  }}class GunShip extends Entity {  constructor(scene, x, y) {    super(scene, x, y, "sprEnemy0", "GunShip");    this.play("sprEnemy0");  }}class CarrierShip extends Entity {  constructor(scene, x, y) {    super(scene, x, y, "sprEnemy2", "CarrierShip");    this.play("sprEnemy2");  }}

Классы ChaserShip, GunShip и CarrierShip должны расширить класс Entity, который мы создали ранее. Затем мы вызываем конструктор с соответствующими параметрами. Для каждого класса врагов под ключевым словом super добавьте следующее:

this.body.velocity.y = Phaser.Math.Between(50, 100);

Приведенная выше строка устанавливает скорость противника как случайное целое число между 50 и 100. Мы будем создавать врагов в верхней части экрана, что заставит их двигаться вниз по холсту.

Затем вернитесь к SceneMain.js. Нам нужно будет создать группу, чтобы удерживать наших врагов, лазеры, стреляющие врагами, и лазеры, стреляющие игроком. В функции create после установки строки this.keySpace добавьте:

this.enemies = this.add.group();this.enemyLasers = this.add.group();this.playerLasers = this.add.group();

Если мы запустим нашу игру, то там еще не будет никаких врагов. Сначала мы должны создать событие (оно будет действовать как таймер), которое породит наших врагов. После нашей группы playerLasers добавьте следующий код:

this.time.addEvent({  delay: 100,  callback: function() {    var enemy = new GunShip(      this,      Phaser.Math.Between(0, this.game.config.width),      0    );    this.enemies.add(enemy);  },  callbackScope: this,  loop: true});

Если мы попробуем запустить игру сейчас, то увидим множество врагов-боевых кораблей, движущихся вниз. Теперь мы дадим нашим противникам возможность стрелять. Во-первых, мы должны создать еще один класс под названием EnemyLaser сразу после класса игрока. Откройте Entities.js. Вражеский лазер также должен расширить класс Entity.

class EnemyLaser extends Entity {  constructor(scene, x, y) {    super(scene, x, y, "sprLaserEnemy0");    this.body.velocity.y = 200;  }}

Теперь мы можем вернуться к нашему классу боевых кораблей, а именно к конструктору. В разделе, где мы устанавливаем скорость, мы можем добавить новое событие.

this.shootTimer = this.scene.time.addEvent({  delay: 1000,  callback: function() {    var laser = new EnemyLaser(      this.scene,      this.x,      this.y    );    laser.setScale(this.scaleX);    this.scene.enemyLasers.add(laser);  },  callbackScope: this,  loop: true});

Обратите внимание, что мы присваиваем описанное выше событие переменной this.shootTimer. Мы должны создать новую функцию внутри GunShip под названием onDestroy. onDestroy - это не функция, используемая Phaser, поэтому вы можете назвать ее как угодно. Мы будем использовать эту функцию, чтобы уничтожить таймер стрельбы, когда враг будет уничтожен. Добавьте функцию onDestroy в наш класс GunShip и добавьте внутрь следующее:

if (this.shootTimer !== undefined) {  if (this.shootTimer) {    this.shootTimer.remove(false);  }}

Когда вы запустите игру вы должны увидеть:

Когда мы запустим игру, вы должны увидеть армию боевых кораблей, спускающихся с верхней части экрана. Все враги также должны стрелять лазерами. Теперь, когда мы видим, что все работает, мы можем сразу сократить количество боевых кораблей. Для этого перейдите в SceneMain.js файл и изменить задержку времени.

delay: 1000,

Теперь вернитесь обратно в Entities.js, нам нужно будет добавить немного кода в конструктор класса ChaserShip:

this.states = {  MOVE_DOWN: "MOVE_DOWN",  CHASE: "CHASE"};this.state = this.states.MOVE_DOWN;

Этот код делает две вещи: создает объект с двумя свойствами, которые мы можем использовать для установки состояния корабля-преследователя, а затем мы устанавливаем состояние в значение свойства MOVE_DOWN.

Теперь мы можем добавить функцию обновления в класс ChaserShip. Функция обновления - это то, где мы будем кодировать ИИ для класса корабля-охотника. Сначала мы закодируем разведданные для вражеского истребителя, так как это немного сложнее. Перейдите обратно к Entities.js, а в функции обновления класса ChaserShip добавьте следующее:

if (!this.getData("isDead") && this.scene.player) {  if (Phaser.Math.Distance.Between(    this.x,    this.y,    this.scene.player.x,  this.scene.player.y  ) < 320) {    this.state = this.states.CHASE;  }  if (this.state == this.states.CHASE) {    var dx = this.scene.player.x - this.x;    var dy = this.scene.player.y - this.y;    var angle = Math.atan2(dy, dx);    var speed = 100;    this.body.setVelocity(      Math.cos(angle) * speed,      Math.sin(angle) * speed    );  }}

С помощью этого кода враги-преследователи будут двигаться вниз по экрану. Однако, как только он окажется в пределах 320 пикселей от игрока, он начнет преследовать игрока. Если вы хотите, чтобы корабль-охотник вращался, добавьте следующее сразу после (или в конце) нашего условия погони:

if (this.x < this.scene.player.x) {  this.angle -= 5;} else {  this.angle += 5;} 

Чтобы породить корабль-охотник, нам придется вернуться на SceneMain.js и добавьте новую функцию под названием getEnemiesByType. Внутри этой новой функции добавьте:

getEnemiesByType(type) {  var arr = [];  for (var i = 0; i < this.enemies.getChildren().length; i++) {    var enemy = this.enemies.getChildren()[i];    if (enemy.getData("type") == type) {      arr.push(enemy);    }  }  return arr;}

Приведенный выше код позволит нам указать тип врага и получить всех врагов из группы врагов. Этот код проходит через группы врагов и проверяет, равен ли тип врага в цикле типу, заданному в качестве параметра.

Как только мы добавили функцию getEnemiesByType, нам нужно будет изменить наше событие spawner. В анонимной функции свойства обратного вызова давайте изменим:

на:

var enemy = null;if (Phaser.Math.Between(0, 10) >= 3) {  enemy = new GunShip(  this,  Phaser.Math.Between(0, this.game.config.width),0);} else if (Phaser.Math.Between(0, 10) >= 5) {  if (this.getEnemiesByType("ChaserShip").length < 5) {    enemy = new ChaserShip(    this,    Phaser.Math.Between(0, this.game.config.width),0);}} else {  enemy = new CarrierShip(  this,  Phaser.Math.Between(0, this.game.config.width),0);}if (enemy !== null) {  enemy.setScale(Phaser.Math.Between(10, 20) * 0.1);this.enemies.add(enemy);}

Проходя через этот блок, мы добавляем условие, которое выбирает один из наших трех классов врагов: GunShip, ChaserShip или CarrierShip, который будет создан. Установив переменную enemy, мы затем добавляем ее в группу enemies. Если CarrierShip выбран для нереста, мы проверяем, чтобы было не более пяти ChaserShip, прежде чем нерестить еще один. Прежде чем добавить врага в группу, мы также применяем к нему случайную шкалу. Поскольку каждый враг расширяет наш класс Entity, который, в свою очередь, расширяет Phaser.GameObjects.Sprite мы можем установить масштаб для врагов, как и для любого другого Phaser.GameObjects.Sprite.

В функции обновления нам нужно обновить врагов в группе this.enemies. Для этого в конце функции обновления добавьте следующее:

for(vari=0;i<this.enemies.getChildren().length;i++){varenemy=this.enemies.getChildren()[i];enemy.update();}

Если мы попробуем запустить игру сейчас, то увидим, что корабли преследователей должны двигаться к кораблю игрока, как только они окажутся на расстоянии досягаемости.

Шаг одиннадцатый. Дадим игроку возможность стрелять

Вернитесь к классу Player и в конструкторе добавьте:

this.setData("isShooting",false);this.setData("timerShootDelay",10);this.setData("timerShootTick",this.getData("timerShootDelay")-1);

Мы устанавливаем то, что я бы назвал ручным таймером. Мы не используем события для способности игрока стрелять. Это происходит потому, что мы не хотим, чтобы задержка снималась при первоначальном нажатии клавиши пробела. В функции обновления класса игрока мы добавим остальную логику для нашего ручного таймера:

if(this.getData("isShooting")){  if(this.getData("timerShootTick")<this.getData("timerShootDelay")){    // каждое обновление игры увеличивайте timerShootTick на единицу, пока мы не достигнем значения timerShootDelay    this.setData("timerShootTick",this.getData("timerShootTick")+1);  } else{//когда "ручной таймер" срабатывает:    varlaser=newPlayerLaser(this.scene,this.x,this.y);    this.scene.playerLasers.add(laser);    this.scene.sfx.laser.play();//воспроизвести звуковой эффект лазера    this.setData("timerShootTick",0);  }}

Единственное, что нам осталось сделать, это добавить класс лазера игрока в наш Entities.js файл. Мы можем добавить этот класс прямо под классом Player и перед классом EnemyLaser. Это позволит сохранить наши классы, связанные с игроком, и наши классы, связанные с врагами, вместе. Создайте конструктор внутри класса PlayerLaser и добавьте в него тот же код, что и в классе EnemyLaser. Затем установите отрицательный знак в том месте, где мы установили значение скорости. Это приведет к тому, что лазеры игроков будут двигаться вверх, а не вниз. Лазерный класс игрока теперь должен выглядеть так:

classPlayerLaserextendsEntity{constructor(scene,x,y){super(scene,x,y,"sprLaserPlayer");this.body.velocity.y=-200;}}

Последнее, что нам нужно сделать, чтобы позволить игроку стрелять, - это вернуться к SceneMain.js и добавьте следующее условие под нашим кодом движения:

if(this.keySpace.isDown){this.player.setData("isShooting",true);} else{this.player.setData("timerShootTick",this.player.getData("timerShootDelay")-1);this.player.setData("isShooting",false);}

Мы закончили с добавлением возможности стрелять лазерами как для игрока, так и для врагов!

Шаг двенадцатый. Немного оптимизации

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

Чтобы добавить отбраковку усеченного конуса, нам придется перейти к функции обновления главной сцены. В настоящее время у нас должен быть реализован цикл for, в котором мы обновляем врагов:

for (var i = 0; i < this.enemies.getChildren().length; i++) {var enemy = this.enemies.getChildren()[i];enemy.update();}

После строки enemy.update(), нужно добавить следующий код:

if(enemy.x<-enemy.displayWidth||enemy.x>this.game.config.width+enemy.displayWidth||enemy.y<-enemy.displayHeight*4||enemy.y>this.game.config.height+enemy.displayHeight){    if(enemy){      if(enemy.onDestroy!==undefined){      enemy.onDestroy();      }      enemy.destroy();    }}

Мы также можем добавить то же самое для вражеских лазеров и лазеров игрока:

for(vari=0;i<this.enemyLasers.getChildren().length;i++){varlaser=this.enemyLasers.getChildren()[i];laser.update();if(laser.x<-laser.displayWidth||laser.x>this.game.config.width+laser.displayWidth||laser.y<-laser.displayHeight*4||laser.y>this.game.config.height+laser.displayHeight){if(laser){laser.destroy();}}}for(vari=0;i<this.playerLasers.getChildren().length;i++){varlaser=this.playerLasers.getChildren()[i];laser.update();if(laser.x<-laser.displayWidth||laser.x>this.game.config.width+laser.displayWidth||laser.y<-laser.displayHeight*4||laser.y>this.game.config.height+laser.displayHeight){if(laser){laser.destroy();}}}

Шаг тринадцатый. Столкновения объектов

Чтобы добавить столкновения, мы перейдем к нашему SceneMain.js и взглянем на нашу функцию create. Нам нужно будет добавить то, что называется коллайдером, ниже события появления нашего врага. Коллайдеры позволяют добавить проверку столкновения между двумя игровыми объектами. Таким образом, если происходит столкновение между двумя объектами, будет вызван указанный вами обратный вызов, и вы получите два экземпляра, которые столкнулись в качестве параметров. Мы можем создать коллайдер между лазерами игрока и врагами. В коде мы напишем это так:

this.physics.add.collider(this.playerLasers,this.enemies,function(playerLaser,enemy){});

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

if(enemy){  if(enemy.onDestroy!==undefined){  enemy.onDestroy();  }  enemy.explode(true);  playerLaser.destroy();}

Если мы запустим это, то получим ошибку, так как explode - это не функция. Впрочем, не беспокойтесь, мы можем просто вернуться в Entities.js и посмотреть на класс Entity. В классе Entity нам нужно добавить новую функцию под названием explode. Мы будем принимать canDestroy в качестве единственного параметра этой новой функции. Параметр canDestroy определяет, будет ли при вызове explode объект уничтожен или просто установлен невидимым. Внутри функции explode мы можем добавить:

explode(canDestroy){  if(!this.getData("isDead")){    //устанавливаем анимацию взрыва для текстуры    this.setTexture("sprExplosion");//это относится к тому же ключу анимации, котрый мы добавляли в this.anims.create ранее        this.play("sprExplosion");//запускаем анимацию    //использовать случайный звук взрыва который мы определиливthis.sfxвSceneMain    this.scene.sfx.explosions[Phaser.Math.Between(0,this.scene.sfx.explosions.length-1)].play();    if(this.shootTimer!==undefined){      if(this.shootTimer){      this.shootTimer.remove(false);      }    }    this.setAngle(0);    this.body.setVelocity(0,0);    this.on('animationcomplete',function(){      if(canDestroy){      this.destroy();      } else{      this.setVisible(false);      }    },this);    this.setData("isDead",true);  }}

Если мы запустим игру, вы можете заметить, что игрок все еще может двигаться и стрелять, даже если корабль игрока взорвется. Мы можем исправить это, обернув проверкой логику движения игрока в SceneMain.js Конечный результат должен выглядеть следующим образом:

if(!this.player.getData("isDead")){  this.player.update();  if(this.keyW.isDown){  this.player.moveUp();  }  elseif(this.keyS.isDown){  this.player.moveDown();  }  if(this.keyA.isDown){  this.player.moveLeft();  }  elseif(this.keyD.isDown){  this.player.moveRight();  }  if(this.keySpace.isDown){  this.player.setData("isShooting",true);  }  else{  this.player.setData("timerShootTick",this.player.getData("timerShootDelay")-1);  this.player.setData("isShooting",false);  }}

На этом основная часть кода уже реализована. Далее останется дописать некоторые штрихи.

Шаг четырнадцатый. Финальные действия

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

Мы начнем с добавления фона прокрутки. Фон прокрутки будет иметь несколько слоев с разной скоростью. Во-первых, давайте перейдем к нашему Entities.js. В нижней части файла мы можем добавить новый класс, прокручивающий фон. Ему не нужно ничего расширять.

class ScrollingBackground {  constructor(scene, key, velocityY) {      }}

Наш конструктор будет принимать сцену, в которой мы создаем прокручивающийся фон, и ключ изображения нашего звездного фона. Сначала мы установим сцену экземпляра из нашего параметра, который мы приняли. Мы также будем хранить ключ в экземпляре прокрутки фона. Добавьте в кнутри конструктора:

this.scene = scene;this.key = key;this.velocityY = velocityY;

Мы будем реализовывать функцию под названием createLayers. Однако прежде чем мы это сделаем, нам еще нужно создать группу внутри нашего конструктора.

this.layers = this.scene.add.group();

Теперь создадим функцию createLayers и добавим внутри следующий код для создания спрайтов из ключа изображения:

for (var i = 0; i < 2; i++) {  var layer = this.scene.add.sprite(0, 0, this.key);  layer.y = (layer.displayHeight * i);  var flipX = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;  var flipY = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;  layer.setScale(flipX * 2, flipY * 2);  layer.setDepth(-5 - (i - 1));  this.scene.physics.world.enableBody(layer, 0);  layer.body.velocity.y = this.velocityY;  this.layers.add(layer);}

Приведенный выше код повторяется через каждый ключ, который мы принимаем. Для каждого ключа мы создаем спрайт с ключом на каждой итерации цикла for. Затем мы добавляем спрайт в нашу группу слоев.

Затем мы применяем нисходящую скорость, при которой каждый слой тем медленнее, чем дальше назад на значении i.

Затем мы можем вызвать createLayers в нижней части нашего конструктора.

this.createLayers();

Теперь мы можем вернуться в SceneMain.js и инициализируйте фон прокрутки. Вставьте следующий код перед созданием this.player и добавьте его после определения this.sfx.

this.backgrounds=[];for(vari=0;i<5;i++){//создание пяти слоев фона  varbg=newScrollingBackground(this,"sprBg0",i*10);  this.backgrounds.push(bg);}

Попробуйте запустить игру, вы должны увидеть звезды позади игрока. Теперь мы можем вернуться в Entities.js и добавьте функцию обновления в класс фона со следующим кодом внутри:

if(this.layers.getChildren()[0].y>0){  for(vari=0;i<this.layers.getChildren().length;i++){  varlayer=this.layers.getChildren()[i];  layer.y=(-layer.displayHeight)+(layer.displayHeight*i);  }}
for (var i = 0; i < this.backgrounds.length; i++) {this.backgrounds[i].update();}

Это завершает добавление нашего фона в игру! Если мы запустим игру, то увидим несколько фоновых слоев, прокручивающихся вниз с разной скоростью.

Мы можем закончить, добавив наше главное меню и экран GameOver. Перейдите к SceneMainMenu и удалите строку, которая начинается с SceneMain. Однако прежде чем мы продолжим, мы должны создать объект звукового эффекта для SceneMainMenu. Добавьте следующее в самую верхнюю часть функции create:

this.sfx={btnOver:this.sound.add("sndBtnOver"),btnDown:this.sound.add("sndBtnDown")};

Затем мы можем добавить кнопку воспроизведения в функцию создания, добавив спрайт.

this.btnPlay=this.add.sprite(this.game.config.width*0.5,this.game.config.height*0.5,"sprBtnPlay");

Чтобы запустить SceneMain, нам нужно сначала установить наш спрайт как интерактивный. Добавьте следующее непосредственно ниже, где мы определили this.btnPlay:

this.btnPlay.setInteractive();

Поскольку мы настроили наш спрайт как интерактивный, теперь мы можем добавлять указатели на события, такие как over, out, down и up. Мы можем выполнить код, когда каждое из этих событий запускается мышью или нажатием клавиши. Первое событие, которое мы добавим, - это pointerover. Мы изменим текстуру кнопки на наше изображение sprBtnPlayHover.png, когда указатель находится поверх кнопки. Добавьте следующее после того, как мы установили нашу кнопку как интерактивную:

this.btnPlay.on("pointerover",function(){this.btnPlay.setTexture("sprBtnPlayHover");//установка текстуры для кнопкиthis.sfx.btnOver.play();//проигрывание звука при наведении на кнопку},this);

Теперь мы можем добавить событие pointerout. В этом случае мы сбросим текстуру обратно к обычному изображению кнопки. Добавьте следующее в разделе где мы определяем указатель на событие:

this.btnPlay.on("pointerout", function() {  this.setTexture("sprBtnPlay");});

Если мы снова запустим игру и наведем курсор мыши на кнопку, а затем уведем его, то увидим, что текстура кнопки сброшена на изображение по умолчанию.

Далее мы можем добавить событие pointerdown. Здесь мы изменим текстуру кнопки запуска на sprBtnPlayDown.png.

this.btnPlay.on("pointerdown",function(){this.btnPlay.setTexture("sprBtnPlayDown");this.sfx.btnDown.play();},this);

Затем мы можем добавить событие pointerup для сброса текстуры кнопки после нажатия.

this.btnPlay.on("pointerup",function(){this.setTexture("sprBtnPlay");},this);

Мы можем дополнить логику внутри нашего события pointerup, чтобы начать основную сцену. Окончательное событие pointerup должно выглядеть следующим образом:

this.btnPlay.on("pointerup",function(){this.btnPlay.setTexture("sprBtnPlay");this.scene.start("SceneMain");},this);

Когда мы запускаем игру и нажимаем кнопку воспроизведения, теперь она должна начать главную сцену!

Теперь есть только пара вещей, которые мы можем сделать, чтобы закончить наше главное меню. Первое - это добавление заголовка. Чтобы добавить заголовок, мы можем создать текст. Добавьте следующее под событием pointerup:

this.title=this.add.text(this.game.config.width*0.5,128,"SPACESHOOTER",{fontFamily:'monospace',fontSize:48,fontStyle:'bold',color:'#ffffff',align:'center'});

Чтобы центрировать заголовок, мы можем установить начало текста на половину ширины и половину высоты. Мы можем сделать это, написав следующее под определением title:

this.title.setOrigin(0.5);

На этом статья заканчивается. Это моя первая статья на Хабре и я старался максимально точно донести смысл оригинала. Если вы заметите где-то неточности или ошибки, напишите в комментариях и мы это обсудим.

Ссылка на оригинал статьи

Ссылка на исходники оригинала

Приведенный выше код позволяет фоновым слоям вернутся обратно. Если бы у нас не было этого кода, фон просто исчез бы с экрана, и остался бы черный фон. Мы можем вернуться к SceneMain.js. В функции обновления SceneMain добавьте следующий код:

Подробнее..

Скрываем номера курьеров и клиентов с помощью key-value хранилища

17.06.2021 18:06:38 | Автор: admin

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

Каждый сервис использует свои решения для маскировки номеров клиентов и курьеров. В данной статье я расскажу, как сделать это с помощью key-value хранилища в Voximplant.

Как это будет работать

Мы создадим сценарий, который позволит курьеру и клиенту созваниваться, не зная при этом личные номера телефонов друг друга.

У нас будет только один нейтральный номер, на который будут звонить и клиент, и курьер. Номер мы арендуем в панели Voximplant. Затем создадим некую структуру данных, где клиент и курьер будут связаны между собой номером заказа (то есть ключом в терминологии key-value storage).

Так при звонке на арендованный номер звонящий введёт номер заказа, и если такой заказ есть в базе, наш сценарий проверит номера телефонов, привязанные к нему. Далее если номер звонящего будет идентифицирован как номер клиента, произойдет соединение с курьером, ответственным за заказ, и наоборот.

Например, звонок курьера клиенту будет выглядеть следующим образом:

Если номер телефона звонящего не будет найден в базе, ему предложат перезвонить с того номера, который использовался при оформлении заказа, или переключиться на оператора.

Перейдем непосредственно к реализации.

Вам понадобятся

1) Чтобы начать разработку, войдите в свой аккаунт: manage.voximplant.com/auth. В меню слева нажмите Приложения, затем Создать приложение в правом верхнем углу. Дайте ему имя, например, numberMasking и снова кликните Создать.

2) Зайдите в новое приложение, переключитесь на вкладку Сценарии и создайте сценарий, нажав на +. Назовём его kvs-scenario. Здесь мы будем писать код, но об этом чуть позже.

3) Сначала перейдем во вкладку Роутинг и создадим правило для нашего сценария. Маску (регулярное выражение) оставим .* по умолчанию, так правило будет срабатывать для всех номеров.

4) Далее арендуем реальный городской номер. Для этого перейдем в раздел Номера, выберем и оплатим номер. На него будут звонить и клиент, и курьер, и он будет отображаться вместо их настоящих номеров.

В Voximplant вы можете приобретать в том числе тестовые номера, которые удобно использовать при знакомстве с платформой, но в нашем случае потребуется реальный для совершения исходящего звонка с платформы.

5) Осталось привязать его к нашему приложению. Заходим в приложение, открываем вкладку Номера Доступные и нажимаем Прикрепить. В открывшемся окне можно также прикрепить наше правило, тогда оно будет автоматически назначено для входящих вызовов, а все остальные правила будут проигнорированы.

6) Далее необходимо верифицировать аккаунт, чтобы использовать этот номер для звонков.

Отлично, структура готова, осталось заполнить key-value хранилище и добавить код в сценарий.

Key-value хранилище

Чтобы сценарий заработал, нужно положить что-то в хранилище. Это можно сделать, воспользовавшись Voximplant Management API. Я буду использовать Python API client, он работает с Python 2.x или 3.x с установленным pip и setuptools> = 18.5.

1) Зайдем в папку проекта и установим SDK, используя pip:

python -m pip install --user voximplant-apiclient

2) Создадим файл с расширением .py и добавим в него код, при выполнении которого данные о заказе попадут в key-value хранилище. Применим метод set_key_value_item:

from voximplant.apiclient import VoximplantAPI, VoximplantExceptionif __name__ == "__main__":    voxapi = VoximplantAPI("credentials.json")        # SetKeyValueItem example.    KEY = 12345    VALUE = '{"courier": "79991111111", "client": "79992222222"}'    APPLICATION_ID = 1    TTL = 864000        try:        res = voxapi.set_key_value_item(KEY,            VALUE,            APPLICATION_ID,            ttl=TTL)        print(res)    except VoximplantException as e:        print("Error: {}".format(e.message))

Файл с необходимыми credentials вы сможете сгенерировать при создании сервисного аккаунта в разделе Служебные аккаунты в настройках панели.

APPLICATION_ID появится в адресной строке при переходе в ваше приложение.

В качестве ключа (KEY) будет использоваться пятизначный номер заказа, а в качестве значений телефонные номера: courier номер курьера, client номер клиента. TTL нам здесь необходимо для указания срока хранения значений.

3) Осталось запустить файл, чтобы сохранить данные заказа:

python3 kvs.py

Если мы больше не захотим, чтобы клиент и курьер беспокоили друг друга, можно будет удалить их данные из хранилища. Информацию о всех доступных методах key-value storage вы найдёте в нашей документации: management API и VoxEngine.

Код сценария

Код, который необходимо вставить в сценарий kvs-scenario, представлен ниже, его можно смело копировать as is:

Полный код сценария
require(Modules.ApplicationStorage);/** * @param {boolean} repeatAskForInput - была ли просьба ввода произнесена повторно * @param longInputTimerId - таймер на отсутствие ввода * @param shortInputTimerId - таймер на срабатывание фразы для связи с оператором * @param {boolean} firstTimeout - индикатор срабатывания первого таймаута * @param {boolean} wrongPhone - индикатор совпадения номера звонящего с номером, полученным из хранилища * @param {boolean} inputRecieved - получен ли ввод от пользователя *  */let repeatAskForInput;let longInputTimerId;let shortInputTimerId;let firstTimeout = true;let wrongPhone;let inputRecieved;const store = {    call: null,    caller: '',    callee: '',    callid: '74990000000',    operator_call: null,    operatorNumber: '',    input: '',    data: {        call_operator: '',        order_number: '',        order_search: '',        phone_search: '',        sub_status: '',        sub_available: '',        need_operator: '',        call_record: ''    }}const phrases = {    start: 'Здр+авствуйтте. Пожалуйста, -- введите пятизначный номер заказa в тт+ооновом режиме.',    repeat: 'Пожалуйста , , - - введите пятизначный номер заказа в т+оновом режиме,, или нажмите решетку для соединения со специалистом',    noInputGoodbye: 'Вы - ничего не выбрали. Вы можете посмотреть номер заказа в смс-сообщении и позвонить нам снова. Всего д+обровоо до свидания.',    connectToOpearator: 'Для соединения со специалистом,, нажмите решетку',    connectingToOpearator: 'Ожидайте, соединяю со специалистом',    operatorUnavailable: 'К сожалению,, все операторы заняты. Пожалуйста,,, перезвоните позднее. Всего д+обровоо до свидания.',    wrongOrder: 'Номер заказа не найден. Посмотрите номер заказа в смс-сообщении и введите его в т+оновом режиме. Или свяжитесь со специалистом,, нажав клавишу решетка.',    wrongOrderGoodbye: 'Вы ничего не выбрали, всего д+обровоо до свидания.',    wrongPhone: 'Номер телефона не найден. Если вы кли+ент, перезвоните с номера, который использовали для оформления заказа. Если вы курьер, перезвоните с номера, который зарегистрирован в нашей системе. Или свяжитесь со специалистом,,- нажав клавишу решетка.',    wrongPhoneGoodbye: 'Вы ничего не выбрали. Всего доброго, до свидания!',    courierIsCalling: `Вам звонит курьер по поводу доставки вашего заказа, - - ${store.data.order_number}`,    clientIsCalling: `Вам звонит клиент по поводу доставки заказа, - - ${store.data.order_number} `,    courierUnavailable: 'Похоже,,, курь+ер недоступен. Пожалуйста,,, перезвоните через п+ару мин+ут. Всего д+обровоо до свидания.',    clientUnavailable: 'Похоже,,, абонент недоступен. Пожалуйста,,, перезвоните через пп+ару мин+ут. Всего д+обровоо до свидания.',    waitForCourier: 'Ожидайте на линии,, - соединяю с курьером.',    waitForClient: 'Ожидайте на линии,, соединяю с клиентом.'}VoxEngine.addEventListener(AppEvents.Started, async e => {    VoxEngine.addEventListener(AppEvents.CallAlerting, callAlertingHandler);})async function callAlertingHandler(e) {    store.call = e.call;    store.caller = e.callerid;    store.call.addEventListener(CallEvents.Connected, callConnectedHandler);    store.call.addEventListener(CallEvents.Disconnected, callDisconnectedHandler);    store.call.answer();}async function callDisconnectedHandler(e) {    await sendResultToDb();    VoxEngine.terminate();}async function callConnectedHandler() {    store.call.handleTones(true);    store.call.addEventListener(CallEvents.RecordStarted, (e) => {        store.data.call_record = e.url;    });    store.call.record();    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.start);    addInputTimeouts();}function dtmfHandler(e) {    clearInputTimeouts();    store.input += e.tone;    Logger.write('Введена цифра ' + e.tone)    Logger.write('Полный код ' + store.input)    if (e.tone === '#') {        store.data.need_operator = "Да";        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        callOperator();        return;    }    if (!wrongPhone) {        if (store.input.length >= 5) {            repeatAskForInput = true;            Logger.write(`Получен код ${store.input}. `);            store.call.handleTones(false);            store.call.removeEventListener(CallEvents.ToneReceived);            handleInput(store.input);            return;        }    }    addInputTimeouts();}function addInputTimeouts() {    clearInputTimeouts();    if (firstTimeout) {        Logger.write('Запущен таймер на срабатывание фразы для связи с оператором');        shortInputTimerId = setTimeout(async () => {            await say(phrases.connectToOpearator);        }, 1500);        firstTimeout = false;    }    longInputTimerId = setTimeout(async () => {        Logger.write('Сработал таймер на отсутствие ввода от пользователя ' + longInputTimerId);        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        if (store.input) {            handleInput(store.input);            return;        }        if (!repeatAskForInput) {            Logger.write('Просим пользователя повторно ввести код');            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.repeat);            addInputTimeouts();            repeatAskForInput = true;        } else {            Logger.write('Код не введен. Завершаем звонок.');            await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);            store.call.hangup();        }    }, 8000);    Logger.write('Запущен таймер на отсутствие ввода от пользователя ' + longInputTimerId);}function clearInputTimeouts() {    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    if (longInputTimerId) clearTimeout(longInputTimerId);    if (shortInputTimerId) clearTimeout(shortInputTimerId);}async function handleInput() {    store.data.order_number = store.input;    Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)    inputRecieved = true;    let kvsAnswer = await ApplicationStorage.get(store.input);    if (kvsAnswer) {        store.data.order_search = 'Заказ найден';        Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)        let { courier, client } = JSON.parse(kvsAnswer.value);        if (store.caller == courier) {            Logger.write('Звонит курьер')            store.callee = client;            store.data.sub_status = 'Курьер';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else if (store.caller == client) {            Logger.write('Звонит клиент')            store.callee = courier;            store.data.sub_status = 'Клиент';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else {            Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');            wrongPhone = true;            store.data.phone_search = 'Телефон не найден';            store.input = '';            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.wrongPhone);            addInputTimeouts();        }    } else {        Logger.write('Совпадение в kvs по введенному коду не найдено');        store.data.order_search = 'Заказ не найден';        store.input = '';        store.call.handleTones(true);        store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);        await say(phrases.wrongOrder);        Logger.write(`Очищаем таймер ${longInputTimerId}. `);        addInputTimeouts();    }}async function callCourierOrClient() {    clearInputTimeouts();    Logger.write('Начинаем звонок курьеру/клиенту');    await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);    const secondCall = VoxEngine.callPSTN(store.callee, store.callid);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    secondCall.addEventListener(CallEvents.Connected, async () => {        store.data.sub_available = 'Да';        await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);        store.call.stopPlayback();        VoxEngine.sendMediaBetween(store.call, secondCall);    });    secondCall.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    secondCall.addEventListener(CallEvents.Failed, async () => {        store.data.sub_available = 'Нет';        store.call.stopPlayback();        await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);        store.call.hangup();    });}async function callOperator() {    Logger.write('Начинаем звонок оператору');    await say(phrases.connectingToOpearator, store.call);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    store.operator_call = VoxEngine.callPSTN(store.operatorNumber, store.callid);    store.operator_call.addEventListener(CallEvents.Connected, async () => {        store.data.call_operator = 'Оператор свободен';        VoxEngine.sendMediaBetween(store.call, store.operator_call);    });    store.operator_call.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    store.operator_call.addEventListener(CallEvents.Failed, async () => {        store.data.call_operator = 'Оператор занят';        await say(phrases.operatorUnavailable, store.call);        store.call.hangup();    });}async function sendResultToDb() {    Logger.write('Данные для отправки в БД');    Logger.write(JSON.stringify(store.data));    const options = new Net.HttpRequestOptions();    options.headers = ['Content-Type: application/json'];    options.method = 'POST';    options.postData = JSON.stringify(store.data);    await Net.httpRequestAsync('https://voximplant.com/', options);}function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

Код тщательно прокомментирован, но в некоторые моменты углубимся подробнее.

Вводим номер заказа

Первое, что мы делаем при звонке просим звонящего ввести номер заказа и обрабатываем введенное значение с помощью функции dtmfHandler.

store.input += e.tone;

Если звонящий ввел #, сразу соединяем его с оператором:

if (e.tone === '#') {    store.data.need_operator = "Да";    store.call.removeEventListener(CallEvents.ToneReceived);    store.call.handleTones(false);    callOperator();    return;}

Если он ввел последовательность из 5 цифр, вызываем функцию handleInput:

if (store.input.length >= 5) {    repeatAskForInput = true;    Logger.write('Получен код ${store.input}. ');    store.call.handleTones(false);    store.call.removeEventListener(CallEvents.ToneReceived);    handleInput(store.input);    return;}

Ищем заказ в хранилище

Здесь мы будем сравнивать введенный номер заказа с номером в хранилище, используя метод ApplicationStorage.get(), в качестве ключа используем введенную последовательность:

store.data.order_number = store.input;Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)inputRecieved = true;let kvsAnswer = await ApplicationStorage.get(store.input);

Если заказ найден, получаем для него номера клиента и курьера:

if (kvsAnswer) {    store.data.order_search = 'Заказ найден';    Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)    let { courier, client } = JSON.parse(kvsAnswer.value);

Теперь осталось разобраться, кому звонить. Если номер звонящего принадлежит курьеру, будем выполнять переадресацию на клиента, если клиенту на курьера. В этом нам поможет функция callCourierOrClient:

if (store.caller == courier) {    Logger.write('Звонит курьер')    store.callee = client;    store.data.sub_status = 'Курьер';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();} else if (store.caller == client) {    Logger.write('Звонит клиент')    store.callee = courier;    store.data.sub_status = 'Клиент';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();}

Если номера нет в хранилище, просим перезвонить с другого номера, который указывался при оформлении заказа:

else {    Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');    wrongPhone = true;    store.data.phone_search = 'Телефон не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongPhone);    addInputTimeouts();}

И наконец, обрабатываем вариант, когда номер заказа не был найден в базе. В этом случае просим попробовать ввести его снова, предварительно удостоверившись, что номер верный:

else {    Logger.write('Совпадение в kvs по введенному коду не найдено');    store.data.order_search = 'Заказ не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongOrder);    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    addInputTimeouts();}

Звоним клиенту/курьеру

Переходим непосредственно к звонку клиенту/курьеру, то есть к логике функции callCourierOrClient. Здесь мы сообщим звонящему, что переводим его звонок на курьера/клиента, и включим музыку на ожидание. С помощью метода callPSTN позвоним клиенту или курьеру (в зависимости от того, чей номер был ранее идентифицирован как номер звонящего):

await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);const secondCall = VoxEngine.callPSTN(store.callee, store.callid);store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');

В этот же момент сообщим второй стороне о том, что звонок касается уточнения информации по заказу:

secondCall.addEventListener(CallEvents.Connected, async () => {    store.data.sub_available = 'Да';    await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);    store.call.stopPlayback();    VoxEngine.sendMediaBetween(store.call, secondCall);});

Обработаем событие дисконнекта:

secondCall.addEventListener(CallEvents.Disconnected, () => {    store.call.hangup();});

И оповестим звонящего, если вторая сторона недоступна:

secondCall.addEventListener(CallEvents.Failed, async () => {    store.data.sub_available = 'Нет';    store.call.stopPlayback();    await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);    store.call.hangup();});

За все фразы, который произносит робот, отвечает функция say, а сами фразы перечислены в ассоциативном массиве phrases. В качестве TTS провайдера мы используем Yandex, голос Alena:

function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

Кроме всего прочего, наш сценарий записывает звонки, используя метод record, и показывает, как можно сохранить статистику в базу данных (в нашем коде за это отвечает функция sendResultToDb). Это очень важно для бизнеса, поскольку позволяет анализировать статистику, обеспечивать контроль качества и оперативно решать спорные ситуации, которые могли возникнуть в процессе доставки заказа.

Тестируем

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

Позвоним с телефона клиента или курьера на номер, арендованный в панели. Затем введем номер заказа (в нашем случае это 12345) и будем ждать соединения со второй стороной.

Если все сделано верно, клиент и курьер смогут созваниваться и обсуждать детали заказа, не зная настоящих номеров друг друга, а значит, не нарушая прайваси. Круто, не так ли?) Желаем вам успешной разработки и беспроблемной доставки!

P.S. Также мой коллега недавно рассказал, как обезопасить общение клиента и курьера с помощью Voximplant Kit (наш low-code/no-code продукт). Если эта тема вас заинтересовала, переходите по ссылке :)

Подробнее..

Как я попал на стажировку в Яндекс

18.06.2021 00:22:38 | Автор: admin

Всем привет, меня зовут Виктор и я frontend разработчик. Хочу поделиться тем, как я решил стать программистом и попал на стажировку в компанию Яндекс в 27 лет без высшего образования.

Сначала моя история о том, как я заинтересовался it сферой и в частности web разработкой.

Знакомство с кодом

Впервые с программным кодом я познакомился на третьем курсе института по специальности прикладная лингвистика. Это был язык программирования python и мне очень понравилось то, как я могу заставить компьютер делать разные классные штуки с помощью циклов, условных конструкций, функций и переменных. Преподаватель был современный и за основу учебной программы взял курс по основам программирования с учебной платформы udacity. В процессе нужно было написать простенький web crawler и небольшой поисковый движок для ранжирования веб страниц по количеству ссылок на них с других веб страниц. По определённым обстоятельствам я вынужден был уйти из вуза после третьего курса и стал зарабатывать на жизнь переводами.

Глубже в технологии

Тем временем я понял, что мне также интересна it сфера и я начал думать, как бы мне продолжить её осваивать и что выучить в первую очередь. Уже не помню почему, но выбор пал на курсеровский курс HTML, CSS and JavaScript Гонконгского университета. Так я впервые познакомился с языком гипертекстовой разметки, каскадными таблицами стилей и языком программирования javaScript. Мне показалось интересным манипулировать различными объектами на web странице, менять стили, расположение элементов и добавлять разную интерактивность. Затем было много разных других курсов с курсеры, edx, степика, главным курсом первого времени был наверное знамений курс Гарвардского университета cs50 на котором я впервые познакомился с алгоритмами и структурами данных и языком си. Это был очень непростой, но интересный курс по основам computer science.

Первые попытки заработать на разработке

Долгое время программирование было для меня эдаким хобби и я несильно видел себя в коммерческой разработке, я проходил курсы, делал разные учебные и личные проекты, изучал разные языки программирования (Haskell, java, golang, scheme etc) и парадигмы, в общем всё это было больше в интерес. В какой-то момент я решил попробовать сделать пару заказов на фрилансе, связанных с веб разработкой и мне удалось заработать первые деньги. После этого я начал помимо переводческой деятельности подрабатывать и веб разработкой, делал простые вещи: правки в вёрстке, подвязка сайта к crm через ajax, калькуляторы стоимостей услуг и товаров и всё в таком духе.

Смена профессии

После этого я начал задумываться о том, что нужно сменить таки профессию и стать полноценным разработчиком, специализироваться я решил на javaScript по причине его гибкости и возможности программировать во всех возможных парадигмах, а также его присутствию, как на клиенте, так и на сервере. Нужно было освоить какой-нибудь фреймворк, научиться тестировать код, верстать адаптивно, кроссбраузерно, валидно и семантично, освоить препроцессор для css, node js, typescript, webpack. Если до этого моё изучение программирования было в большей степени академичным, то теперь я решил взяться за дело всерьёз. Выбор пал на react за счёт того, что это javaScript first библиотека с минимумом магии и функциональным подход в построении интерфейсов ui, как чистая функция от состояния и свойств, переданных компоненту. На данный момент я также знаю на базовом уровне vue js, который меня восхитил своей магией и скоростью разработки, но опечалил отладкой и поиском ошибок, когда что-то ломается в шаблоне.

Хочу в крутую компанию!

Теперь перейду уже к стажировке. Я понимал, что в 25+ устроиться без опыта работы в крутую it компанию будет довольно непросто и уже пытался до этого попасть в школу программистов Хэдхантера и курсы от Тинькофф банка, после которых можно было попасть в штат, но там были очень сложные алгоритмические задачи, с которыми я не мог справиться полностью, обычно решая половину задач или меньше. В мэйл дорога была заказана сразу, потому что на свои стажировки они берут только выпускников своих образовательных программ. Про Яндекс я почему-то всё это время даже не думал, потому что считал, что там всё будет ещё гораздо страшнее. Но месяц назад в телеграм канале одного хорошего ютубера (S0ER) я наткнулся на пост о стажировке в Яндекс и подумал ну а почему бы и не попробовать, я вообще ни на что не расcчитывал и заполнил анкету указав честно все свои скромные достижения в виде одной курсовой, пачки сертификатов и резюме с указанием технологий, которые я освоил на тот момент.

Письмо счастья

В ответ мне пришла ссылка на контест. Я не буду говорить о том какие там были задания, но я был приятно удивлён тому, что на алгоритмы там была ровно одна задача из четырёх и именно её я завалил, моё решение прошло только половину тестов, из остальных там было задание на вёрстку, оно было довольно жёстким, нужно было pixel perfect сверстать определённый рисунок, состоящий из геометрических фигур без использования svg и готовых картинок, только html и css. Две оставшиеся задачи проверяли базовые вещи для javaScript разработчика: асинхронность, контекст вызова, прототипы, замыкания. Надо отметить, что каким-то образом я умудрился все три этих задания сдать с первой попытки в контесте. На всё это было дано 6 часов.

На следующий день со мной связался рекрутёр Яндекса с предложением пройти 2 интервью в зуме, одно из них на общие навыки программирования и javaScript, другое на алгоритмы.

На первом интервью меня попросили рассказать о том, как пришёл в разработку, спросили про навыки из резюме (правда ли я знаю вебпак и тайпскрипт, показал на гитхабе игру крестики нолики, которую я как раз недавно сделал, написанную на тайпскрипте. Спросили про опыт разработки и мои проекты. Дальше началось само интервью. Сначала мне были предложены куски кода с предложением найти ошибки и объяснить, что в них работает не правильно и почему. Затем были задания на написание кода. Работа со стандартными методами массивов, объектов, в общем на знание языка и понимание его особенностей. В конце была ещё алгоритмическая задачка (задачи на алгоритмы должны были быть на следующем собеседовании) потому что как оказалось, я решил основную часть раньше установленного срока. После предыдущих задач я немного расслабился и сначала встал в ступор, но после небольшой наводки от собеседующего смог решить и её.

На втором собеседовании проверялось понимание сложности алгоритмов, основных структур данных и умение с нуля построить рабочее решение. Я смог решить две задачи. Никаких красно чёрных деревьев там не было (даже обычных деревьев не было), всё обошлось словарём, массивами и пониманием того, как работают стек и очередь. Багов я не избежал, но смог их отловить.

После этого мне на следующий день позвонил рекрутёр и предложил пройти финальные собеседования с командами.

Это были команды картинок, лавки и маркета.

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

Спасибо за внимание, надеюсь, что это было кому-то полезно. Задавайте вопросы в комментариях.

Подробнее..

Темизация. История, причины, реализация

18.06.2021 22:18:25 | Автор: admin

Введение. Причины появления

Когда веб только зарождался единственной его целью было размещение контента (гипертекстовые страницы), чтобы у пользователей из всемирной паутины был к нему доступ. В то время не могло идти и речи о дизайне, ведь зачем нужен дизайн страницам с научными публикациями, разве они станут от этого полезнее (первый сайт). Времена меняются и сегодня во всемирной паутине далеко не только научные публикации. Блоги, сервисы, социальные сети и многое, многое другое. Каждый сайт нуждается в своей индивидуальности, ему необходимо заинтересовывать и привлекать пользователей. Даже научные сайты постепенно это понимают, ведь большинство ученых хотят не просто изучать те или иные аспекты, а доносить их до людей, тем самым повышая свою популярность и ценность своих исследований (пример 15 из 15 научных сайтов списка сделали редизайн в последние 6 лет). Рядовым обывателям не интересен серый сайт с непонятным содержанием. Наука становится доступнее, а сайты преобразуются в приложения с удобным и приятным интерфейсом.

Так как "удобство" у каждого свое нет четкого определения и конкретных правил по реализации удобного для всех сервиса. В последние годы к этому понятию стали привязывать такое понятие, как Темизация. Именно о нем я и хочу рассказать в данной статье.

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

Темная тема для ночного периода это не единственная причина добавления темизации на сайт. Другой важной задачей стоит доступность сервиса. Во все мире 285 млн людей с полной или частичной потерей зрения, в России 218т [ист.], до 2,2 млрд с различными дефектами [ист.] почти треть детей в России заканчивает школу в очках[ист.]. Статистика поражает воображение. Однако, большинство людей не лишено зрения полностью, а имеют лишь небольшие отклонения. Это могут быть цветовые отклонения или качественные. Если для качественных отклонений доступность достигается за счет добавления поддержки разных размеров шрифтов, то для цветовых отличным решением является именно добавление темы.

История развития. Бесконечный путь

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

Дизайн только одна из составляющих каждого сайта. Во время разработки сайт проходит через множество людей разработчиков, аналитиков, тестировщиков, маркетологов. Темизация так или иначе затрагивает каждого из них. И, в первую очередь, конечно же, разработчиков.

Добавление темизации в проект может быть крайне простой задачей, если эта задача ставится на этапах планирования проекта. Несмотря на то, что она стала популярна только в последние годы, сама эта технология совсем не нова. Этот процесс, как и многие другие отлаживался и активно развивался с каждым годом последние 5-10 лет. Сегодня даже страшно представить, как это делали первопроходцы. Нужно было поменять всем элементам классы, оптимизировать это через наследование цветов, обновлять почти весь ДОМ. А это все во временя такого монстра, как IE, снящегося в худших кошмарах бывалым разработчикам, и до появления ES6. Сейчас же, все эти проблемы уже далеки от разработчиков. Многие невероятно трудные процессы под влиянием времени постепенно уходят в былое, оставляя будущим поколениям разработчиков память о тех ужасных временах и прекрасные решения, доведенные во многом до идеала.

JS один из самых динамично развивающихся языков программирования, но в вебе развивается далеко не только он. Добавляются новые возможности и устраняются старые проблемы в таких технологиях, как HTML и CSS. Это, конечно же, невозможно без обновления и самих браузеров. Развитие и популяризация современных браузеров скидывают большой груз с плеч программистов. На этом все эти технологии не останавливаются и уверен, что через годы, о них будут отзываться также, как программисты сейчас отзываются об IE. Все эти обновления дают нам не только упрощение разработки и повышение ее удобства, но и добавляет ряд новых возможностей. Одной из таких возможностей стали переменные в css, впервые появившиеся в браузерах в 2015 году. 2015 год во многом получился знаменательным для веба это исторически важное обновления JS, утверждения стандарта HTTP/2, появление WebAssembly, популяризация минимализма в дизайне и появление ReactJS. Эти и многие другие нововведения нацелены на ускорение сайта, упрощение разработки и повышение удобства взаимодействия с интерфейсом.

Немного из истории css-переменных:

Первое упоминание переменных, которое мне удалось откопать, числится за 2012 годом. В апреле сего года в документации появилось описание новой концепции для css переменные.

Был описан весьма интересный способ создания и использования переменных:

:root {  var-header-color: #06c;}h1 { background-color: var(header-color); }

Однако, до появления этой функциональности в браузерах, должно было пройти значительное время на продумывание и отладку. Так, впервые поддержка css-переменных была добавлена в firefox лишь в 2015 году. Затем, в 2016, к нему присоединились google и safari.

Итоговая реализация отличалась от первоначальной идеи и выглядела следующим, уже привычным нам, образом:

:root {  --header-color: #06c;}h1 { background-color: var(--header-color); }

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

Также, интересно проследить цели добавления переменных. Судя по первым версиям спецификации таковыми были возможность избавиться от дублирования констант и улучшение возможностей для разработки адаптивности. В 2015 году появился пример с использованием для интернационализации. О темизации в том далеком и важном для фронтенда году, речи практически не шло тренд на нее появился лишь в последние несколько лет.

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

В параллель спецификации Css развиваются также его пре и постпроцессоры. Их развитие было значительно быстрее, так как им не нужно было описывать спецификацию и продвигать ее во все браузеры. Одним из первых препроцессоров был stylus, созданный в далеком 2011, позднее были созданы sass и less. Они дают ряд преимуществ и возможностей, за счет того, что все сложные функции и модификации во время сборки конвертируются в css. Одной из таких возможностей являются переменные. Но это уже совершенно иные переменные, больше похожие на js, нежели css. В сочетании с миксинами и js можно было настроить темизацию.

Прошло уже 10 лет с появления препроцессора, гигантский отрезок по меркам веба. Произошло множество изменений и дополнений. HTML5, ES6,7,8,9,10. JS обзавелся целым рядом библиотек, отстроив вокруг себя невообразимый по масштабам зверинец. Некоторые из этих библиотек стали стандартом современного веба react, vue и angular, заменив привычный разработчикам HTML на свои альтернативы, основанные на js. JS заменяет и css, сделав такую замечательную технологию, как css in js, дающую те же возможности, но только динамичнее и в привычном формате (порою большой ценой, но это уже совсем другая история). JS захватил веб, а затем перешел на захват всего мира.

Темы нужны современному миру и раз уж это так, значит дизайнером нужно знать, как их спроектировать, а разработчикам как это реализовать. Самих способов реализации, как уже было описано немало. Не меньше тонкостей и потенциальных проблем, которые могут появляться в ходе развития этой возможности.

Проектирование дизайна

Как уже говорилось, гораздо лучше, если идея добавления тем появилась на зарождении проекта. Можно сразу заложить фундамент и продолжать с его учетом. Ведь это явно проще, чем закладывать фундамент после постройки дома. Хотя, справедливости ради, стоит отметить, что если дома проектировались как модульные, с учетом расширения и передвижений то это будет возможно и без дополнительных усилий.

Так как тема это элемент интерфейса часть работ по планированию возьмут на себя дизайнеры. Подходы к разработке дизайн-систем не стоят на месте. Если раньше дизайн сайта разрабатывали в программах, подобных фотошопу (хотя есть отдельные личности, которые занимаются подобным и сейчас, доводя разработчиков до состояния истинного ужаса). У них была масса минусов, особенно во времена медленных компьютеров и больших идей клиентов. Конечно же, эти программы не канут в лету, они будут использоваться по их основному назначению обработка фотографий, рисование иллюстраций. Их роль получают современные альтернативы, предназначенные в первую очередь для веба Avocode, Zeplin, Figma, Sketch. Удобно, когда основные инструменты, используемые программистом предназначены именно для целей разработки. В таких случаях, развитие инструментов идет в ногу с развитием сфер, для которых они предназначены. Эти инструменты являются отличным тому подтверждением. Когда они появились в них можно было копировать css стили, делать сетки, проверять margin-ы и padding-и. Не прямоугольниками и даже не линейкой, а просто движением мыши. Затем появились переменные, после в мир веба пришел компонентный подход и этот подход появился в данных инструментах. Они следят за тенденциями, делая те или иные утилиты, добавляют наборы инструментов и не останавливаются на всем этом, чудесным образом поспевая за этой, разогнавшейся до невероятных скоростей, машиной.

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

Темизация может выходить и за рамки страницы сайта. Одной из таких возможностей является цвет строки состояния и вкладки в некоторых мобильных браузерах. Для этих элементов также стоит продумать оттенки.

Цветовая гамма

Просматривая дизайн нового проекта, часто можно заметить странный, но весьма популярный способ именования цветов blue200. Конечно же, за подобное можно сказать спасибо дизайнеру, ведь это тоже верный подход, однако для иных целей. Такой способ хорошо подходит, если разработчики будут использовать атомарный css, ставшим в последние годы самым интересным и приятным для разработчиков, но все еще значительно отстающим по использованию от БЭМ-а [ист.]. Однако, ни такой способ именования переменных, ни атомарный css не годятся для сайтов, которые проектируются с учетом темизации. Причин тому много, одна из них заключается в том, что blue200 это всегда светло-синий цвет и для того, чтобы цвет у всех светло-синих кнопок стал темно-синим нужно у всех кнопок поменять его на blue800. Значительно более верным вариантом будет назвать цвет primary-color, потому что такое имя может быть как blue200, так и blue800, но всем участникам разработки будет понятно, что эта переменная означает основной цвет сайта.

colors: {  body: '#ECEFF1',  antiBody: '#263238',  shared: {    primary: '#1565C0',    secondary: '#EF6C00',    error: '#C62828',    default: '#9E9E9E',    disabled: '#E0E0E0',  },},

Для текста можно использовать схему подобную кнопкам (основной, второстепенный, по умолчанию, для отключенных элементов), а можно использовать уровни цвета:

colors: {  ...  text: {    lvl1: '#263238',    lvl3: '#546E7A',    lvl5: '#78909C',    lvl7: '#B0BEC5',    lvl9: '#ECEFF1',  },},

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

Примеры названия переменных:

shared-primary-color,

text-lvl1-color.

Конечно же, подобный способ именования переменных не может быть абсолютно универсальным, однако он (при незначительных изменениях) подойдет для большинства случаев.

Теперь, разобравшись с проектированием дизайна в контексте разработки, можно переходить на следующий этап.

Проектирование кода.

Как уже говорилось, на уровне кода есть 3 основных пути проектирования темизации через нативные переменные (с препроцессорами или без), через css in js, через замену файлов стилей. Каждое решение может так или иначе свестись к нативным переменным, но беда заключается в том, что в IE нет их поддержки. Дальше будет описано 2 варианта проектирования темизации с помощью переменных на нативном css и с помощью css in js.

Основные шаги при темизации сайта:

  1. Создание стилей каждой темы (цвета, тени, рамки);

  2. Настройка темы по умолчанию, в зависимости от темы устройства пользователя (в случае с темной и светлой темой);

  3. Настройка манифеста и мета тегов;

  4. Создание стилизованных компонентов;

  5. Настройка смены темы при нажатии на кнопку;

  6. Сохранение выбранной темы на устройстве пользователя.

Третий шаг универсален для любого варианта темизации. Поэтому, сперва кратко о нем.

Манифест это файл, используемый, в первую очередь, для PWA. Однако, его содержимое является альтернативой метатегам и загружается быстрее них. В случае темизации нас интересуют такие ключи, как theme_color и background_color. Мети-теги с этими параметрами также можно добавить в head страницы.

Theme_color цвет темы сайта. Указанный цвет будет использоваться в качестве цвета вкладки и строки состояния на мобильных устройствах на системе Android. У данного функционала крайне скудная поддержка браузеров, однако доля этих браузеров составляет 67%.

caniuse.comcaniuse.com

Background_color цвет, который будет загружаться до загрузки стилей. Поддержка данного атрибута еще более скудная, чем у цвета темы:

caniuse.comcaniuse.com

Переменные

Начать описание этого варианта стоит с поддержки, так как это, пожалуй, его единственный минус.

caniuse.comcaniuse.com

Полное отсутствие поддержки в IE, долгое ее отсутствие в популярных браузерах и в Safari являются не критическими проблемами, но ощутимыми, хоть и соотносятся с фриками, не готовыми обновлять свои браузеры и устройства. Однако, IE все еще используется и даже популярнее Safari (5,87% против 3,62% по данным на 2020г).

Теперь о реализации данного способа.

1. Создание классов dark и light, содержащих переменные темы.

Способ именования переменных описан в разделе Проектирование дизайна.

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

.theme-light {  --body-color: #ECEFF1;  --antiBody-color: #263238;  --shared-primary-color: #1565C0;  --shared-secondary-color: #EF6C00;  --shared-error-color: #C62828;  --shared-default-color: #9E9E9E;  --shared-disabled-color: #E0E0E0;  --text-lvl1-color: #263238;  --text-lvl3-color: #546E7A;  --text-lvl5-color: #78909C;  --text-lvl7-color: #B0BEC5;  --text-lvl9-color: #ECEFF1;}.theme-dark {--body-color: #263238;  --antiBody-color: #ECEFF1;  --shared-primary-color: #90CAF9;  --shared-secondary-color: #FFE0B2;  --shared-error-color: #FFCDD2;  --shared-default-color: #BDBDBD;  --shared-disabled-color: #616161;  --text-lvl1-color: #ECEFF1;  --text-lvl3-color: #B0BEC5;  --text-lvl5-color: #78909C;  --text-lvl7-color: #546E7A;  --text-lvl9-color: #263238;}

Вы должны решить, какая тема будет использоваться по умолчанию и добавить ее класс в тег body.

2. Настройка класса по умолчанию, в зависимости от темы устройства пользователя.

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

Для решения этой задачи есть как минимум 2 корректных подхода

2.1) Настройка темы по умолчанию внутри css

Добавляется новый класс, который устанавливается по умолчанию - .theme-auto

Для этого класса добавляются переменные в зависимости от темы устройства посредством media запросов:

@media (prefers-color-scheme: dark) {body.theme-auto {--background-color: #111;--text-color: #f3f3f3;}}@media (prefers-color-scheme: light) {body.theme-auto {--background-color: #f3f3f3;    --text-color: #111;}}

Плюсы данного способа:

  • отсутствие скриптов

  • быстрое выполнение

Минусы:

  • дублирование кода (переменные повторяются с .theme-dark и .theme-light)

  • для определения темы, выбранной при последнем посещении все-равно потребуется скрипт

2.2) Установка класса по умолчанию с помощью js

У js есть полезная функция отслеживание и проверка правил css. Одним из таких правил, как уже было описано выше, является тема устройства пользователя.

Для проверки темы и добавления класса нужной темы нужно добавить следующий код:

if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {body.classlist.add('theme-dark')} else {body.classlist.add('theme-light')}

Дополнительно вы можете подписаться на изменение темы устройства:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {    if (e.matches) {        body.classlist.remove('theme-light')        body.classlist.add('theme-dark')    } else {        body.classlist.remove('theme-dark')        body.classlist.add('theme-light')    }});

Плюсы:

  • отсутствие дублирования переменных

Минусы:

  • Чтобы не было прыжков темы данный код должен выполняться на верхнем уровне (head или начало body). То есть он должен выполняться отдельно от основного бандла.

3. Создание стилизованных классов для элементов

./button.css

.button {  color: var(--text-lvl1-color);  background: var(--shared-default-color);  ...  &:disabled {    background: var(--shared-disabled-color);  }}.button-primary {background: var(--shared-primary-color);}.button-secondary {background: var(--shared-secondary-color)}

./appbar.css

.appbar {display: flex;  align-items: center;  padding: 8px 0;  color: var(--text-lvl9-color);  background-color: var(--shared-primary-color);}

4. Настройка смены класса при нажатии на кнопку смены темы

Пожалуй, самый простой пункт. Вам необходимо добавить на кнопку слушатель, который будет:

  • удалять прошлые классы, связанные с темой:

body.classlist.remove('theme-light', 'theme-high')
  • добавлять класс выбранной темы:

body.classlist.add('theme-dark')

5. Сохранение выбранной темы на устройстве пользователя.

Тему можно сохранять как в куки, так и в локальном хранилище. Структура и в первом, и во втором случае будет одинаковая: theme: 'light' | 'dark' | 'rose'

На верхнем уровне сайта нужно добавить получение сохраненной темы и добавление нужного класса тегу body. Например, в случае с локальным хранилищем:

const savedTheme = localStorage.getItem('theme')if (['light', 'dark', 'rose'].includes(savedTheme)) {body.classlist.remove('theme-light', 'theme-dark', 'theme-rose')body.classList.add(`theme-${savedTheme}`)}

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

Css-in-js

Данный вариант больше всего подходит для приложения, выполняемых на стороне клиента.

В качестве примера будет показана связка React + styled-components + typescript.

1. Создание объектов dark и light, содержащих переменные темы.

Способ именования переменных описан в разделе Проектирование дизайна.

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

Вы должны решить, какая тема будет использоваться по умолчанию и передать нужный объект Provider-у.

./App.tsx

import { useState } from 'react'import { ThemeProvider } from 'styled-components'import themes from './theme'const App = () => {const [theme, setTheme] = useState<'light' | 'dark'>('light')const onChangeTheme = (newTheme: 'light' | 'dark') => {setTheme(newTheme)}return (<ThemeProvider theme={themes[theme]}>// ...</ThemeProvide>)}

2. Настройка класса по умолчанию, в зависимости от темы устройства пользователя.

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

Для этого можно настроить тему по умолчанию на верхнем уровне приложения:

useEffect(() => {  if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {    onChangeTheme('dark')  }}, [])

Дополнительно вы можете подписаться на изменение темы устройства:

useEffect(() => {  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {    if (e.matches) {      onChangeTheme('dark')    } else {      onChangeTheme('light')    }  })}, [])

3. Создание стилизованных компонентов

./src/components/atoms/Button/index.tsx - git

import type { ButtonHTMLAttributes } from 'react'import styled from 'styled-components'interface StyledProps extends ButtonHTMLAttributes<HTMLButtonElement> {  fullWidth?: boolean;  color?: 'primary' | 'secondary' | 'default'}const Button = styled.button<StyledProps>(({ fullWidth, color = 'default', theme }) => `  color: ${theme.colors.text.lvl9};  width: ${fullWidth ? '100%' : 'fit-content'};  ...  &:not(:disabled) {    background: ${theme.colors.shared[color]};    cursor: pointer;    &:hover {      opacity: 0.8;    }  }  &:disabled {    background: ${theme.colors.shared.disabled};  }`)export interface Props extends StyledProps {  loading?: boolean;}export default Button

./src/components/atoms/AppBar/index.tsx - git

import styled from 'styled-components'const AppBar = styled.header(({ theme }) => `  display: flex;  align-items: center;  padding: 8px 0;  color: ${theme.colors.text.lvl9};  background-color: ${theme.colors.shared.primary};`)export default AppBar

4. Настройка смены класса при нажатии на кнопку смены темы

Через context api или redux/mobx изменяется имя текущей темы

./App.tsx - git

import { useState } from 'react'import { ThemeProvider } from 'styled-components'import themes from './theme'const App = () => {  const [theme, setTheme] = useState<'light' | 'dark'>('light')  const onChangeTheme = (newTheme: 'light' | 'dark') => {    setTheme(newTheme)  }  return (    <ThemeProvider theme={themes[theme]}>    <ThemeContext.Provider value={{ theme, onChangeTheme }}>    ...</ThemeContext.Provider>    </ThemeProvide>)}

.src/components/molecules/Header/index.tsx - git

import { useContext } from 'react'import Grid from '../../atoms/Grid'import Container from '../../atoms/Conrainer'import Button from '../../atoms/Button'import AppBar from '../../atoms/AppBar'import ThemeContext from '../../../contexts/ThemeContext'const Header: React.FC = () => {  const { theme, onChangeTheme } = useContext(ThemeContext)  return (    <AppBar>      <Container>        <Grid container alignItems="center" justify="space-between" gap={1}>          <h1>            Themization          </h1>          <Button color="secondary" onClick={() => onChangeTheme(theme === 'light' ? 'dark' : 'light')}>            set theme          </Button>        </Grid>      </Container>    </AppBar>  )}export default Header

5. Сохранение выбранной темы на устройстве пользователя.

Тему можно сохранять как в куки, так и в локальном хранилище. Структура и в первом, и во втором случае будет одинаковая: theme: 'light' | 'dark' | 'rose'

На верхнем уровне сайта нужно добавить получение сохраненной темы и обновлять значение текущем темы. Например, в случае с локальным хранилищем:

./App.tsx - git

...function App() {  const [theme, setTheme] = useState<'light' | 'dark'>('light')  const onChangeTheme = (newTheme: 'light' | 'dark') => {    localStorage.setItem('theme', newTheme)    setTheme(newTheme)  }  useEffect(() => {    const savedTheme = localStorage?.getItem('theme') as 'light' | 'dark' | null    if (savedTheme && Object.keys(themes).includes(savedTheme)) setTheme(savedTheme)    else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {      onChangeTheme('dark')    }  }, [])  useEffect(() => {    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {      if (e.matches) {        onChangeTheme('dark')      } else {        onChangeTheme('light')      }    })  }, [])  return (  ...  )}

Финальный код

Демо

Итоги

Вариантов внедрения темизации много от создания файлов со всеми стилями для каждой темы и их смены при необходимости до css-in-js решений (с нативными css переменными или встроенными в библиотеки решениями). Браузерное api дает возможности для настройки сервиса под каждого конкретного пользователя, считывая и отслеживая тему его устройства.

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

Конечно же, темизация нужна не всем. В любом случае она связана с, пусть и небольшими, но все же усложнениями. Она нужна, например, для приложений и веб-сервисов.

Сервисы Google и apple, банки, соц. сети, редакторы, github и gitlab. Продолжать список можно бесконечно, несмотря на то, что это только начало развития технологии, а дальше больше, лучше и проще.

Подробнее..

За что я не люблю Redux

19.06.2021 18:15:23 | Автор: admin

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

Flux - это вовсе не что-то новое либо революционное

Не то, чтобы я не люблю его за это. Скорее, в этом даже нет ничего плохого - если решение хорошее и проверенное временем, разве это плохо? Скорее мне просто удивительно слышать, как подход, реализованный в Flux в целом (и в Redux в частности) некоторые пытаются выдавать за что-то инновационное и революционное. Да и само решение, на мой взгляд, как минимум не лишено недостатков. Но об этом далее, а пока вспомню молодость.

В начале нулевых я разрабатывал ПО и библиотеки компонент на Delphi под Windows (сначала Win9x, потом XP). В операционных системах Windows с самых первых, если не ошибаюсь, версий, для визуальных элементов интерфейса (кнопки, поля ввода) существует понятие окна - да, окно это не только то, что с рамкой, почти любой визуальный элемент управления имел свое собственное окно. Окно в данном случае - это некая структура в памяти, которая имеет ассоциированный с ним идентификатор (window handle) и оконную функцию (см. далее). Если мы хотим выполнить какое-либо действие над элементом, например - изменить текст кнопки, мы должны упаковать это действие в специальную структуру-сообщение (Window message) и отправить ее соответствующему окну. Структура состоит из закодированного типа сообщения (например WM_SETTEXT - для установки текста) и собственно payload. Будучи отправленным, сообщение не попадает в обработчик напрямую - вместо этого оно отправится в очередь, из которой его извлекает некий диспетчер и вызывает оконную функцию того окна, в которое мы сообщение отправили, передав его в виде параметра. Оконная функция в простейшем случае - это большой switch, где в зависимости от типа сообщения мы передаем управление более конкретному обработчику. Ничего не напоминает?

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

Нарушение принципа "Low coupling, high cohesion"

Если вы ищите простую и понятную формулировку, что такое качественный дизайн, то эти четыре слова из подзаголовка коротко и емко его описывают - внутри модуля или компонента его элементы должны быть тесно связанны друг с другом, в то время как связи между отдельными модулями/компонентами должны быть слабыми. Это базовая ценность. Все остальные принципы и подходы в проектировании - следствия из этого принципа. "Low coupling, high cohesion" отвечает на вопрос "чего мы хотим добиться", в то время как, скажем, SOLID-принципы или любой из Design Pattern указывает нам "как мы можем этого добиться".

И вот тут Redux подводит - то, что должно быть цельным внутри компонента, оказывается размазанным по множеству файлов и сущностей - получаем Low cohesion вместо High. Связи, которые должны оставаться внутри, выходят наружу. Если нарушение принципа Low Coupling обычно представляют себе в виде переплетений из лапши, то здесь у меня в голове всплывает другое кулинарное блюдо. Позаимствовав терминологию у Java-разработчиков, если отдельный компонент - это фасолинка (Bean) - цельная, замкнутая вещь в себе, то тут мы получаем что-то вроде рагу, где фасоль полопалась и его содержимое вытекло, образовав густую однородную кашу, обволакивающую всю систему целиком, и не позволяющую на нее смотреть как на композицию отдельных законченных и слабо-зависимых сущностей.

Множество Boilerplate кода

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

Неуместное использование

А еще мне не нравится, что Redux или схожие с ним инструменты пытаются использовать там, где они не нужны - скажем, в Angular (angular-redux, NgRx). Redux предназначен для решения проблемы передачи данных в компоненты путем использования глобального State, и в React.js действительно существует такая проблема, там его использование кажется уместным. Но в Angular такой проблемы нет, Injectable-сервисы прекрасно справляются с этой задачей. Зачем решать несуществующую проблему, порождая при этом новые (о которых было написано выше)?

Ну и если говорить о неуместном использовании, также неуместным кажется использования Redux для управления локальным стейтом, что встречается довольно часто.

В заключение надо бы отметить и что-нибудь хорошее. Допустим, это не так плохо, когда у вас большая команда разработчиков разного уровня, и все пишут понятный всем код примерно в одном стиле ничего не выдумывая. Если иначе такая разношерстная команда не сможет работать эффективно, значит овчинка выделки определенно стоит. Но на самом деле хотелось бы иметь простое и эффективное решение, лишенное вышеназванных недостатков. Context API? Может быть.

Подробнее..
Категории: Javascript , React , Reactjs , Web , Redux , Flux

Идеальный инструмент для создания прогрессивных веб-приложений или Все, что вы хотели знать о Workbox. Часть 2

21.06.2021 12:17:59 | Автор: admin

image


Что такое Workbox?


Workbox (далее WB) это библиотека (точнее, набор библиотек), основной целью которой является "предоставление лучших практик и избавление от шаблонного кода при работе с сервис-воркерами" (далее СВ).


Если вы впервые слышите о СВ, то перед изучением данного руководства настоятельно рекомендуется ознакомиться со следующими материалами:



WB предоставляет следующие возможности:


  • предварительное кэширование
  • кэширование во время выполнения
  • стратегии (кэширования)
  • обработка (перехват сетевых) запросов
  • фоновая синхронизация
  • помощь в отладке

Это вторая часть руководства. Вот ссылка на первую часть.


Модули, предоставляемые WB


Каждый модуль решает определенную задачу и может быть использован как совместно с другими модулями, так и самостоятельно.


  • workbox-background-sync: фоновая синхронизация, позволяющая выполнять сетевые запросы в режиме офлайн
  • workbox-broadcast-update: отправка уведомлений об обновлении кэша (через Broadcast Channel API)
  • workbox-cacheable-response: фильтрация кэшируемых запросов на основе статус-кодов или заголовков ответов
  • workbox-core: изменение уровня логгирования и названий кэша. Содержит общий код, используемый другими модулями
  • workbox-expiration: установка лимита записей в кэше и времени жизни сохраненных ресурсов
  • workbox-google-analytics: фиксация действий пользователей на странице в режиме офлайн
  • workbox-navigation-preload: предварительная загрузка запросов, связанных с навигацией
  • workbox-precaching: предварительное кэширование ресурсов и управление их обновлением
  • workbox-range-request: поддержка частичных ответов
  • workbox-recipes: общие паттерны использования WB
  • workbox-routing: обработка запросов с помощью встроенных стратегий кэширования или колбэков
  • workbox-strategies: стратегии кэширования во время выполнения, как правило, используемые совместно с workbox-routing
  • workbox-streams: формирование ответа на основе нескольких источников потоковой передачи данных
  • workbox-window: регистрация, управление обновлением и обработка событий жизненного цикла СВ

workbox-background-sync


Иногда запрос на отправку данных на сервер проваливается. Это может произойти из-за потери соединения или из-за "упавшего" сервера. В любом случае, было бы здорово иметь возможность автоматически повторять выполнение такого запроса.


Новый BackgroundSync API отличное решение для такой ситуации. Когда СВ обнаруживает провалившийся запрос, он может регистрировать возникновение события sync, отправляемого брузером при восстановлении соединения. Данное событие отправляется даже если пользователь вышел из приложения, что делает этот подход гораздо более эффективным, чем традиционные способы повторного выполнения провалившихся запросов.


Браузеры, поддерживающие BackgroundSync API, автоматически отправляют повторный запрос от вашего имени через интервал, определяемый браузером. В браузерах, которые не поддерживают указанную технологию, рассматриваемый модуль отправит повторный запрос при очередном запуске СВ.


Базовое использование


Простейший способ использования фоновой синхронизации заключается в применении плагина, который автоматически помещает провалившиеся запросы в очередь и выполняет их повторную отправку при регистрации события sync:


import { BackgroundSyncPlugin } from 'workbox-background-sync'import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {  maxRetentionTime: 24 * 60, // Попытка выполнения повторного запроса будет выполнена в течение 24 часов (в минутах)})registerRoute(  /\/api\/.*\/*.json/,  new NetworkOnly({    plugins: [bgSyncPlugin],  }),  'POST')

Продвинутое использование


Рассматриваемый модуль предоставляет класс Queue, который, после инстанцирования, может использоваться для хранения провалившихся запросов. Такие запросы записываются в IndexedDB и извлекаются из нее при восстановлении соединения.


Создание очереди


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName') // название очереди должно быть уникальным

Название очереди используется как часть названия "тега", который получает register() глобального SyncManager. Оно также используется как название "объектного хранилища" IndexedDB.


Добавление запроса в очередь


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName')self.addEventListener('fetch', (event) => {  // Клонируем запрос для безопасного чтения  // при добавлении в очередь  const promiseChain = fetch(event.request.clone()).catch((err) => {    return queue.pushRequest({ request: event.request })  })  event.waitUntil(promiseChain)})

После добавления в очередь, запрос будет автоматически выполнен повторно при получении СВ события sync (или при следующем запуске СВ в браузерах, которые не поддерживают фоновую синхронизацию).


workbox-cacheable-response


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


Рассматриваемый модуль позволяет определять пригодность ответа для кэширования на основе статус-кода или присутствия заголовка с определенным значением.


Кэширование на основе статус-кода


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) =>    url.origin === 'https://example.com' && url.pathname.startsWith('/images/'),  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Данная настройка указывает WB кэшировать любые ответы со статусом 0 или 200 при обработке запросов к https://example.com.


Кэширование на основе заголовка


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При обработке ответов на запросы к URL, начинающемуся с /path/to/api/, проверяется, присутствует ли в ответе заголовок X-Is-Cacheable (который добавляется сервером). Если заголовок присутствует и имеет значение true, такой ответ кэшируется.


При определении нескольких заголовков, для кэширования ответа достаточно совпадения с одним из них.


Разумеется, указанные техники определения пригодности ответов для кэширования можно комбинировать:


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [200, 404],        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При использовании встроенной стратегии без явного определения cacheableResponse.CacheableResponsePlugin, для проверки валидности ответа используются следющие критерии:


  • staleWhileRevalidate и networkFirst: ответы со статусом 0 (непрозрачные ответы) и 200 считаются валидными
  • cacheFirst: только ответы со статусом 200 считаются валидными

По умолчанию заголовки ответа для определения его валидности не используются.


Продвинутое использование


Для определения логики кэширования за пределами стратегии можно использовать класс CacheableResponse:


import { CacheableResponse } from 'workbox-cacheable-response'const cacheable = new CacheableResponse({  statuses: [0, 200],  headers: {    'X-Is-Cacheable': 'true'  }})const response = await fetch('/path/to/api')if (cacheable.isResponseCacheable(response)) {  const cache = await caches.open('api-cache')  cache.put(response.url, response)} else {  // Ответ не может быть кэширован}

workbox-expiration


Данный плагин позволяет ограничивать количество ресурсов, сохраняемых в кэше, а также время их хранения.


Ограничение количества записей в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем количество записей в кэше        maxEntries: 20      })    ]  }))

При достижении лимита удаляются самые старые записи.


Ограничение времени хранения ресурсов в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем время хранения ресурсов в кэше        maxAgeSeconds: 24 * 60 * 60      })    ]  }))

Проверка записей на соответствие этому критерию и, при необходимости, их удаление осуществляется после каждого запроса или обновления кэша.


Продвинутое использование


Класс CacheExpiration позволяет отделять логику ограничения от других модулей. Для установки ограничений создается экземпляр названного класса:


import { CacheExpiration } from 'workbox-expiration'const cacheName = 'my-cache'const expirationManager = new CacheExpiration(cacheName, {  maxAgeSeconds: 24 * 60 * 60,  maxEntries: 20})

Затем, при обновлении записи в кэше, вызывается метод updateTimestamp() для обновления "возраста" записи.


await openCache.put(request, response)await expirationManager.updateTimestamp(request.url)

Для проверки всех записей в кэше на предмет их соответствия установленным критериям вызывается метод expireEntries():


await expirationManager.expireEntries()

workbox-precaching


СВ позволяет записывать файлы в кэш во время установки. Это называется предварительным кэшированием, поскольку контент кэшируется перед использованием СВ.


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


WB предоставляет простой и понятный API для реализации этого паттерна и эффективной загрузки ресурсов.


При первом запуске приложения workbox-precaching "смотрит" на загружаемые ресурсы, удаляет дубликаты и регистрирует соответствующие события СВ для загрузки и хранения ресурсов. URL, которые содержат информацию о версии (версионную информацию) (например, хэш контента) используются в качестве ключей кэша без дополнительной модификации. К ключам кэша URL, которые не содержат такой информации, добавляется параметр строки запроса, представляющий хэш контента, генерируемый WB во время выполнения.


workbox-precaching делает все это при обработке события install СВ.


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


Новый СВ не будет использоваться для ответов на запросы до его активации. В событии activate workbox-precaching определяет кэшированные ресурсы, отсутствующие в новом списке URL, и удаляет их из кэша.


Обработка предварительно кэшированных ответов


Вызов precacheAndRoute() или addRoute() создает маршрутизатор, который определяет совпадения запросов с предварительно кэшированными URL.


В этом маршрутизаторе используется стратегия "сначала кэш".


Порядок вызова названных методов имеет важное значение. Обычно, они вызываются в начале файла с кодом СВ, перед регистрацией и дополнительными маршрутизаторами, определяемыми с помощью registerRoute(). Если вызвать сначала registerRoute(), то любой маршрутизатор, совпавший с входящим запросом, независимо от стратегии, определенной в этом маршрутизаторе, будет использован для формирования ответа.


Список предварительно кэшируемых ресурсов


workbox-precaching ожидает получения массива объектов со свойствами url и revision. Данный массив иногда называют "манифестом предварительного кэширования":


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([  { url: '/index.html', revision: '383676' },  { url: '/styles/app.0c9a31.css', revision: null },  { url: '/scripts/app.0d5770.js', revision: null },  // другие записи])

Свойства revision второго и третьего объектов имеют значения null. Это объясняется тем, что версионная информация этих объектов является частью значений их свойств url.


В отличие от JavaScript и CSS URL, указывающие на HTML-файлы, как правило, не включают в себя версионную информацию по той причине, что ссылки на такие файлы должны быть статическими.


Версионирование позволяет рассматриваемому модулю определять необходимость обновления кэшированного ресурса.


Обратите внимание: для генерации списка предварительно кэшируемых ресурсов следует использовать один из встроенных инструментов WB: workbox-build, workbox-webpack-plugin или workbox-cli. Создавать такой список вручную очень плохая идея.


Автоматическая обработка входящих запросов


При поиске совпадения входящего запроса с кэшированным ресурсом workbox-precaching автоматически выполняет некоторые манипуляции с URL.


Например, запрос к / оценивается как запрос к index.html.


Игнорирование параметров строки запроса


По умолчанию игнорируются параметры поиска, которые начинаются с utm_ или точно совпадают с fbclid. Это означает, что запрос к /about.html?utm_campaign=abcd оценивается как запрос к /about.html.


Игнорируемые параметры указываются в настройке ignoreURLParametersMatching:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    // Игнорируем все параметры    ignoreURLParametersMatching: [/.*/]  })

Основной файл директории


По умолчанию основным файлом директории считается index.html. Именно поэтому запросы к / оцениваются как запросы к /index.html. Это поведение можно изменить с помощью настройки directoryIndex:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null },  ],  {    directoryIndex: null  })

"Чистые" URL


По умолчанию к запросу добавляется расширение .html. Например, запрос к /about оценивается как /about.html. Это можно изменить с помощью настройки cleanUrls:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([{ url: '/about.html', revision: 'b79cd4' }], {  cleanUrls: false})

Кастомные манипуляции


Настройка urlManipulation позволяет кастомизировать логику определения совпадений. Эта функция должна возвращать массив возможных совпадений:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    urlManipulation: ({ url }) => {      // Логика определения совпадений      return [alteredUrlOption1, alteredUrlOption2]    }  })

workbox-routing


СВ может перехватывать сетевые запросы. Он может отвечать браузеру кэшированным контентом, контентом, полученным из сети, или контентом, генерируемым СВ.


workbox-routing это модуль, позволяющий "связывать" поступающие запросы с функциями, формирующими на них ответы.


При отправке сетевого запроса возникает событие fetch, которое регистрирует СВ для формирования ответа на основе маршрутизаторов и обработчиков.


Обратите внимание на следующее:


  • важно указывать метод запроса. По умолчанию перехватываются только GET-запросы
  • маршрутизаторы должны регистрироваться в правильном порядке. При наличии нескольких "роутов" для одного запроса, сработает только первый из них

Определение совпадений и обработка запросов


В WB "роут" это две функции: функция "определения совпадения" и функция "обработки запроса".


WB предоставляет некоторые утилиты для помощи в реализации названных функций.


Функция определения совпадения принимает ExtendableEvent, Request и объект URL. Возврат истинного значения из этой функции означает совпадение. Например, вот пример определения совпадения с конкретным URL:


const matchCb = ({ url, request, event }) => {  return (url.pathname === '/special/url')}

Функция обработки запроса принимает такие же параметры + аргумент value, который имеет значение, возвращаемое из первой функции:


const handlerCb = async ({ url, request, event, params }) => {  const response = await fetch(request)  const responseBody = await response.text()  return new Response(`${responseBody} <!-- Глядите-ка! Новый контент. -->`, {    headers: response.headers  })}

Обработчик должен возвращать промис, разрешающийся Response.


Регистрация колбэков выглядит следующим образом:


import { registerRoute } from 'workbox-routing'registerRoute(matchCb, handlerCb)

Единственным ограничением является то, что функция определения совпадения должна возвращать истинное значение синхронно. Это связано с тем, что Router должен синхронно обрабатывать событие fetch или передавать его другим обработчикам.


Как правило, в функции обработки запроса используется одна из встроенных стратегий, например:


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  matchCb,  new StaleWhileRevalidate())

Определение совпадений с помощью регулярного выражения


Вместо функции определения совпадения, можно использовать регулярное выражение:


import { registerRoute } from 'workbox-routing'registerRoute(  new RegExp('/styles/.*\\.css'),  handlerCb)

Для запросов из одного источника данная "регулярка" будет регистрировать совпадения для следующих URL:



Однако, в случае с запросами к другим источникам, регулярка должна совпадать с началом URL. Поэтому совпадение для следующих URL обнаружено не будет:



Для решения этой проблемы можно использовать такое регулярное выражение:


new RegExp('https://cdn\\.third-party-site\\.com.*/styles/.*\\.css')

Для регистрации совпадений как с локальными, так и со сторонними URL можно использовать wildcard, но при этом следует проявлять особую осторожность.


Роут для навигации


Если ваше приложение это одностраничник, для обработки всех запросов, связанных с навигацией, можно использовать NavigationRoute.


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'// Предположим, что страница `/app-shell.html` была предварительно кэшированаconst handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler)registerRoute(navigationRoute)

При посещении пользователем вашего сайта, запрос на получение страницы будет считаться навигационным, следовательно, ответом на него будет кэшированная страница /app-shell.html.


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


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'const handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler, {  allowlist: [    new RegExp('/blog/')  ],  denylist: [    new RegExp('/blog/restricted/')  ]})registerRoute(navigationRoute)

Обратите внимание, что denyList имеет приоритет перед allowList.


Обработчик по умолчанию


import { setDefaultHandler } from 'workbox-routing'setDefaultHandler(({ url, event, params }) => {  // ...})

Обработчик ошибок


import { setCatchHandler } from 'workbox-routing'setCatchHandler(({ url, event, params }) => {  // ...})

Обработка не-GET-запросов


import { registerRoute } from 'workbox-routing'registerRoute(  matchCb,  handlerCb,  // определяем метод  'POST')registerRoute(  new RegExp('/api/.*\\.json'),  handlerCb,  // определяем метод  'POST')

workbox-strategies


Стратегия кэширования это паттерн, определяющий порядок формирования СВ ответа на запрос (после возникновения события fetch).


Вот какие стратегии предоставляет рассматриваемый модуль.


Stale-While-Revalidate


Данная стратегия возвращает ответ из кэша (при наличии ответа в кэше) или из сети (при отсутствии кэшированного ответа). Сетевой запрос используется для обновления кэша. Такой запрос выполняется независимо от возраста кэшированного ответа.


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/images/avatars/'),  new StaleWhileRevalidate())

Cache-Fisrt


Данная стратегия отлично подходит для больших статических редко изменяемых ресурсов.


При наличии ответа в кэше, он просто возвращается, а сеть не используется совсем. Если ответа в кэше нет, выполняется сетевой запрос, ответ на который возвращается пользователю и кэшируется для следующего использования.


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'style',  new CacheFirst())

Network-First


Данная стратегия подходит для часто обновляемых запросов. Сначала выполняется сетевой запрос. Если запрос выполняется успешно, ответ на него возвращается пользователю и записывается в кэш. Если запрос проваливается, возвращается кэшированный ответ.


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/social-timeline/'),  new NetworkFirst())

Network-Only


import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/admin/'),  new NetworkOnly())

Cache-Only


import { registerRoute } from 'workbox-routing'import { CacheOnly } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/app/v2/'),  new CacheOnly())

Настройка стратегии


Каждая стратегия позволяет кастомизировать:


  • название кэша
  • лимит записей в кэше и время их "жизни"
  • плагины

Название кэша


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',  }))

Плагины


В стратегии могут использоваться следующие плагины:


  • workbox-background-sync
  • workbox-broadcast-update
  • workbox-cacheable-response
  • workbox-expiration
  • workbox-range-requests

import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // Хранить ресурсы в течение недели        maxAgeSeconds: 7 * 24 * 60 * 60,        // Хранить до 10 ресурсов        maxEntries: 10      })    ]  }))

WB также позволяет создавать и использовать собственные стратегии.


workbox-recipies


Некоторые паттерны, особенно касающиеся маршрутизации и кэширования, являются достаточно общими для возможности их стандартизации в виде переиспользуемых рецептов. workbox-recipies предоставляет набор таких рецептов.


Рецепты


Каждый рецепт это комбинация определенных модулей WB. Ниже приводятся рецепты и паттерны, которые они используют под капотом, на случай, если вы захотите кастомизировать тот или иной рецепт.


Резервный контент


Данный рецепт позволяет СВ возвращать резервную страницу, изображение или шрифт при возникновении ошибки при выполнении запроса на получения любого из указанных ресурсов.


По умолчанию резервная страница должна иметь название offline.html.


Резервный контент возвращается при совпадении с определенным запросом. При использовании рассматриваемого рецепта в отдельности, необходимо реализовать соответствующие роуты. Простейшим способом это сделать является использование метода setDefaultHandler() для создания роута, применяющего стратегию "только сеть" в отношении всех запросов.


Рецепт


import { offlineFallback } from 'workbox-recipes'import { setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'setDefaultHandler(  new NetworkOnly())offlineFallback()

Паттерн


import { setCatchHandler, setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const pageFallback = 'offline.html'const imageFallback = falseconst fontFallback = falsesetDefaultHandler(  new NetworkOnly())self.addEventListener('install', event => {  const files = [pageFallback]  if (imageFallback) {    files.push(imageFallback)  }  if (fontFallback) {    files.push(fontFallback)  }  event.waitUntil(self.caches.open('workbox-offline-fallbacks').then(cache => cache.addAll(files)))})const handler = async (options) => {  const dest = options.request.destination  const cache = await self.caches.open('workbox-offline-fallbacks')  if (dest === 'document') {    return (await cache.match(pageFallback)) || Response.error()  }  if (dest === 'image' && imageFallback !== false) {    return (await cache.match(imageFallback)) || Response.error()  }  if (dest === 'font' && fontFallback !== false) {    return (await cache.match(fontFallback)) || Response.error()  }  return Response.error()}setCatchHandler(handler)

Подготовка кэша


Данный рецепт позволяет записывать определенные URL в кэш во время установки СВ. Она может использоваться в качестве альтернативы предварительного кэширования в случае, когда нам заранее известен список URL для сохранения.


Рецепт


import { warmStrategyCache } from 'workbox-recipes'import { CacheFirst } from 'workbox-strategies'// Здесь может испоьзоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html']warmStrategyCache({urls, strategy})

Паттерн


import { CacheFirst } from 'workbox-strategies'// Здесь может использоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html',]self.addEventListener('install', event => {  // `handleAll` возвращает два промиса, второй промис разрешается после добавления всех элементов в кэш  const done = urls.map(path => strategy.handleAll({    event,    request: new Request(path),  })[1])  event.waitUntil(Promise.all(done))})

Кэширование страницы


Данный рецепт позволяет СВ отвечать на запрос на получение HTML-страницы с помощью стратегии "сначала сеть". При этом, СВ оптимизируется таким образом, что в случае отсутствия подключения к сети, возвращает ответ из кэша менее чем за 4 секунды. По умолчанию запрос к сети выполняется в течение 3 секунд. Настройка warmCache позволяет подготовить ("разогреть") кэш к использованию.


Рецепт


import { pageCache } from 'workbox-recipes'pageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'pages'const matchCallback = ({ request }) => request.mode === 'navigate'const networkTimeoutSeconds = 3registerRoute(  matchCallback,  new NetworkFirst({    networkTimeoutSeconds,    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Кэширование статических ресурсов


Данный рецепт позволяет СВ отвечать на запросы на получение статических ресурсов, таких как JavaScript, CSS и веб-воркеры с помощью стратегии "считается устаревшим после запроса" (ответ возвращается из кэша, после чего кэш обновляется). Поддерживается разогрев кэша (warmCache).


Рецепт


import { staticResourceCache } from 'workbox-recipes'staticResourceCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'static-resources'const matchCallback = ({ request }) =>  // CSS  request.destination === 'style' ||  // JavaScript  request.destination === 'script' ||  // веб-воркеры  request.destination === 'worker'registerRoute(  matchCallback,  new StaleWhileRevalidate({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Кэширование изображений


Данный рецепт позволяет СВ отвечать на запросы на получение изображений с помощью стратегии "сначала кэш". По умолчанию кэшируется до 60 изображений в течение 30 дней. Поддерживается разогрев кэша.


Рецепт


import { imageCache } from 'workbox-recipes'imageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const cacheName = 'images'const matchCallback = ({ request }) => request.destination === 'image'const maxAgeSeconds = 30 * 24 * 60 * 60const maxEntries = 60registerRoute(  matchCallback,  new CacheFirst({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      }),      new ExpirationPlugin({        maxEntries,        maxAgeSeconds      })    ]  }))

Кэширование гугл-шрифтов


Данный рецепт кэширует таблицу стилей для шрифтов с помощью стратегии "считается устаревшим после запроса" и сами шрифты с помощью стратегии "сначала кэш". По умолчанию кэшируется до 30 шрифтов в течение 1 года.


Рецепт


import { googleFontsCache } from 'workbox-recipes'googleFontsCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const sheetCacheName = 'google-fonts-stylesheets'const fontCacheName = 'google-fonts-webfonts'const maxAgeSeconds = 60 * 60 * 24 * 365const maxEntries = 30registerRoute(  ({ url }) => url.origin === 'https://fonts.googleapis.com',  new StaleWhileRevalidate({    cacheName: sheetCacheName  }))// Кэшируем до 30 шрифтов с помощью стратегии "сначала кэш" и храним кэш в течение 1 годаregisterRoute(  ({ url }) => url.origin === 'https://fonts.gstatic.com',  new CacheFirst({    cacheName: fontCacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200],      }),      new ExpirationPlugin({        maxAgeSeconds,        maxEntries      })    ]  }))

Быстрое использование


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


import {  pageCache,  imageCache,  staticResourceCache,  googleFontsCache,  offlineFallback} from 'workbox-recipes'pageCache()googleFontsCache()staticResourceCache()imageCache()offlineFallback()

workbox-window


Данный модуль выполняется в контексте window. Его основными задачами является следующее:


  • упрощение процесса регистрации и обновления СВ в наиболее подходящие для этого моменты жизненного цикла СВ
  • помощь в обнаружении наиболее распространенных ошибок, совершаемых разработчиками при работе с СВ
  • облегчение коммуникации между кодом СВ и кодом, запускаемым в window

Использование CDN


<script type="module">import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-window.prod.mjs'if ('serviceWorker' in navigator) {  const wb = new Workbox('/sw.js')  wb.register()}</script>

Использование сборщика модулей


Установка


yarn add workbox-window# илиnpm i workbox-window

Использование


import { Workbox } from 'workbox-window'if ('serviceWorker' in navigator) {  const wb = new Workbox('/sw.js')  wb.register()}

Примеры


Регистрация СВ и уведомление пользователя о его активации


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // `event.isUpdate` будет иметь значение `true`, если другая версия СВ  // управляет страницей при регистрации данной версии  if (!event.isUpdate) {    console.log('СВ был активирован в первый раз!')    // Если СВ настроен для предварительного кэширования ресурсов,    // эти ресурсы могут быть получены здесь  }})// Региструем СВ после добавления обработчиков событийwb.register()

Уведомление пользователя о том, что СВ был установлен, но ожидает активации


Когда на странице, управляемой СВ, региструется новый СВ, по умолчанию последний не будет активирован до тех пор, пока все клиенты, контрлируемые первым, не завершат свои сессии.


const wb = new Workbox('/sw.js')wb.addEventListener('waiting', (event) => {  console.log(    `Новый СВ был установлен, но он не может быть активирован, пока все вкладки браузера не будут закрыты или перезагружены`  )})wb.register()

Уведомление пользователя об обновлении кэша


Модуль workbox-broadcast-update позволяет информировать пользователей об обновлении контента. Для получения этой информации в браузере используется событие message с типом CACHE_UPDATED:


const wb = new Workbox('/sw.js')wb.addEventListener('message', (event) => {  if (event.data.type === 'CACHE_UPDATED') {    const { updatedURL } = event.data.payload    console.log(`Доступна новая версия ${updatedURL}!`)  }})wb.register()

Отправка СВ списка URL для кэширования


В некоторых приложениях имеет смысл кэшировать только те ресурсы, которые используются посещенной пользователем страницей. Модуль workbox-routing принимает список URL и кэширует их на основе правил, определенных в маршрутизаторе.


В приведенном ниже примере при каждой активации нового СВ в роутер отправляется список URL для кэширования. Обратите внимание, что мы можем отправлять все URL, поскольку будут кэшированы только те из них, которые совпадут с роутами.


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // Получаем `URL` текущей страницы + все загружаемые страницей ресурсы  const urlsToCache = [    location.href,    ...performance      .getEntriesByType('resource')      .map((r) => r.name)  ]  // Передаем этот список СВ  wb.messageSW({    type: 'CACHE_URLS',    payload: { urlsToCache }  })})wb.register()

Практика


В этом разделе представлено несколько сниппетов, которые можно использовать в приложениях "как есть", а также краткий обзор готовых решений для разработки PWA, предоставляемых такими фреймворками для фронтенда, как React и Vue.


Для обеспечения работы приложения в режиме офлайн требуется не только СВ и его регистрация в основной файле приложения, но и так называемый "манифест".


О том, что такое манифест можно почитать здесь, здесь и здесь.


Как правило, манифест (и СВ) размещаются на верхнем уровне (в корневой директории) проекта. Манифест может иметь расширение .json или .webmanifest (лучше использовать первый вариант).


Манифест


{  "name": "Название приложения",  "short_name": "Краткое название (будет указано под иконкой приложения при его установке)",  "scope": "/", // зона контроля СВ, разные страницы могут обслуживаться разными СВ  "start_url": ".", // начальный URL, как правило, директория, в которой находится index.html, в котором регистрируется СВ  "display": "standalone",  "orientation": "portrait",  "background_color": "#f0f0f0",  "theme_color": "#3c3c3c",  "description": "Описание приложения",  // этих иконок должно быть достаточно для большинства девайсов  "icons": [    {      "src": "./icons/64x64.png",      "sizes": "64x64",      "type": "image/png"    },    {      "src": "./icons/128x128.png",      "sizes": "128x128",      "type": "image/png"    },    {      "src": "./icons/256x256.png",      "sizes": "256x256",      "type": "image/png",      "purpose": "any maskable"    },    {      "src": "./icons/512x512.png",      "sizes": "512x512",      "type": "image/png"    }  ],  "serviceworker": {    "src": "./service-worker.js" // ссылка на файл с кодом СВ  }}

Ручная реализация СВ, использующего стратегию "сначала кэш"


// Название кэша// используется для обновления кэша// в данном случае, для этого достаточно изменить версию кэша - my-cache-v2const CACHE_NAME = 'my-cache-v1'// Критические для работы приложения ресурсыconst ASSETS_TO_CACHE = [  './index.html',  './offline.html',  './style.css',  './script.js']// Предварительное кэширование ресурсов, выполняемое во время установки СВself.addEventListener('install', (e) => {  e.waitUntil(    caches      .open(CACHE_NAME)      .then((cache) => cache.addAll(ASSETS_TO_CACHE))  )  self.skipWaiting()})// Удаление старого кэша во время активации нового СВself.addEventListener('activate', (e) => {  e.waitUntil(    caches      .keys()      .then((keys) =>        Promise.all(          keys.map((key) => {            if (key !== CACHE_NAME) {              return caches.delete(key)            }          })        )      )  )  self.clients.claim()})// Обработка сетевых запросов/*  1. Выполняется поиск совпадения  2. Если в кэше имеется ответ, он возвращается  3. Если ответа в кэше нет, выполняется сетевой запрос  4. Ответ на сетевой запрос кэшируется и возвращается  5. В кэш записываются только ответы на `GET-запросы`  6. При возникновении ошибки возвращается резервная страница*/self.addEventListener('fetch', (e) => {  e.respondWith(    caches      .match(e.request)      .then((response) =>          response || fetch(e.request)            .then((response) =>              caches.open(CACHE_NAME)                .then((cache) => {                  if (e.request.method === 'GET') {                    cache.put(e.request, response.clone())                  }                  return response                })          )      )      .catch(() => caches.match('./offline.html'))  )})

Конфигурация Webpack


Пример настройки вебпака для производственной сборки прогрессивного веб-приложения.


Предположим, что в нашем проекте имеется 4 директории:


  • public директория со статическими ресурсами, включая index.html, manifest.json и sw-reg.js
  • src директория с кодом приложения
  • build директория для сборки
  • config директория с настройками, включая .env, paths.js и webpack.config.js

В файле public/sw-reg.js содержится код регистрации СВ:


if ('serviceWorker' in navigator) {  window.addEventListener('load', () => {    navigator.serviceWorker      .register('./service-worker.js')      .then((reg) => {        console.log('СВ зарегистрирован: ', reg)      })      .catch((err) => {        console.error('Регистрация СВ провалилась: ', err)      })  })}

В файле config/paths.js осуществляется экспорт путей к директориям с файлами приложения:


const path = require('path')module.exports = {  public: path.resolve(__dirname, '../public'),  src: path.resolve(__dirname, '../src'),  build: path.resolve(__dirname, '../build')}

Допустим, что в качестве фронтенд-фреймворка мы используем React, а также, что в проекте используется TypeScript. Тогда файл webpack.config.js будет выглядеть следующим образом:


const webpack = require('webpack')// импортируем пути к директориям с файлами приложенияconst paths = require('../paths')// плагин для копирования статических ресурсов в директорию сборкиconst CopyWebpackPlugin = require('copy-webpack-plugin')// плагин для обработки `index.html` - вставки ссылок на стили и скрипты, добавления метаданных и т.д.const HtmlWebpackPlugin = require('html-webpack-plugin')// плагин для обеспечения прямого доступа к переменным среды окруженияconst Dotenv = require('dotenv-webpack')// плагин для минификации и удаления неиспользуемого CSSconst MiniCssExtractPlugin = require('mini-css-extract-plugin')// плагин для сжатия изображенийconst ImageminPlugin = require('imagemin-webpack-plugin').default// плагин для добавления блоков кодаconst AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')// Плагин для генерации СВconst { GenerateSW } = require('workbox-webpack-plugin')// настройки Babelconst babelLoader = {  loader: 'babel-loader',  options: {    presets: ['@babel/preset-env', '@babel/preset-react'],    plugins: [      '@babel/plugin-proposal-class-properties',      '@babel/plugin-syntax-dynamic-import',      '@babel/plugin-transform-runtime'    ]  }}module.exports = {  // режим сборки  mode: 'production',  // входная точка  entry: {    index: {      import: `${paths.src}/index.js`,      dependOn: ['react', 'helpers']    },    react: ['react', 'react-dom'],    helpers: ['immer', 'nanoid']  },  // отключаем логгирование  devtool: false,  // результат сборки  output: {    // директория сборки    path: paths.build,    // название файла    filename: 'js/[name].[contenthash].bundle.js',    publicPath: './',    // очистка директории при каждой сборке    clean: true,    crossOriginLoading: 'anonymous',    module: true  },  resolve: {    alias: {      '@': `${paths.src}/components`    },    extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json']  },  experiments: {    topLevelAwait: true,    outputModule: true  },  module: {    rules: [      // JavaScript, React      {        test: /\.m?jsx?$/i,        exclude: /node_modules/,        use: babelLoader      },      // TypeScript      {        test: /.tsx?$/i,        exclude: /node_modules/,        use: [babelLoader, 'ts-loader']      },      // CSS, SASS      {        test: /\.(c|sa|sc)ss$/i,        use: [          'style-loader',          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      },      // статические ресурсы - изображения и шрифты      {        test: /\.(jpe?g|png|gif|svg|eot|ttf|woff2?)$/i,        type: 'asset'      },      {        test: /\.(c|sa|sc)ss$/i,        use: [          MiniCssExtractPlugin.loader,          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      }    ]  },  plugins: [    new CopyWebpackPlugin({      patterns: [        {          from: `${paths.public}/assets`        }      ]    }),    new HtmlWebpackPlugin({      template: `${paths.public}/index.html`    }),    // это позволяет импортировать реакт только один раз    new webpack.ProvidePlugin({      React: 'react'    }),    new Dotenv({      path: './config/.env'    }),    new MiniCssExtractPlugin({      filename: 'css/[name].[contenthash].css',      chunkFilename: '[id].css'    }),    new ImageminPlugin({      test: /\.(jpe?g|png|gif|svg)$/i    }),    // Добавляем код регистрации СВ в `index.html`    new AddAssetHtmlPlugin({ filepath: `${paths.public}/sw-reg.js` }),    // Генерируем СВ    new GenerateSW({      clientsClaim: true,      skipWaiting: true    })  ],  optimization: {    runtimeChunk: 'single'  },  performance: {    hints: 'warning',    maxEntrypointSize: 512000,    maxAssetSize: 512000  }}

Здесь вы найдете шпаргалку по настройке вебпака. Пример полной конфигурации вебпака для JS/React/TS-проекта можно посмотреть здесь.


React PWA


Для того, чтобы получить готовый шаблон React-приложения с возможностями PWA, достаточно выполнить команду:


yarn create react-app my-app --template pwa# илиnpx create-react-app ...

Или, если речь идет о TypeScript-проекте:


yarn create react-app my-app --template pwa-typescript# илиnpx create-react-app ...

Кроме прочего, в директории src создаются файлы service-worker.ts и serviceWorkerRegister.ts (последний импортируется в index.tsx), а в директории public файл manifest.json.


Затем, перед сборкой проекта с помощью команды yarn build или npm run build, в файл src/index.tsx необходимо внести одно изменение:


// доserviceWorkerRegistration.unregister();// послеserviceWorkerRegistration.register();

Подробнее об этом можно прочитать здесь.


Vue PWA


С Vue дела обстоят еще проще.


Глобально устанавливаем vue-cli:


yarn global add @vue/cli# илиnpm i -g @vue/cli

Затем, при создании шаблона проекта с помощью команды vue create my-app, выбираем Manually select features и Progressive Web App (PWA) Support.


Кроме прочего, в директории src создается файл registerServiceWorker.ts, который импортируется в main.ts. Данный файл содержит ссылку на файл service-worker.js, который, как и manifest.json, автоматически создается при сборке проекта с помощью команды yarn build или npm run build. Разумеется, содержимое обоих файлов можно кастомизировать.

Подробнее..

История о том, как я иду к должности JS разработчика через обучение на курсах в Skillbox

21.06.2021 14:12:41 | Автор: admin

Как пришел я к тому чтобы вообще начать учить JS

В 2019 году, 1 сентября, в дождливый осенний день, я решил навсегда завязать с прошлым. Последние 5 лет работы менеджером не приносили удовольствия и не несли перспектив. Увольняюсь с должности менеджера вино-торговой компании, подумал я. И погружаюсь в программирование!

Три месяца до декабря я упорно изучал HTML + CSS и верил, что легко попаду в разработчики, стоит мне только захотеть! Каким же большим было моё разочарование, когда я понял что в этом мире не все так просто. Первое тестовое задание я получил от компании Тензор это была разработка игры на чистом JavaScript либо с применением любого фреймворка:

  • Angular.js;

  • Vue.js;

  • React.js;

Что вы думаете? Конечно я не смог написать и строчки, точнее строчку я конечно написал, одну или две. Но в функции не умел и не понимал как объединять код. Всё сказал я себе, иду изучать программирование на курсы, так как самостоятельно я ничего не добьюсь. Я слишком мало понимаю. Мне нужны менторы, задания, последовательность обучения.
Поиграть в морской бой можно тут.

Морской бой тестовое задание в ТензорМорской бой тестовое задание в Тензор

Мой первый купленный курс

В Феврале 2020 года, работаю курьером в пиццерии. Мечтаю о ноутбуке что бы параллельно кодить. Всем рассказываю про свой путь. Но все еще одинок в этом.

Принимаю решение вложить 13 тысяч рублей в курсы по верстке сайтов. Стоит сказать что такую смешную сумму, мне пришлось просить разбить на 4 платежа по 3 250 рублей. Ибо в додо я делал 20 тысяч в месяц максимум.

Моя морда на работе в феврале 2020Моя морда на работе в феврале 2020

К тому моменту, я имел за спиной 1 2 кривых пет-проекта по верстке, но основ все еще не понимал до конца. На курсе познакомился с такими инструментами как: HTML, CSS Bootstrap, SASS Git, Gulp Autoprefixer, Pixel perfect, БЭМ JavaScript, Ajax, PHP

Нужно сказать что было крайне интересно и не сложно. Хорошая подача материала и трудолюбие сделали свое дело. Но почему-то дальше все мое продвижение встало мертвым грузом. Как будто я застрял и не знал как выбраться, как продолжить увеличивать знания?! Основные ошибки новичка (на моём опыте).

Ошибка 1 Не закреплял полученные знания

Стоило бы сделать 1, а лучше 2 учебных проекта используя все, чему меня научили на курсе. Сделать сайт другу, жене, кошке, деревьям. Какой угодно, главное закрепить полученный результат.

Ошибка 2 Дал слабину, расслабился

Решил, что я итак все знаю и никогда не забуду, ведь простые люди привыкли, что стоит научится кататься на велосипеде и ты никогда не разучишься. А на деле с кодом это так не работает. По крайней мере на старте.

Ошибка 3 Начал искать новую информацию для изучения не имея четкого плана

Еще 3 месяца я читал, пытался кодить, изучал JavaScript. Но не по учебнику или по плану, а просто так, хаотично, без примеров и задач, чаще просто смотря видео. А без базы понять что такое prototype или методы перебора массивов было почти нереальной задачей.

Ошибка 4 Мало теории, еще меньше практики

Просмотр видео 2 раза в неделю, без четких целей и совсем не понимая о чём речь, при этом совсем почти не повторяя код. Не делая пет-проектов и не закрепляя знания.

Справедливости ради, хочу отметить, что огромное количество информации в один момент преобразовались в какие-никакие знания. Однажды утром, пазл сошелся. В этом очень помогла книга JavaScript для самых маленьких.

Книга по JS для начинающихКнига по JS для начинающих

Вот я и на полпути к мечте! подумал про себя и начал искать работу снова

Редактирование резюме, вставка парочки кривых учебных проектов и подача на все вакансии в своем городе. Из 30 откликов 25 игноров, 3 отказа и 2 тестовых задания. Два тестовых задания были мне не по зубам и я честно признаться, сразу писал что-то типа:

Прошу меня простить, но я еще видимо не дорос до уровня чтобы выполнить этот тест и вернусь к Вам, как только у меня появится понимание как решать.

Ошибочное поведение подумаете вы? Не знаю, можете написать в комментариях как бы вы поступили в моей ситуации.

Олицетворения меня как трусаОлицетворения меня как труса

Я же поступал как удобно мне. Ведь я был вынужден где-то работать. У меня ребенок, кредиты, я как обычный среднестатистический человек устаю в течении дня и приношу домой копейки чтобы хватало на оплату еды, жилья и дешевых вещей. А тут вечером нужно еще позаниматься JavaScript + верстка, а лучше React поучить и всякие там видеоуроки посмотреть (обязательно с практикой). Одним словом не до тестовых заданий, особенно которые не знаешь даже как решать.

Все эти мысли натолкнули меня на очевидный вывод. Рано куда-то рыпаться, необходимо учиться дальше. А прошел уже год как я принял для себя решение изменить свою жизнь. На работу не связанную с кодом я уже даже смотреть не мог. Спал и видел, как в кругу разработчиков я самый отсталый, но счастливый и замотивированный делаю легкие задачи, которые не интересны остальным. И я погнал учиться! Как вы думаете куда? В Skillbox.

Сразу скажу это не реклама для Skillbox, это лично мое мнение

В моменте захотелось выбрать какой-то серьезный для себя вектор, мол, мало мне коротких курсов или самостоятельного обучения. Хочу связать себя с долгим и кропотливым трудом на года. Да и диплом об окончании скилов показался мне престижной бумагой. Размечтался что поеду в офис компании в Москву, буду защищать свой дипломный проект. А я такой опытный манагер, все презентую по красоте! Все будут аплодировать стоя и кричать:

Это junior с задатками team lead! Наш мальчик! Браво!.

Мой пыл спустя восемь месяцев подостыл, но я все еще фигачу и двигаюсь дальше. Кто знает, может я действительно поеду защищать диплом в МСК.

Несколько выводов для сомневающихся:

  • покупка курса в кредит (точнее в рассрочку) не сделала меня более мотивированным. Однако, у меня появился структурированный материал по стеку Frontend по которому я в свободное время мог теперь двигаться и не тратить время на поиск инфы;

  • общение с преподавателем не супер дружеское конечно, но присутствует. Если вы просите фидбек по примерам, задачам или домашней работе вы его получаете;

  • чат таких же сокурсников как вы, в телеграмм, где можно спросить, подсказать или просто увидеть, что сложно не только вам, или что кто-то уже впереди;

  • много полезных материалов по основам верстки, программирования и алгоритмам алгоритмы вообще отдельный крутейший блок, который я бы выделил в отдельный плюс центр карьеры, который поможет и резюме составить и работу найти.

Моя шкала прогресса Моя шкала прогресса

Как лучше проходить курс на Skillbox по моему мнению

Сложно сказать однозначно. Типа так делай, а так не делай. Ведь все мы разные. Кто-то обладает 12 часами свободного времени в день и тонной мотивации. А кто-то только 1 час вечером и абсолютно без сил.

Но скажу следующее. Важно ежедневно, от 30 минут до 2 часов (хотя бы) заниматься каждый день.

  1. Верстайте один день у вас только верстка. Проходите урок, делаете задания, комитете его в свой репозиторий и задаете вопросы если не понимаете, как быть дальше преподавателю.

  2. Программируете громко сказано, конечно, но учите базу, делаете задачи, примеры, повторяете, потеете, пытаетесь, это все делаете второй день. В конце повторяете, коммит в репозиторий + вопросы если что непонятно к преподавателю.

  3. Алгоритмы третий день у вас пусть пройдет в просмотре и понимании алгоритмов, крутая и нужная вещь. Расставляет по местам знания, которые не усваиваются. В моем случае даже домашек нет. Просто смотри, вникай, наслаждайся!

  4. Фреймворк тут на ваше усмотрение какой выбрать. Главное начинайте освоение хотя бы после прохождения основ.

Представим что у вас 30 уроков в каждом разделе. При ежедневных вложениях по чуть-чуть, уже через 90 дней вы сможете сделать от 30% до 50%. А это всего 3 месяца. Еще 3 месяца и можно приступать к фреймворкам.

Конечно, супер крутым разработчиком вы не станете. Но массу полезной информации усвоите а это самое важное на старте. Ведь когда ты новичок. Тебе сложно понять элементарные вещи. Методы массивов например довольно сложные в понимании для неокрепших умов. Но только через упорство и труд получится что-то понять. Дерзайте!

Мотивация на результатМотивация на результат

В процессе обучения мои амбиции росли и я вместе с ними.

В какой-то момент, решил, что могу попробовать снова устроиться в какую-нибудь компанию. Например Junior разработчиком или хотя бы HTML верстальщиком. Тут стоит отметить про центр карьеры от Skillbox.

У вас есть личный менеджер подбора персонала. Который раскидывает ваше резюме по вакансиям, на которые вы просите это сделать. А резюме составляете, конечно вы сами. По рекомендациям. Если уделить время и не стесняться, то получится грамотный и интересный такой дайджест про вашу личность. Делайте по гайдам и модернизируйте!

Представьте, вам звонят днем, вы поднимаете трубку с незнакомого номера телефона, а там:
Добрый день, это HR компании войтиВайти, ваше резюме нам прислали из центра подбора персонала компании Skillbox, вас рекомендовали как разработчика и так далее.
Помню когда получил первый раз такой звонок, чуть не выпал в осадок. Хорошо стул был неподалеку. К сожалению, у меня не вышло устроиться пока что никуда.
НО отмечу, что я прошел 8 собеседований. Выполнил несколько тестовых заданий, вспотел на нескольких технических интервью. С вопросами про reduce, map, filter, работу с объектами и про жизненный цикл компонент (и массу чего еще).

Получил ссылки на книги которые лучше бы прочитать. Получил ссылки на источники которые лучше бы изучить.

Вот и фидбек. Получается я стал и стану еще опытнее и умнее. А получилось бы это если бы я ничего не делал. А просто пытался учиться самостоятельно? Не знаю.

Напишите в комментариях что вы думаете по этому поводу!?

И вот в очередной раз, я убедился, что нужно сделать паузу и углубиться в обучение, а не в тестовые задания. Тратить время на изучение моего любимого JSa с добавками React и приправами Redux. А не серфить просторы инета в поисках сладких вакансий.

Вот я и на Хабре, благодаря обучению и постоянному желанию поймать свою первую компанию за хвост и устроиться Junior разработчиком.
Здесь научился заполнять аккаунт таким образом, чтобы быть не привлекательным, а техничным. Смешно, конечно, когда смотря на резюме Senior разработчика, там написано только знание 2-3 технологий и нет волшебных слов, которые можно скопипастить себе в профиль.
Но главное писать то в чем ты точно разбираешься. Я так и сделал! Жаль что Junior специалистов ищут крайне редко, в наше время. Сейчас скорее возможно устроиться middle. Но это другая тема для следующего разговора.

В итоге мое резюме выглядит следующим образом:

Резюме мое первая страничкаРезюме мое первая страничка

Как это все организовало меня как личность и изменило в лучшую сторону

В итоге, друзья, две попытки устроиться куда-либо показали, что порог входа в IT-индустрию на 2021 год серьезно так подрос!

Конечно же, спустя полтора года своего пути становления разработчиком, я не опустил руки. А наоборот, только воодушевился.

Между нами говоря. я как будто очень медленно двигаюсь. Учусь, пишу что-то, читаю книги, но движение все равно медленное! Проанализировав все что делаю и делал, понял, видимо мало реализовал пет-проектов, мало практики. Нужно больше!

Сейчас план такой:

  • делать 1 пет-проект в неделю, чтобы их было больше. Это плюс для работодателя понять мой уровень, а для меня, набить руку на практике;

  • изучать алгоритмы 1 в неделю, с закреплением на практическом примере;

  • читать 10 страниц в день разнообразных книг по JavaScript, таким образом если 1\10 от всей информации прилипнет это уже победа;

  • 1 метод в неделю изучать, усваивать и закреплять на примере.

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

В просторах телеграмма или инстаграма существует масса блогов по IT, вёрстке и digital профессий. И у каждого есть возможность зайти, спросить совет, задать вопрос или узнать что-нибудь новое для себя. Какие курсы пройти, как лучше учиться и т.п. И вот в одну из таких групп меня однажды занесло.

Изучая React по гайдам Димыча IT-kamasutra путь самурая, в каком то 6 или 8 выпуске, в комментах нашел инстаграм не равнодушного человека изучающего JS + React. Попав к нему на канал телеграмм, познакомился с ребятами из разных уголков мира и все хотят что то уметь, и умеют в чем-то больше меня, а в чем-то меньше.

На какие-то вопросы я с удовольствием отвечаю и даже могу найти время разобрать с кем-то вместе методы, которые они не понимают. А иногда прошу часами или даже днями кого-нибудь помочь мне с тем что я ну никак не понимаю.

Постоянно ищу друзей и единомышленников в сфере IT. Ведь в мои почти 30 лет у меня нет знакомых или друзей, с которыми можно было бы обсуждать или пилить какие-то вещи вместе. К сожалению.

Спасибо Андрею из Питера за помощь в освоении сложных моментов и Тимуру из Владикавказа за легкое менторство. Это очень ценно парни!

Учусь писать на ReactУчусь писать на React

Точно не пожалел о своем пути

Конечно меня постоянно спрашивают знакомые, друзья и родственники. Когда ты уже будешь работать программистом? Думаю они совсем не верят в меня и находят это увлечение скорее временным или не серьезным. Но они ошибаются. Есть такая поговорка:
Никому не нужен твой успех

Я уверен, что важно не сдаваться! В любом случае нас неминуемо ждет успех если очень долго и упорно идти к цели. И если ежедневно уделять этому время!

В апреле и мае у меня был прикольный опыт, я шумел везде где мог. Выжигал просто поле под собой. Хэдхантер, Хабр Карьера, карьера от скилбокс. Куда я только не писал, как я только не пытался найти работу. Мне каждый день, не шучу, каждый день писали по работе, всегда кто-то новый. Я за эти два месяца подумал что вот-вот уже что-то выстрелит. Но увы. Поиск работы для джуна без образования оказалось более сложной задачей чем я думал.

Тогда еще в 2019 году я задался целью, изучать JavaScript минимум три года. И только после этого времени оглянуться и спросить себя на верном ли я пути?! Знаете, в нашем мире сегодня все достается очень быстро. Кредит пожалуйста за 15 минут. Пицца доставка от 30 минут. Телевизор доставим на дом.

Мы привыкли, что все чего бы не захотели, достигается быстрым путем. И это ошибка. Ведь скорость и доступность создает в нас иллюзию простоты. И когда у нас что-то не получается быстро, мы расстраиваемся и бросаем начатое. Я же готов еще 3 года положить на изучение Frontend. Сразу сегодня, готов продлить данное себе же обещание!

Радость за успехРадость за успех

На протяжении трех лет, все неудачи или успех не принесут мне каких-то плохих эмоций, ведь я не жду результата быстро! Я понимаю, все сложности на моём пути это нормально, ведь я учусь. А это не так быстро. И тогда плохие эмоции не трогают меня. А успехи заряжают и приободряют!

Продолжайте учиться каждый день, друзья.

Продолжайте делать, даже когда не понимаете.

Продолжайте стараться, даже когда нет сил.

И мы вместе однажды достигнем результата ради которого начинали.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru