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

Javascript

Перевод Прости, React, но я так больше не могу

30.06.2020 16:15:52 | Автор: admin
Недавно мне попалась фраза усталость от JavaScript, JavaScript fatigue, и я мгновенно понял, что это про меня.

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



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

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

Переломный момент


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

Всё, что я могу сказать сейчас о Recoil, это то, что, по видимому, программистам эта библиотека нравится, и то, что в ней используется нечто, называемое атомами. Может, её назвали Recoil именно поэтому?


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

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

Что дальше?


Я пока ещё стою на распутье, но у меня есть некоторые идеи.

Написание статей


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

Небольшие эксперименты с архитектурами, применяемыми в крупномасштабных проектах


Подобные эксперименты можно проводить, пользуясь бесплатным тарифным планом AWS. Раньше мне интересно было этим заниматься. Скажем, я брал простой список кофеен и, на базе SAM, делал из него нечто нереально раздутое. В этом проекте использовались лямбда-функции, DynamoDB и SNS.


Интересные эксперименты с переусложнённой архитектурой

Изучение Rust


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


Результаты исследования Stack Overflow 2020 года

Дело в том, что Rust, 5 лет подряд, лидирует в исследованиях Stack Overflow как самый любимый язык программирования.

Кроме того, меня сильно привлекает то, что Rust компилируется в WASM. А WebAssembly это ещё одна технология, которую мне хотелось бы освоить.

Для того чтобы облегчить себе жизнь, я могу начать с Yew. Это Rust-фреймворк, предназначенный для разработки веб-приложений, создателей которого вдохновила библиотека React. Yew это фреймворк, основанный на компонентах, в котором используются что-то вроде JSX.

Или, хотя это, вероятно, будет сложнее, я, возможно, попробую создать на Rust какой-нибудь инструмент командной строки.

Разработка мобильного приложения на Flutter


Я никогда особенно не интересовался мобильной разработкой. Именно поэтому я и собираюсь её попробовать.


Flutter-приложения пишут на Dart

Во Flutter меня привлекает то, что, используя этот фреймворк, мне не придётся думать о том, для iOS или для Android мне писать приложение. То, что сделано на Flutter, заработает и там, и там. Кроме того, интересным мне кажется язык программирования Dart.

Итоги


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

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

А вы устали от JavaScript?



Подробнее..

Перевод Vue.js для начинающих, урок 1 экземпляр Vue

06.07.2020 18:16:50 | Автор: admin
Сегодня мы предлагаем вашему вниманию перевод первого урока учебного курса по Vue.js для начинающих. Освоив этот урок, вы узнаете о том, что такое экземпляр Vue, и о том, как приступить к разработке собственных Vue-приложений.



Предварительные требования


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

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

image

Страница, разработкой которой мы будем заниматься

Цель урока


В этом уроке мы разберёмся с тем, как использовать Vue для вывода данных на веб-странице.

Начальный вариант кода


Мы начнём работу с очень простого HTML- и JavaScript-кода, расположенного в двух файлах.

Файл index.html:

<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width, initial-scale=1"><title>Product App</title></head><body><div id="app"><h1>Product Name</h1></div><script src="main.js"></script></body></html>

Файл main.js:

var product = "Socks";

В этом курсе в качестве среды, в которой предлагается выполнять домашние задания, используется платформа codepen.io. Соответствующие заготовки оформлены в виде CodePen-проектов. Тем, кто проходит этот курс, рекомендуется самостоятельно запускать весь код, который они здесь встречают.

В интерфейсе CodePen есть три области для кода. Это, соответственно, поля HTML, CSS и JS. Код, введённый в полях CSS и JS, автоматически подключается к веб-странице, описанной в поле HTML. То есть для того чтобы воссоздать в среде CodePen вышеприведённый пример нужно ввести в область HTML код, содержащийся в теге <body> файла index.html без последней строчки, подключающей main.js, а в область JS код main.js.


Начало экспериментов в CodePen

Использовать CodePen для запуска кода примеров необязательно. Вы вполне можете использовать какой-нибудь другой онлайн-сервис, или можете обойтись локальной средой разработки, воссоздав у себя описываемые здесь файлы.

Задача


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

Решить эту задачу нам поможет фреймворк Vue.js. Вот официальное русскоязычное руководство по нему.

Первым шагом нашей работы с Vue будет подключение фреймворка к странице. Для этого внесём изменения в файл index.html, добавив в него, прямо над кодом подключения файла main.js, следующее:

<script src="http://personeltest.ru/aways/unpkg.com/vue"></script>

Далее, в main.js, вводим следующий код, убрав из него объявление переменной product:

var app = new Vue({el: '#app',data: {product: "Socks"}})

Теперь нужно связать DOM с данными экземпляра Vue. Делается это с использованием особой HTML-конструкции, с помощью синтаксиса Mustache, при применении которого используются двойные фигурные скобки:

<div id="app"><h1>{{ product }}</h1></div>

JavaScript-выражение в фигурных скобках будет заменено на значение свойства product объекта data.

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


Данные перенесены из JavaScript на HTML-страницу

Как видите, нам удалось перенести данные из JavaScript-кода на HTML-страницу. А теперь давайте разберёмся в том, что мы только что сделали.

Экземпляр Vue


Вот схема кода, с помощью которого создают экземпляр Vue:

var app = new Vue({options})

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

Подключение экземпляра Vue к элементу веб-страницы


Обратите внимание на следующее свойство объекта с опциями, использованного при создании экземпляра Vue:

el: '#app'

С помощью этого свойства мы подключаем экземпляр Vue к элементу нашей страницы. Благодаря этому мы создаём связь между экземпляром Vue и соответствующей частью DOM. Другими словами, мы активируем Vue в элементе <div> с идентификатором app, записывая '#app' в свойство el объекта с опциями, который был использован при создании экземпляра Vue.

Размещение данных в экземпляре Vue


В экземпляре Vue имеется место для хранения данных. Эти данные описывают с помощью свойства data объекта с опциями:

data: {product: "Socks"}

К данным, хранящимся в экземпляре Vue, можно обратиться из элемента веб-страницы, к которому подключён экземпляр Vue.

Использование JavaScript-выражений в HTML-коде


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

<h1>{{ product }}</h1>

Фактически, речь идёт о том, что в двойных фигурных скобках находится JavaScript-выражение, результаты вычисления которого фреймворк подставляет в тег <h1> в качестве текста.

Важный термин: выражение


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

Когда Vue видит выражение {{ product }}, он понимает, что мы ссылаемся на данные, связанные с экземпляром Vue, используя ключ product. Фреймворк заменяет имя ключа на соответствующее ему значение. В данном случае это Socks.

Примеры выражений


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

{{ product + '?' }}{{ firstName + ' ' + lastName }}{{ message.split('').reverse().join('') }}

Знакомство с реактивностью


Причина, по которой Vue сразу же после загрузки страницы выводит в теге <h1> значение, соответствующее свойству product, заключается в том, что Vue это реактивный фреймворк. Другими словами, данные экземпляра Vue связаны со всеми местами веб-страницы, в которых есть ссылки на эти данные. В результате Vue может не только вывести данные в некоем месте страницы, но и обновить соответствующий HTML-код в том случае, если данные, на которые он ссылается, будут изменены.

Для того чтобы это доказать, давайте откроем консоль инструментов разработчика браузера и изменим значение, записанное в свойство product объекта app. Когда мы это сделаем, например, введя в консоли app.product = 'Coat', изменится и текст, выводимый на странице.

image

Изменение значения свойства product приводит к изменению текста, выводимого на веб-странице

Видите, как легко это делается?

Практикум


Добавьте к уже имеющимся в экземпляре Vue данным ключ description, содержащий текст A pair of warm, fuzzy socks. Затем выведите значение этого ключа в элементе <p>, который должен находиться ниже элемента <h1>.

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

Вот решение задачи.

Итоги


Поговорим о том, что мы сегодня изучили:

  • Мы узнали о том, как начать разработку Vue-приложения, создав экземпляр Vue, и о том, как выводить данные на веб-страницу.
  • Экземпляр Vue является корнем каждого Vue-приложения.
  • Экземпляр Vue подключается к выбранному при его создании элементу DOM.
  • Данные, хранящиеся в экземпляре Vue, можно выводить на страницу, используя синтаксис Mustache, в котором используются двойные фигурные скобки, {{ }}, содержащие JavaScript-выражения.
  • Vue это реактивный фреймворк.

Планируете ли вы пройти этот курс?

Подробнее..

Из песочницы E-learning на костылях. Исправляем курсы из Articulate Rise

02.07.2020 14:05:51 | Автор: admin
Рынок насыщен разными программами для разработки электронных курсов авторскими средствами (authoring tools). Есть продукты на любой вкус: хочешь прогу, чтобы просто конвертировать электронные презентации в HTML5? Да пожалуйста! Хочешь делать одностраничники в редакторе вроде Тильды? Держи! Хочешь создать игрушку в стиле Interactive Fiction? Да кто же тебе запретит, родной? Любой каприз, как говориться.

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

Но всё ли так гладко на самом деле? Что делать, когда кажущаяся простота разворачивается и стреляет вам в колено?

Давайте препарируем несколько популярных инструментов и посмотрим, какие неприятные сюрпризы поджидают наивных Instructional Designer-ов. Сеанс чёрной магии с последующим её разоблачением, так сказать.

Эта статья посвящается Articulate Rise.

Что это такое? Облачный сервис для разработки лонгридов. Вроде Тильды, только ориентированный на e-learning. Берёте контен, берёте готовые компоненты и в визуальном редакторе создаете веб-страницу. Добавляете электронные тесты и экспортируете в нужном вам формате (по нужному стандарту). Просто, быстро, интуитивно понятно. Изюм. Вот пример.

И в чем проблема, спросите вы?

Что ж, господа. Присаживайтесь

Как убить титульную страницу


Начнём с простого. Вот, поставьте себя на место пользователя. Идёте вы по LMS, нашли в каталоге курс, посмотрели, полюбовались картинкой на баннере, почитали описание. Поняли: хочу! Жмёте Начать. И вместо того, чтобы сразу получить всё самое вкусное, вам предлагают ещё раз посмотреть на баннер, почитать описание и нажать Начать. Неприятно? Меня, вот, бесит. Причём, создатели тулзы не догадались сделать в интерфейсе разработчика чекбокс убрать титульную страницу.

Итак, первый костыль


  1. Экспортируем курс.
  2. Открываем файл scormdriver/indexAPI.html
  3. Находим переменную strContentLocation. Это URL первой страницы, которая будет показана пользователю. По дефолту значение

    scormcontent/index.html#/preview/
    

    Чтобы пользователь переходил сразу к вкусному после хеша нужно прописать идентификатор урока.

    Как его узнать? Просто открываем scormcontent/index.html в браузере переходим к первому уроку и копируем значение из урла. Всё что после хеша. Должно получиться что-то типа:

    scormcontent/index.html#/lessons/rri34kKfn2348234
    

Как исправить контент в курсе без доступа авторскому средству


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

Костыль


  1. Экспортируем курс
  2. Открываем scormcontent/index.html, находим переменную courseData. Она содержит все данные курса в base64.
  3. Декодируем (например с помощью notepad++) и получаем уже читаемый и редактируемый JSON. С ним можно делать всякие упражнения. Я например сшивал несколько райзовских курсов в один, просто добавляя значения в массив lessons.
  4. Перед сохранением кодируем строку courseData назад в base64.
  5. Справедливости ради стоит заметить, что исправить можно не все данные. Так, например, перед рендерингом страницы Райз проверяет и изменяет гиперссылки. Об этом мы поговорим ниже.

Встраиваем веб-страницы


Райз позволяет встроить в курс фрейм и показывать в нем веб-страницы. Проблема в том, что это может быть либо курс собранный в Storyline (другое авторское средство от той же компании), либо сайт, доступный по абсолютному URL. А вот возможность положить веб-страницу в пакет с курсом и сослаться на неё относительно index.html нет. Более того, если вы попытаетесь прописать абсолютную ссылку, экспортировать курс, а затем поменять её на относительную, как описано выше, ничего не выйдет. Райз проверит URL перед рендерингом страницы и если в нем не будет подстроки :// просто тихонько загнётся.

Костыль


  1. Добавляем на страницу объект из Storyline
  2. Узнаем его id. Либо методом научного тыка, либо через courseData.
  3. Экспортируем курс. Находим в scormcontent/assets папку с соответствующим id.
  4. Заменяем содержимое папки. Свою страницу сохраняем как story.html

Удаляем название курса из темы письма при переходе по ссылке mailto


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

Костыль


  1. Экспортируем курс. Открываем scormcontent/lib/main.bundle.js.
  2. Находим во всем этом месиве подстроку
    (w="mailto:".concat(E,"?subject=")).call(w,o)
    
  3. Меняем на
    (w="mailto:".concat(E,"")).call(w,"")
    

Заставляем Райз отдавать в LMS больше скормовых данных и делать это лучше


Да, я до сих пор работаю по SCORM 2004 4th edition. Более того, мне нравится этот стандарт. Но мне не нравится, как его трактуют создатели некоторых авторских средств. Райз, например, отправляет в LMS жалкую кучку переменных, причем делает это исключительно перед разрывом сессии (один единственный Commit() перед Terminate(), для тех кто понимает).
Например, я хочу, чтобы курс по ходу пьесы сохранял сведения о прогрессе пользователя в LMS, чтобы выводить это значение в интерфейс LMS, а не только на счетчик на сайдбаре внутри самого курса.

Костыль


  1. Экспортируем курс
  2. Открываем scormcontent/index.html
  3. Добавляем в массив stuffToPick значения:
    stuffToPick = [    ...,    "CommitData",    "GetProgressPeasure",    "SetProgressMeasure"]
    
  4. Объект root.Runtime дополняем методами:
    root.Runtime= {    ...    commitData: LMSProxy.CommitData,    getProgressMeasure: LMSProxy.GetProgressMeasure,    setProgressMeasure: LMSProxy.SetProgressMeasure}
    

  5. Дописываем функцию setCourseProgress:

    setCourseProgress(courseProgress) {    var lastProgress = root.Runtime.getProgressMeasure();    if (lastProgress < progress.p/100) {        root.Runtime.setProgressMeasure(progress.p/100);        root.Runtime.commitData();    }}
    

  6. В качестве бонуса, можно заставить курс коммитить почаще для профилактики застревания данных при разрыве сессии. Для этого добавляем root.Runtime.commitData(); в конфе функций completeOut, и reportAnswer.

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

В следующий раз мы будем издеваться над iSpring.
Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 422 (29 июня 5 июля 2020)

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


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


habr Обзор технологий скроллинга
habr Хватит это верстать, ударим автоматизацией по макетам
Google запустили бета-версию плагина для публикации AMP-сториз в WordPress
en Полное руководство по темному режиму в вебе
en Темные времена веб-разработки
en Выделение фрагмента текста: как сделать ссылку на конкретный фрагмент текста на веб-странице и подсветить его
en Прогрессивные веб-приложения: руководство по практическому использованию






CSS


habr TailwindCSS очередной фреймворк или новый шаг эволюции?
habr Упрощаем фоновые рисунки c помощью конических градиентов
habr Стилизация контейнеров для содержимого веб-страниц
Создание простой страницы лендинга за 5 минут используя готовые CSS блоки tailwind.
en Accordion Rows в CSS Grid
en Производительность CSS Painting vs. CSS Houdini Paint API
en Ссылки нестандартной формы с помощью Subgrid
en Получение значений CSS Translate с помощью JavaScript
en Выравнивание изображений логотипа в CSS
en Динамический импорт CSS
en Необычные свойства CSS
en Адаптирующиеся изображения в изменчивых пропорциях контейнера
en Когда строка не разрывается. О вариантах реализации принудительных переносов в списках
en Новое в Chrome: CSSOverview
en Когда Sass и новые функции CSS сталкиваются
en Полное руководство по медиа-запросам CSS


JavaScript


habr Устройство ленивой загрузки в популярных фронтенд-фреймворках
habr Как получить размеры экрана, окна и веб-страницы в JavaScript
habr Избушка на обратно-совместимых ножках компилируем JS для нужных браузеров
Кортежи в JS/ES и TypeScript в 2020
video en Почему подход vanilla JavaScript first может быть НЕ лучшим выбором
en Техническое руководство по SEO с Gatsby.js
en Вашему блогу не нужен JavaScript фреймворк
en Изучение регулярных выражений: руководство для начинающих









Браузеры


Релиз Firefox 78
Firefox78, технические подробности для разработчиков
Новый Edge на базе Chromium распространяется вместе с принудительным обновлением ОС и навязчиво ведёт себя
Google Chrome побил собственный рекорд, а Windows 10 продолжает расти

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



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

Портируем старую игру в жанре shoot em up на JavaScript на коленке

30.06.2020 02:04:50 | Автор: admin

Имеется древняя игрушка LaserAge, которая написана на Flash (на очень древнем Macromedia Flash 4) и работает только под Windows. В детстве она мне очень понравилась, поэтому я решил для души портировать её, чтобы можно было играть с браузера со всех устройств.


Цель игры заключается в том, чтобы уничтожать противников своим космическим кораблём на различных уровнях и получать бонусы, если поймать бонус улучшается оружие. При попадании торпеды противника даунгрейд оружия игрока.
При уничтожении всех противников на уровне происходит переключение на следующий уровень. Всего 100 уровней.
В терминах игры уровень волна (Wave), а несколько волн объединены в большой уровень (Level), который представляет из себя просто смену заднего фона,
т. е. всего 4 больших уровня в каждом из которых 25 волн. В последней волне большого уровня обычно бывает босс противник с огромным значением жизни и мощным оружием.


http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/LaserAgeNext.png?raw=true


[TOC]


Бизнес логика игры


Игровое пространство


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


http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/Stage.png?raw=true


Оружие


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


Оружие космического корабля игрока


  • Торпеда стреляет маленькими ракетами
    • Одинарная Торпеда 1 уровень апгрейда
    • Двойная 2 уровень апгрейда
    • Тройная 3 уровень апгрейда
  • Автоматические пушки
    • Дополнительная автоматическая Торпеда слева корабля 4 уровень апгрейда
    • Дополнительная автоматическая Торпеда справа корабля 5 уровень апгрейда
  • Зелёная плазма 6 и 7 уровень апгрейда (увеличивается скорострельность)
  • Фиолетовая плазма 8 уровень апгрейда (наносит урон всем противникам по траектории полёта)
  • Зелёный лазер 9 уровень (наносит урон всем противникам, а также активно одну секунду, тем самым можно задеть соседних противников)

Дополнительное оружие:


  • Красная плазма 15-19 уровень (наносит урон всем противникам, а также активно одну секунду, тем самым можно задеть соседних противников)
  • Зелёная плазма 20-24 уровень
  • Синяя плазма 25-29 уровень апгрейда
  • Фиолетовая плазма 30-34 уровень апгрейда
  • Фиолетовая плазма 30-34 уровень апгрейда
  • Дополнительная автоматическая Торпеда слева стреляет желтой плазмой 35 39 уровень апгрейда
  • Дополнительная автоматическая Торпеда справа стреляет желтой плазмой 40+ уровень апгрейда

Таблица с характеристиками оружия игрока


Оружие Hit Points Скорость спрайта Интенсивность Тип Дополнительно Вид
Торпеда 1 5 25 Торпеда Одинарная, двойная, тройная
Автоматическая Торпеда 1 5 50 Торпеда Слева и Справа
Зелёная плазма 3 7 30 Торпеда
Фиолетовая плазма 2 8 30 Торпеда Атакует до 3х целей
Красная плазма 2 4 30 Торпеда
Синяя плазма 4 4.5 30 Торпеда
Жёлтая плазма 2 3.8 40 Торпеда Только автоматическая
Зелёный Лазер 4 - 15/55 Лазер Атакует до 5ти целей одновременно

Таблица с конфигурацией оружия игрока в зависимости от уровня жизни


Уровень жизни Конфигурация оружия
1 Торпеда
2 Торпеда + Торпеда
3 Торпеда + Торпеда + Торпеда
4 Торпеда + Торпеда + Торпеда + Автоматическая торпеда слева
5 Торпеда + Торпеда + Торпеда + Автоматическая торпеда слева + справа
6 Зелёная плазма + Автоматическая торпеда слева + справа
7 Зелёная плазма + Автоматическая торпеда слева + справа
8 Фиолетовая плазма + Автоматическая торпеда слева + справа
9 Зелёный лазер + Автоматическая торпеда слева + справа
15 19 Зелёный лазер + Красная плазма + Автоматическая торпеда слева + справа
20 24 Зелёный лазер + Красная плазма + Автоматическая торпеда слева + справа
25 29 Зелёный лазер + Синяя плазма + Автоматическая торпеда слева + справа
30 34 Зелёный лазер + Фиолетовая плазма + Автоматическая торпеда слева + справа
35 39 Зелёный лазер + Фиолетовая плазма + Автоматическая желтая плазма слева + торпеда справа
40+ Зелёный лазер + Фиолетовая плазма + Автоматическая желтая плазма слева + желтая плазма справа

Оружие противников


Таблица с конфигурацией оружия противников


Оружие Скорость спрайта Тип
Торпеда 2.5 Торпеда
Красная плазма 3.5 Торпеда
Синяя плазма 4.5 Торпеда
Зелёная плазма 5 Торпеда
Синяя Торпеда 3 Торпеда
Жёлтая плазма 3.2 3.8 Торпеда
Белая плазма 4 6 Торпеда
Зелёный Лазер - Лазер

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


Пример конфигурации оружия:


"torpedo": {    "sprite": "Bullet1_1.png", //картинка спрайта    "isRandomIntensity": false, //нужно ли переключать случайно слоты - true или по порядку - false    "intensity": [        //слот 0        {            "min": 50, //минимальное число фреймов            "max": 200, //максимальное число фреймов            "type": "pause" //pause - оружие неактивно, shoot - активное (стреляет)        },        //слот 1        {            "min": 100,            "max": 200,            "type": "shoot"        },        {            "min": 50,            "max": 80,            "type": "pause"        },        {            "min": 30,            "max": 100,            "repeat": 2        }    ],    "speed": 2.5, //скорость    "type": "bullet", //тип оружия    "sound": "alienTorpedo"}

Действующие лица


Корабль игрока


Корабль игрока может перемещаться в ограниченной области, чтобы не пересекаться с кораблями противников.
Управляется движением мыши или стрелочками и . На экране мобильного телефона тапом и движением по экрану.
Оружие активирует при удержании левой клавиши мыши (тапом и удержанием по экрану на мобильном телефоне).


Противники


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


Корабль противника Жизнь Тип Движения Оружие Вид
Чужой 1 2 Обычный Нормальное горизонтальное Торпеда
Чужой 2 4 Обычный Нормальное все направления Торпеда
Быстрый чужой 10 Обычный Быстрое горизонтальное Торпеда (Интенсивная)
Фрегат чужого 10 Обычный Нормально-быстрое все направления Красная плазма
Броневик чужого 10 Обычный Медленное вниз Торпеда (Очень интенсивная)
Быстрый Фрегат чужого 30 Обычный Медленное вниз (следит за игроком) Красная плазма (Очень интенсивная)
Красный истребитель 30 Обычный Медленное вниз (следит за игроком) Синяя плазма
Зелёный истребитель 30 Обычный Быстро вертикально Синяя плазма
Чужой 1 модификация 2 Обычный Нормальное горизонтальное Синяя Торпеда
Бомбардировщик 30 Обычный Нормальное все направления (следит за игроком) Зелёная плазма
Тяжёлый Чужой 30 Обычный Нормальное все направления Торпеда
Тяжёлый Фрегат Чужого 35 Обычный Нормальное все направления Синяя Торпеда + Синяя Торпеда
Тяжёлый броневик 35 Обычный Нормальное вниз Жёлтая Плазма + Жёлтая Плазма + Жёлтая Плазма + Жёлтая Плазма
Линкор 100 Босс Нормальное все направления Синяя плазма (очень интенсивная) + Зелёная плазма (очень интенсивная)
Крейсер 250 Босс Нормальное все направления Зелёная плазма (сверх интенсивная)
Тяжёлый Крейсер 500 Босс Быстрое все направления Жёлтая Плазма + Жёлтая Плазма + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Белая плазма + Белая плазма
Эпичный Тяжёлый Крейсер 1000 (восстанавливается) Босс Быстрое все направления Жёлтая Плазма + Жёлтая Плазма + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Белая плазма + Белая плазма+ Зелёная плазма (очень интенсивная)

JSON-конфигурация противника:


"alien10": {    "life": 35,    "weapons": [        {            "weapon": "blueTorpedo",            "position": {                "x": -6,                "y": 0            }        },        {            "weapon": "blueTorpedo",            "position": {                "x": 6,                "y": 0            }        }    ],    "sprite": "AlienShip10_1.png",    "movement": "horizontalFast",    "killPoints": 2100}

JSON-конфигурация движения противника :


"horizontalFast": {    "movements": [        {            "type": "freeMovement", //freeMovement - обычное, followPlayer - следит за игроком (движется в направление)            "speedDelta": {                "vx": -6,                "vy": 0            },            "intensity": [ //интенсивность движения в виде слотов                {                    "min": 20,                    "max": 150                },                {                    "min": 150,                    "max": 350                }            ]        }    ]}

Бонусы


Специальный вид противника http://personeltest.ru/aways/raw.githubusercontent.com/EntityFX/laseroid/master/resources/laser-age/graphics/PowerUps_1.png , который не имеет оружия и при уничтожении порождает спрайт с бонусом http://personeltest.ru/aways/raw.githubusercontent.com/EntityFX/laseroid/master/resources/laser-age/graphics/Upgrade.png , который должен поймать корабль игрока. Если игрок поймает бонус, то увеличивается его уровень (жизнь).


Уровни


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


JSON-конфигурация уровня :


        "2": {            "level": 1,             "enemies": [ // список противников                {                    "id": "alien1",                    "position": {                        "x": 200,                        "y": 35                    }                },                //...                {                    "id": "alien1",                    "position": {                        "x": 525,                        "y": 40                    }                }            ],            "bonuses": [ // список бонусов                {                    "id": "bonus1",                    "position": {                        "x": 350,                        "y": 10                    }                }            ]        },

Выбор JavaScript библиотеки для реализации


Я просмотрел множество библиотек графики для JavaScript, но остановился на Hexi JS: https://github.com/kittykatattack/hexi .


Возможности библиотеки:


  • Простота
  • Рисование примитивов
  • Рисование просты интерфейсов (кнопки, события)
  • Перемещение, масштабирование, вращение
  • Рисование спрайтов
    • Анимированные спрайты
    • Работа со спрайтами как с объектами
    • Загрузка спрайтов в виде большой текстуры-атласа. Можно разместить множество изображений в одном файлы и на выходе получить одну большую текстуру и JSON файл с описанием спрайтов (область, смещение)
  • Логика столкновений
  • Работа с устройствами ввода (клавиатура), тач-скрин.

Пример текстуры-атласа создаваемого с помощью программы TexturePacker
http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/ships-atlas-texture.png?raw=true


Звуковая библиотека: https://github.com/kittykatattack/sound.js


Возможности библиотеки:


  • Простота
  • Воспроизведение звуков
  • Воспроизведение музыки
  • Эффекты

Архитектура


Общая диаграмма классов:


http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/diagrams/game.png?raw=true


Ядро игры


http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/diagrams/core.png?raw=true


Класс Main


Является точкой входа и контейнером игрового кода.


Поля:


  • resources содержит список всех загружаемых ресурсов (текстуры, звук, json)
  • sounds словарь звуков: Ключ название, Значение путь
  • gameScene объект HexiJS на
  • game экземпляр объекта Game
  • hexi инстанс HexiJS
  • gameStorage сохраняет состояние игры в localStorage

Методы:


  • init() инициализирует HexiJS
  • load() загружает ресурсы (текстуры, звук, json)
  • setup() устанавливает игровую область, события нажатия кнопок, запускает фоновую музыку
  • playLoop() точка изменения состояния игры (считает движение, коллизии, снаряды, перерисовывает пространство).
  • saveGame() сохраняет игру
  • loadGame() загружает игру

Пример списка ресурсов текущей реализации игры:


Main.resources = [        "images/environment1.png",        "images/environment2.png",        "images/environment3.png",        "images/environment4.png",        "images/interface.png",        "images/life-icon.png",        "images/ships-texture.json",        "images/bullet-texture.json",        "sounds/alien-torpedo-shoot.wav",        "sounds/alien-red-plasma-shoot.wav",        "sounds/hero-torpedo-shoot.wav",        "sounds/explode.wav",        "sounds/hero-green-plasma-shoot.wav",        "sounds/alien-green-plasma-shoot.wav",        "sounds/alien-blue-torpedo-shoot.wav",        "sounds/alien-yellow-laser.wav",        "sounds/pulse-plasma.wav",        "sounds/laser.wav",        "sounds/track0.ogg",        "sounds/track1.ogg",        "sounds/track2.ogg",        "sounds/track3.ogg",        "sounds/track4.ogg",        "data/hero-configuration.json",        "data/levels-configuration.json",        "data/enemy-configuration.json",        "data/ui-configuration.json",    ];

Класс Game


Основной класс игры.


Поля:


  • level информация о уровне. Значение: { "wave": 1 //номер волны, "type": 1 }
  • score информация об очках. Значение: {"points": 0 }
  • bulletsController Экземпляр класса BulletsController. Управляет поведение торпед и лазеров оружия
  • enemyController Экземпляр класса EnemyController. Управляет поведением всех противников на уровне (в т.ч. и бонусами)
  • player Экземпляр Player
  • hexi экземпляр класса Hexi (ссылка)
  • game экземпляр объекта Game
  • gameStorage экземпляр объекта GameStorage

Методы:


  • clearShips() очистка всех проиивников, бонусов
  • setupLevel() настроить уровень (добавить противников, бонусы, расстановка)
  • nextLevel() переход на следующий уровень
  • previousLevel() переход на предыдущий уровень
  • forwardLevel() перепрыгнуть на несколько уровней вперёд (на 5)
  • rewindLevel() перепрыгнуть на несколько уровней назад (на 5)
  • restoreState(gameState: JSON) восстановить по объекту gameState
  • resetGame() сбросить игру (начать сначала)
  • update() обновить игровой мир
  • enemyDestroyed() обработчик срабатывает при уничтожении всех противников

Класс GameStorage


Сохраняет и загружает состояние игры .


Поля:


  • game экземпляр объекта Game

Методы:


  • save() сохранить состояние игры
  • load() загрузить состояние игры

Класс InputDevice


Работает с событиями устройств ввода: click и touch кнопок, нажатие клавиш клавиатуры.


Поля:


  • game экземпляр объекта Game

Методы:


  • init() инициализирует все обработчики события и callback'и
  • loadTapped() нажата кнопка "Load"
  • storeTapped() нажата кнопка "Store"
  • resetTapped() нажата кнопка "Reset"
  • pauseTapped() нажата кнопка "Pause"

Иерархия классов действующих лиц


http://personeltest.ru/aways/github.com/EntityFX/laseroid/blob/master/doc/diagrams/actors.png?raw=true


Actor


Класс участника.


Поля:


  • hexi экземпляр класса Hexi (ссылка)
  • game экземпляр объекта Game
  • life текущее значение жизни
  • initialLife начальное значение жизни
  • sprite экземпляр класса Hexi.Sprite
  • shipConfiguration конфигурация бонуса

Методы:


  • move() переместить действующее лицо
  • update() обновить действующее лицо
  • setPosition(position: {x, y}) установить по координатам

WeaponedActor


Класс участника (противник или игрок) обладающем оружием.


Поля:


  • automatedWeapons массив автоматических оружий
  • canShoot мжет ли стрелять
  • isWeaponShooting активено ли оружие

Методы:


  • startShoot() запустить выстрелы оружием
  • stopShoot() остановить выстрелы оружием
  • onShootStarted() обработчик события, что запущены выстрелы оружием
  • onShootStopped() обработчик события, что остановлены выстрелы оружием
  • updateShooting() выполняет алгоритмы выстрелов

Enemy


Класс противника.


Поля:


  • type тип противника
  • syncWeapons массив конфигураций для синхронного оружия
  • movementEngine экземпляр класса MovementEngine

Методы:


  • setWeapon() установить оружие используя текущую конфигурацию
  • shootWithWeapon() выполняет выстрел противником
  • setLifeLine() рисует линию жизни противника
  • hit() проверяет столкновение торпед (лазера) игрока с текущим противником

MovementEngine


Класс управляющий движением.
Для придания сложности движения, используется конфигурация со слотами. В каждом слоте задаётся
вектор направления vx, vy и интенсивность. Имеется возможность отключения отражения от нижней границы и
режим слежения за игроком (противник всегда движется за игроком).


Поля:


  • movementsConfiguration конфигурация движения
  • firstMovementConfiguration первый элемент из списка конфигураций
  • movementItensity интенсивность движения
  • movementItensityCounter счётчик интенсивности движения
  • movementItensitySlot номер слота интенсивности
  • isBounceBottom флаг на проверку отражения от нижней границы. Если false, то противник не отражается от нижней границы

Методы:


  • setMovement() настраивает движение
  • updateMovement() обновляет движение по конфигурации движения

Player


Класс игрока.


Поля:


  • weapons массив оружия игрока
  • collisionSprite спрайт коллизии (торпеды противника сталкиваются со спрайтом коллизии, а не спрайтом игрока)
  • weaponLifeLevels значения уровня жизни для проверки на апгрейд оружия
  • invisibilityCounter счётчик невидимости от торпед (нужен для того, чтобы при столкновении с торпедой противника игрок стал временно недосягаем для других торпед)

Методы:


  • upgrade() апгрейд игрока (+1 жизнь)
  • downgrade() даунгрейд игрока (+1 жизнь)
  • shootWithLaser(currentWeapon, weapon) выстрел лазером
  • shootWithBullets(currentWeapon, weapon) выстрел торпедой
  • setWeapon() установить оружие используя текущую конфигурацию
  • setLife(life: number) установить значение жизни (меняет оружие в соответствии со значением жизни)
  • hitUpgrade(upgradeItem) проверить столкновение со спрайтом апгрейда

Bonus


Класс Бонуса. При уничтожении порождает спрайт апгрейда.


Поля:


  • type тип бонуса
  • movementEngine экземпляр класса MovementEngine
  • upgradeBonus конфигурация апгрейда

Методы:


  • shootWithUpgrade(upgradeBonus: JSON) породить спрайт апгрейда

EnemyController


Управляет состоянием противников, бонусов, апгрейдами.


Поля:


  • enemies массив всех противников на уровне
  • bonuses массив всех бонусных кораблей на уровне
  • player объект игрока
  • upgrades массив всех спрайтов апгрейда

Методы:


  • isLevelCompleted() проверка на завершённость уровня (уничтожены все противники и бонусы, пойманы апгрейды)
  • update() обновляет состояние всех противников
  • clear() очистка уровня от проиивников, бонусов

BulletsController


Управляет состоянием торпед (перемещение), лазерами игрока и противников.


Поля:


  • playerBullets массив торпед игрока
  • enemyBullets массив торпед всех противников
  • explosionSplashes массив спрайтов взрыва
  • playerLaser состояние спрайта лазера игрока (Если оружие доступно).

Методы:


  • update() обновляет состояние всех торпед, лазеров
  • clear() очищает уровень от всех торпед, лазеров
  • updatePlayerBullets() изменяет состояние всех торпед игрока
  • updatePlayerLaser() изменяет состояние лазера игрока
  • updateEnemyBullets() изменяет состояние всех торпед противника
  • updateExplosions() изменяет состояние всех взрывов

Выводы


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


Спасибо и интересных Вам проектов!


Ссылки


http://laseroid.azurewebsites.net/ сама игра
https://github.com/EntityFX/laseroid исходный код игры

Подробнее..

Spreadable вариант децентрализованной сети

30.06.2020 12:04:28 | Автор: admin

image


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


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


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


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


Вариант номер один: одноуровневый


Узлы могут выполнять две роли: master и slave (M и S). Роли распределяются рандомно и могут быть изменены в любой момент. То есть ни один узел не имеет реального преимущества над другим. Роль M нужна для ведения списков S, чтобы соединить всех в одну сеть, и в дальнейшем получать и отправлять информацию. Количество M всегда стремится к квадратному корню от размера сети: если в сети 9 узлов, то M будет 3 штуки. Каждый M ведет свой список S. Количество S в одном таком списке тоже всегда стремится к квадратному корню. В итоге, при 9 узлах у нас будет 3 M и у каждого в списке по 3 S. Информация о том, какие узлы выполняют роль M передается и хранится у всех узлов в режиме синхронизации.



Обход сети происходит следующим образом:


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

Давайте посчитаем сколько времени на все это уходит. Рассмотрим идеальный вариант. Допустим среднее время одного запроса 50ms. Тогда у нас получается общее время 3 * 50 = 150 ms вне зависимости от размера сети.



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


Плюсы данного алгоритма:


  • Относительная легкость в реализации
  • Очень быстрый обход сети при большом количестве узлов
  • Высокая устойчивость к подключению/отключению узлов и смене ролей
  • Приемлемое количество работы для предотвращения атак Сивиллы
  • Максимальная близость к одноранговости, несмотря на гибридность

Минусы:


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

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


Вариант номер два: многоуровневый


Этот алгоритм почти тот же, только уровней вложенности списков становится бесконечное количество. Размер списка узлов выбирается опционально, а не квадратный корень. Возьмем число 2 для дальнейших примеров. Роль мастеров становится иерархической. Появляются мастера первого уровня, второго и.т.д. (M1, M2 ...). M3 формируют списки M2, M2 -> M1, M1 -> S.



В такой схеме изначально у нас один уровень. Как только узлов станет больше 4, появится новый уровень и первый M2, когда узлов станет больше 6, появится третий уровень и M3, после 12 узла мы получаем уровень четыре и так далее.


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



Плюсы данного алгоритма:


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

Минусы:


  • Сложность реализации
  • Большое количество работы для предотвращения атак Сивиллы

Безопасность


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


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

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


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


Голосование


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


Создавая класс, мы можем передавать некоторые базовые опции:


  • approversCount количество узлов из всех имеющихся, которые сформируют совет голосующих. По умолчанию, это квадратный корень от размера сети, но можно указать процент от размера или просто какое-то число.
  • decisionLevel достаточное количество голосов для положительного исхода. По умолчанию, 66.6%. Можно указать просто число.
  • period промежуток времени на которое будет сформирован совет голосующих. По умолчанию, 5 минут.

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


Давайте рассмотрим как выбираются узлы для голосования. Для каждого узла вычисляется хеш, исходя из его уникальных данных и временного периода. Допустим, approversCount=3, decisionLevel=2, period=10m для какого-то действия. Все полученные хеши сортируются, и первые 3 узла в массиве имеют право голосовать в течение десятиминутного периода. Как только наступит следующая десятиминутка, хэши узлов поменяются, и голосующими станет уже какой-то другой массив узлов.



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



Исходя из настроек в примере, для того, чтобы "взломать алгоритм" нужно иметь контроль над 2 узлами, рассчитать когда именно эти узлы станут одновременно голосующими (при небольшом улучшении алгоритма, можно убрать возможность расчета заранее) из всех имеющихся, переписать кучу кода и тогда, в течение какого-то десятиминутного периода раз в N времени можно делать запросы без проверок.


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


Каптча тут реализована вышеописанным путем.


Синхронизация


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


Библиотека


Все написано на nodejs. Рассмотрим пример использования:


Сервер:


const  Node  =  require('spreadable').Node;(async () => {try {const node =  new  Node({port: 4000,hostname: 'localhost',initialNetworkAddress: 'localhost:4000'});await node.init();}catch(err) {console.error(err.stack);process.exit(1);}})();

Клиент:


const  Client  =  require('spreadable').Client;(async () => {try {const client =  new  Client({address: 'localhost:4000'});await client.init();}catch(err) {console.error(err.stack);process.exit(1);}})();

Для поднятия узла работаем с классом Node. Для подключения и работы с сетью используем Client.


Некоторые особенности:


  • Сеть работает через http протокол. Можно также настроить https.
  • Чтобы запустить узел нужно как минимум указать порт (port) и точку входа (initialNetworkAddress). Точка входа это любой другой узел, который уже зарегистрирован в сети. Также можно вручную передать имя хоста (hostname).
  • Идентификатором узла является адрес. Он формируется как хост: порт. Хост может быть доменным именем или IP-адресом. Для ipv6 это [ip]: порт.
  • Сеть может быть как открытой, так и закрытой. Для ее закрытия можно использовать базовую аутентификацию или перезаписать методы фильтрации запросов.
  • Клиент библиотеки изоморфен, можно работать из браузера.
  • Можно работать с узлом из командной строки.

Более подробно информация выше описана в readme.


Чего пока нет


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


На данный момент, можно посмотреть как все наследовать на примере уже имеющихся расширений: metastocle, storacle, museria. Если будет смысл, то напишу как-нибудь статью как расширять и использовать библиотеку для своего проекта.


Мои контакты:


Подробнее..

Живые интерактивные логи визуализация логов в Voximplant Kit

30.06.2020 14:13:12 | Автор: admin

Мы продолжаем обновлять Voximplant Kit с помощью JointJS. И рады сообщить о появлении живых логов (live logs) звонков. Насколько они живые и опасны ли для простых юзеров, читайте под катом.

Ранее для анализа звонков в Voximplant Kit пользователям были доступны лишь записи разговоров. Нам же хотелось в дополнение к аудио сделать не просто текстовый лог, а более удобный инструмент для просмотра деталей звонка и анализа ошибок. И поскольку мы имеем дело с low-code/no-code продуктом, появилась идея визуализации логов.

В чем соль?/ Новый концепт


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


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


Управление


Контролы старт\стоп (1) останавливают/возобновляют воспроизведение, а назад\далее (2) точечно перемещают юзера к началу следующего/предыдущего блока. Можно также просто кликать по таймлайну, чтобы начать воспроизведение с определенного момента времени, как с проигрыванием песни.

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


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


Спойлер:
Во вкладке Лог мы планируем показывать детали блоков. Они помогут нам понять, почему из блока вышли по определенному порту и были ли ошибки. Например, для блока распознавания мы увидим результаты и ошибки распознавания.
Наибольший интерес здесь будут представлять сложные блоки, такие как DialogFlowConnector, IVR, ASR и т.д.


Переменные


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


Лайфхак


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

Самостоятельно пощупать логи можно на Voximplant Kit.

Так, а что внутри?


Разберемся, как именно динамические логи реализованы в коде. Скажем сразу, от Joint JS мы взяли лишь анимацию и выделение блоков, как в деморежиме. Остальное (что можно на основе этого сделать) наша фантазия.

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

Получаем timepointы


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

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

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

Обновляем временную шкалу


После нажатия кнопки play временная шкала начинает обновляться каждые 10 мс. Во время каждого обновления проверяем, совпадает ли текущее время с одним из timepointов:

const found = this.timePoints.find((item) => item === this.playTime);

Если совпадение есть, будем искать все блоки у которых timepoint = текущее время + 600 мс (время, за которое происходит анимация перемещения между блоками).

Код метода updatePlayTime():

updatePlayTime(): void {    const interval = 10;    let expected = Date.now() + interval;    const tick = () => {        const drift = Date.now() - expected;        const found = this.timePoints.find((item) => item === this.playTime);        this.$emit('update', {            time: this.playTime,            found: found !== undefined        });        if (this.playTime >= this.duration) {            this.isPlay = false;            this.playTime = this.duration;            clearTimeout(this.playInterval);            this.$emit('end', this.playTime);            return;        }        expected += interval;        this.playTime += 0.01;        this.playTime = +this.playTime.toFixed(2);        this.updateProgress();        this.playInterval = window.setTimeout(tick, Math.max(0, interval - drift));    };    this.playInterval = window.setTimeout(tick, 10);}

Так же каждые 90 мс мы проверяем совпадения для текущего времени и timepoint'ов у измененных переменных + 4000 мс (время, в течение которого висит уведомление об изменении переменной).

Выделяем блоки


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

Если блоков с timepoint = текущее время + 600 мс несколько, то переход анимируется только к последнему:

if (i === blocks.length - 1) {    await this.selectBlock(blocks[i], 600, true, true);}

Это необходимо, поскольку есть блоки, которые обрабатываются очень быстро. Например, Проверка данных, Изменение данных и т.п. за 1 секунду может быть пройдено сразу несколько блоков. Если их анимировать последовательно, то возникнет отставание от времени таймлайна.

Код метода onUpdateTimeline:

async onUpdateTimeline({    time,    found}) {    this.checkHistoryNotify();    if (!found) return;    // Выделяем группу блоков от первой найденной точки + 600мс    const blocks = this.callHistory.log_path.filter((item) => {        return item.timepoint >= this.logTimer && item.timepoint < this.logTimer + 600;    });    if (blocks.length) {        this.editor.unselectAll();        for (let i = 0; i < blocks.length; i++) {            if (i === blocks.length - 1) {                await this.selectBlock(blocks[i], 600, true, true);                const cell = this.editor.getCellById(blocks[i].idTarget);                this.editor.select(cell);            } else {                await this.selectBlock(blocks[i], 0, false, true);            }        }    }}


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

В этом нам помогает метод selectBlock():

async selectBlock(voxHistory, timeout = 700, animate = true, animateLink = true) {    const inQueue = this.selectQueue.find((item) => item[0].targetId === voxHistory.idTarget);    if (!inQueue) this.selectQueue.push(arguments);    return this.exeQueue();}


Перематываем


При перемотке тот же принцип: когда таймлайн переместили, мы получаем время, на которое нужно перемотать и отмечаем блоки с timepoint'ами меньше текущего времени как пройденные:

const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });

Анимированный переход делаем к последнему из них.

Код метода onRewind():

async onRewind({    time,    accurate}, animation = true) {    this.editor.unselectAll();    this.stopLinksAnimation();    this.checkHistoryNotify(true);    const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });    for (let i = 0; i < forSelect.length; i++) {        if (i === forSelect.length - 1) {            await this.selectBlock(forSelect[i], 600, animation, false);            const cell = this.editor.getCellById(forSelect[i].idTarget);            this.editor.select(cell);        } else {            await this.selectBlock(forSelect[i], 0, false, false);        }    }    if (this.isPlay) this.restartAnimateLink();    this.onEndTimeline();}

Проигрываем аудио


С включение/выключением аудиозаписи дела обстоят еще проще. Если время таймлайна совпадает со стартом записи, она начинает проигрываться и далее время синхронизируется. За это отвечает метод updatePlayer():

updatePlayer() {    if (this.playTime >= this.recordStart && this.player.paused && !this.isEndAudio) {        this.player.play();        this.player.currentTime = this.playTime - this.recordStart;    } else if (this.playTime < this.recordStart && !this.player.paused) {        this.player.pause();    }}

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

Здорово, если вам нравится наша серия статей про обновления Кита. Будем и дальше делиться с вами самым свежим и интересным!
Подробнее..

Лабаем на MIDI клавиатуре в Angular

30.06.2020 14:13:12 | Автор: admin

Web MIDI API интересный зверь. Хоть он и существует уже почти пять лет, его все еще поддерживает только Chromium. Но это не помешает нам создать полноценный синтезатор в Angular. Пора поднять Web Audio API на новый уровень!



Ранее я рассказывал про декларативную работу с Web Audio API в Angular.


Программировать музыку, конечно, весело, но что если мы хотим ее играть? В 80-е годы появился стандарт обмена сообщениями между электронными инструментами MIDI. Он активно используется и по сей день, и Chrome поддерживает его на нативном уровне. Это значит, что, если у вас есть синтезатор или MIDI-клавиатура, вы можете подключить их к компьютеру и считывать то, что вы играете. Можно даже управлять устройствами с компьютера, посылая исходящие сообщения. Давайте разберемся, как это сделать по-хорошему в Angular.


Web MIDI API


В интернете не так много документации на тему этого API, не считая спецификации. Вы запрашиваете доступ к MIDI-устройствам через navigator и получаете Promise со всеми входами и выходами. Эти входы и выходы еще их называют портами являются нативными EventTargetами. Обмен данными осуществляется через MIDIMessageEventы, которые содержат Uint8Array сообщения. В каждом сообщении не более 3 байт. Первый элемент массива называется status byte. Каждое число означает конкретную роль сообщения, например нажатие клавиши или движение ползунка параметра. В случае нажатой клавиши второй байт отвечает за то, какая клавиша нажата, а третий как громко нота была сыграна. Полное описание сообщений можно подсмотреть на официальном сайте MIDI. В Angular мы работаем с событиями через Observable, так что первым шагом станет приведение Web MIDI API к RxJs.


Dependency Injection


Чтобы подписаться на события, мы сначала должны получить MIDIAccess-объект, чтобы добраться до портов. navigator вернет нам Promise, а RxJs превратит его для нас в Observable. Мы можем создать для этого InjectionToken, используя NAVIGATOR из @ng-web-apis/common. Так мы не обращается к глобальному объекту напрямую:


export const MIDI_ACCESS = new InjectionToken<Promise<MIDIAccess>>(   'Promise for MIDIAccess object',   {       factory: () => {           const navigatorRef = inject(NAVIGATOR);           return navigatorRef.requestMIDIAccess               ? navigatorRef.requestMIDIAccess()               : Promise.reject(new Error('Web MIDI API is not supported'));       },   },);

Теперь мы можем подписаться на все MIDI-события. Можно создать Observable одним из двух способов:


  1. Создать сервис, который наследуется от Observable, как мы делали в Geolocation API
  2. Создать токен с фабрикой, который будет транслировать этот Promise в Observable событий

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


export const MIDI_MESSAGES = new InjectionToken<Observable<MIDIMessageEvent>>(   'All incoming MIDI messages stream',   {       factory: () =>           from(inject(MIDI_ACCESS).catch((e: Error) => e)).pipe(               switchMap(access =>                   access instanceof Error                       ? throwError(access)                       : merge(                             ...Array.from(access.inputs).map(([_, input]) =>                                 fromEvent(                                     input as FromEventTarget<MIDIMessageEvent>,                                     'midimessage',                                 ),                             ),                         ),               ),               share(),           ),   },);

Если нам нужен какой-то конкретный порт, например если мы хотим отправить исходящее сообщение, достанем его из MIDIAccess. Для этого добавим еще один токен и подготовим фабрику для удобного использования:


export function outputById(id: string): Provider[] {   return [       {           provide: MIDI_OUTPUT_QUERY,           useValue: id,       },       {           provide: MIDI_OUTPUT,           deps: [MIDI_ACCESS, MIDI_OUTPUT_QUERY],           useFactory: outputByIdFactory,       },   ];}export function outputByIdFactory(   midiAccess: Promise<MIDIAccess>,   id: string,): Promise<MIDIOutput | undefined> {   return midiAccess.then(access => access.outputs.get(id));}

Кстати, вы знали, что нет необходимости спрэдить массив Provider[], когда добавляете его в метаданные? Поле providers декоратора @Directive поддерживает многомерные массивы, так что можно писать просто:

providers: [  outputById(someId),  ANOTHER_TOKEN,  SomeService,]

Если вам интересны подобные практичные мелочи про Angular приглашаю почитать нашу серию твитов с полезными советами.

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


Операторы


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


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

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


export function filterByChannel(   channel: MidiChannel,): MonoTypeOperatorFunction<MIDIMessageEvent> {   return source => source.pipe(filter(({data}) => data[0] % 16 === channel));}

Status byte организован группами по 16: 128143 отвечают за нажатые клавиши (noteOn) на каждом из 16 каналов. 144159 за отпускание зажатых клавиш (noteOff). Таким образом, если мы возьмем остаток от деления этого байта на 16 получим номер канала.


Если нас интересуют только сыгранные ноты, поможет такой оператор:


export function notes(): MonoTypeOperatorFunction<MIDIMessageEvent> {   return source =>       source.pipe(           filter(({data}) => between(data[0], 128, 159)),           map(event => {               if (between(event.data[0], 128, 143)) {                   event.data[0] += 16;                   event.data[2] = 0;               }               return event;           }),       );}

Некоторые MIDI-устройства отправляют явные noteOff-сообщения, когда вы отпускаете клавишу. Но некоторые вместо этого отправляют noteOn сообщение с нулевой громкостью. Этот оператор нормализует такое поведение, приводя все сообщения к noteOn. Мы просто смещаем status byte на 16, чтобы noteOff-сообщения перешли на территорию noteOn, и задаем нулевую громкость.

Теперь можно строить цепочки операторов, чтобы получить стрим, который нам нужен:


readonly notes$ = this.messages$.pipe(  catchError(() => EMPTY),  notes(),  toData(),);constructor(  @Inject(MIDI_MESSAGES)  private readonly messages$: Observable<MIDIMessageEvent>,) {}

Пора применить все это на практике!


Создаем синтезатор


С небольшой помощью библиотеки для Web Audio API, которую мы обсуждали ранеесоздадим приятно звучащий синтезатор всего за пару директив. Затем мы скормим ему ноты, которые играем через описанный выше стрим.


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


readonly notes$ = this.messages$.pipe(  catchError(() => EMPTY),  notes(),  toData(),  scan(    (map, [_, note, volume]) => map.set(note, volume), new Map()  ),);

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



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


@Pipe({    name: 'adsr',})export class AdsrPipe implements PipeTransform {    transform(        value: number,        attack: number,        decay: number,        sustain: number,        release: number,    ): AudioParamInput {        return value            ? [                  {                      value: 0,                      duration: 0,                      mode: 'instant',                  },                  {                      value,                      duration: attack,                      mode: 'linear',                  },                  {                      value: sustain,                      duration: decay,                      mode: 'linear',                  },              ]            : {                  value: 0,                  duration: release,                  mode: 'linear',              };    }}

Теперь, когда мы нажимаем клавишу, громкость будет линейно нарастать за время attack. Затем она убавится до уровня sustain за время decay. А когда мы отпустим клавишу, громкость упадет до нуля за время release.

С таким пайпом мы можем набросать синтезатор в шаблоне:


<ng-container  *ngFor="let note of notes | keyvalue; trackBy: noteKey">  <ng-container    waOscillatorNode    detune="5"    autoplay    [frequency]="toFrequency(note.key)"   >    <ng-container       waGainNode       gain="0"      [gain]="note.value | adsr: 0:0.1:0.02:1"    >      <ng-container waAudioDestinationNode></ng-container>    </ng-container>  </ng-container>   <ng-container    waOscillatorNode    type="sawtooth"    autoplay     [frequency]="toFrequency(note.key)"  >    <ng-container       waGainNode      gain="0"      [gain]="note.value | adsr: 0:0.1:0.02:1"    >      <ng-container waAudioDestinationNode></ng-container>      <ng-container [waOutput]="convolver"></ng-container>    </ng-container>  </ng-container></ng-container><ng-container  #convolver="AudioNode"  waConvolverNode  buffer="assets/audio/response.wav">  <ng-container waAudioDestinationNode></ng-container></ng-container>

Мы перебираем собранные ноты с помощью встроенного keyvalue пайпа, отслеживая их по номеру сыгранной клавиши. Затем у нас есть два осциллятора, играющих нужные частоты. А в конце эффект реверберации с помощью ConvolverNode. Довольно нехитрая конструкция и совсем немного кода, но мы получим хорошо звучащий, готовый к использованию инструмент. Попробуйте демо в Chrome:


https://ng-web-apis.github.io/midi


Если у вас нет MIDI клавиатуры можете понажимать на ноты мышкой.


Живое демо доступно тут, однако браузер не позволит получить доступ к MIDI в iframe: https://stackblitz.com/edit/angular-midi

Заключение


В Angular мы привыкли работать с событиями с помощью RxJs. И Web MIDI API не сильно отличается от привычных DOM событий. С помощью пары токенов и архитектурных решений мы смогли с легкостью добавить поддержку MIDI в наше Angular приложение. Описанное решение доступно в виде open-source библиотеки @ng-web-apis/midi. Она является частью большого проекта, под названием Web APIs for Angular. Наша цель создание легковесных качественных оберток для использования нативного API в Angular приложениях. Так что если вам нужен, к примеру, Payment Request API или Intersection Observer посмотрите все наши релизы.


Если вам любопытно, что же такого интересного можно сделать на Angular при помощи Web MIDI API приглашаю вас научиться играть на клавишах в личном проекте Jamigo.app

Подробнее..

Перевод Руководство по Node.js для начинающих. Часть 1. Быстрый старт

01.07.2020 14:11:33 | Автор: admin


Доброго времени суток, друзья!

Представляю Вашему вниманию перевод этого руководства по Node.js.

Введение в Node.js


Node.js это открытая и кроссплатформенная среда выполнения JavaScript. Это отличное решение почти для любого проекта.

Node.js запускает движок JavaScript V8, ядро Google Chrome, вне браузера. Это делает Node.js очень производительным.

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

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

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

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

В Node.js новые ECMAScript-стандарты могут использоваться без проблем, вам не нужно ждать, пока все пользователи обновят браузеры вы сами решаете, какую версию ECMAScript использовать посредством изменения версии Node.js, вы также можете добавить экспериментальные возможности, запустив Node.js с (соответствующими) флагами.

Огромное количество библиотек

Npm с его простой структурой способствует быстрому росту экосистемы Node.js, на сегодняшний день в npm зарегистрировано свыше 1 000 000 открытых пакетов, которые вы может использовать совершенно бесплатно.

Пример Node.js-приложения

Наиболее распространенным примером использования Node.js является создание веб-сервера:

    const http = require('http')const hostname = '127.0.0.1'const port = process.env.PORT const server = http.createServer((req, res) => {    res.statusCode = 200    res.setHeader('Content-Type', 'text/plain')    res.end('Hello World!\n')})server.listen(port, hostname, () => {    console.log(`Server running at http://${hostname}:${port}/`)})

Первым делом мы подключаем модуль http.

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

Метод createServer() http создает новый HTTP-сервер и возвращает его.

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

При получениее нового запроса вызывается событие request, содержащее два объекта: запрос (объект http.IncomingMessage (входящее сообщение)) и ответ (объект http.ServerResponse (ответ сервера)).

Эти объекты необходимы для обработки вызова HTTP.

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

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

В данном случае посредством

    res.statusCode = 200

мы присваиваем свойству statusCode значение 200 в качестве индикатора успешного выполнения запроса.

Мы устанавливает заголовок Content-Type (тип содержимого или контента)

    res.setHeader('Content-Type', 'text/plain')

и закрываем ответ, добавляя контент в качестве аргумента в end():

    res.end('Hello World\n')

Node.js-фреймворки и инструменты

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

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

  • AdonisJs: фуллстек-фреймворк с акцентом на эргономику, стабильность и надежность. Adonis один из самых быстрых веб-фреймворков для Node.js.
  • Express: предоставляет один из самых простых и одновременно мощных способов создания веб-сервера. Его ключем к успеху является минималистичный подход, простой функционал, акцент на основных возможностях сервера.
  • Fastify: веб-фреймворк, сфокусированный на обеспечении лучшего опыта разработки с минимальными накладными расходами и мощной архитектурой плагина. Fastify является одним из самых проивзодительных веб-фреймворков.
  • hapi: фреймворк с богатым функционалом для создания приложений и сервисов, позволяющий разработчикам сосредоточиться на написании переиспользуемой логики приложений вместо траты времени на построение инфраструктуры.
  • koa: создан командой Express в целях упрощения и уменьшения размера с учетом многолетнего опыта. Новый проект возник из-за необходимости внесения несовместимых изменений без разрушения сообщества, сформировавшего вокруг Express.
  • Loopback.io: облегчает создание современных приложений со сложной системой зависимостей.
  • Meteor: невероятно мощный фуллстек-фреймворк, предоставляющий изоморфный подход для создания приложений на JavaScript, разделяя код между клиентом и сервером. Входит в комлект таких библиотек, как React, Vue и Angular. Также может использоваться для создания мобильных приложений.
  • Micro: предоставляет легковесный сервер для создания асинхронных HTTP-микросервисов.
  • NestJS: основанный на TypeScript прогрессивный Node.js-фреймворк для создания корпоративных, надежных и масштабируемых серверных приложений.
  • Next.js: фреймоврк для рендеринга React-приложений на стороне сервера.
  • Nx: инструмент для фуллстек монолитной разработки посредством NestJS, Express, React, Angular и т.д. Nx помогает масштабировать разработку от одной до нескольких команд, работающих одновременно над множеством приложений.
  • Socket.io: движок для коммуникации в режиме реального времени для создания сетевых приложений.

Краткая история Node.js


Верите или нет, но Node.js всего 10 лет.

Для сравнения: JavaScript существует на протяжении 24 лет, а веб 30.

10 лет это небольшой срок для технологии, однако порой кажется, что Node.js был всегда.

Я познакомился с Node.js, когда прошло всего 2 года с момента его появления, но уже тогда, несмотря на ограниченность информации, чувствовалось, что его ждет большое будущее.

В этом разделе мы посмотрим на общую картину истории Node.js.

Немного истории

JavaScript это язык программирования, изобретенный в Netscape как скриптовый инструмент манипуляции веб-страницами в браузере Netscape Navigator.

Частью бизнес-модели Netscape являлась продажа веб-серверов, включающих среду Netscape LiveWire, способную создавать динамические страницы посредством серверного JavaScript. К сожалению, Netscape LiveWire провалился и серверный JavaScript не был популярен до появления Node.js.

Одним из ключевых факторов популярности Node.js является время (его появления). Несколькими годами ранее JavaScript был признан серьезным языком (программирования) благодаря Web 2.0-приложениям (таким как Flickr, Gmail и др.), показавших миру, как может выглядеть современный веб.

Движки JavaScript также стали значительно лучше, поскольку браузеры стремились к повышению производительности во благо пользователей. Команды разработчиком главных браузеров упорно трудились над реализацией лучшей поддержки JavaScript и его максимально быстрого выполнения. Движок, который использует Node.js, V8 (также известный как Chrome V8 открытый движок JavaScript проекта Chromium), вышел победителем из этого соревнования.

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

2009

  • Появился Node.js
  • Появился npm

2010

  • Express
  • Socket.io

2011

  • npm версии 1.0
  • Крупные компании начали внедрять Node.js: LinkedIn, Uber и др.
  • hapi

2012

  • Продолжается быстрый рост популярности Node.js

2013

  • Первая крупная блог-платформа на Node.js: Ghost
  • Koa

2014

  • Большой раскол: из Node.js выделился io.js (форк создание обособленной ветки в системе контроля версий git) ради поддержки синтаксиса ES6 и более динамичного развития

2015

  • Основание Node.js Foundation
  • IO.js вернулся в Node.js (мерж слияние веток в git)
  • В npm появились частные (приватные) модули
  • Node.js версии 4 (версий 1, 2 и 3 не было)

2016


2017

  • Повышение безопасности npm
  • Node.js 8
  • HTTP/2
  • V8 включил Node.js в комплект тестирования, официально признав Node.js движком JS в дополнение к Chrome
  • 3 млрд скачиваний npm каждую неделю

2018

  • Node.js 10
  • Экспериментальная поддержка ES-модулей с расширением .mjs

Как установить Node.js?


Node.js может быть установлен различными способами.

Дистрибутивы для основных платформ доступны на официальном сайте.

Очень удобным способом установки Node.js является использование пакетного менеджера. У каждой операционной системы он свой.

На macOS таковым является Homebrew, позволяющий легко установить Node.js с помощью командной строки:

brew install node

Список пакетных менеджеров для Linux, Windows и др. систем находится здесь.

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

Он также очень полезен при необходимости тестирования кода в старых версиях Node.js.

Для более подробной информации о nvm перейдите по этой ссылке.

Мой совет используйте официальный установщик, если только начинаете разрабатывать и ранее не пользовались Homebrew.

После установки Node.js вы получаете доступ к исполянемой программе node в командной строке.

Насколько хорошо вы должны знать JavaScript для работы с Node.js?


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

Также сложно определить, где заканчивается JavaScript и начинается Node.js, и наоборот.

Лично я бы посоветовал хорошенько разобраться со следующими основными концепциями JavaScript перед погружением в Node.js:

  • Синтаксис или лексическая структура
  • Выражения (по сути, тот же синтаксис)
  • Типы (данных)
  • Переменные
  • Функции
  • Ключевое слово this
  • Стрелочные функции
  • Циклы
  • Область видимости
  • Массивы
  • Шаблонные или строковые литералы
  • Точка с запятой (вероятно, случаи ее обязательного использования, например, при работе с IIFE)
  • Строгий режим
  • ECMAScript 6, 2016, 2017

Освоение названных концепций начало пути профессионального фуллстек-разработчика.

Следующие концепции также являются ключом к пониманию асинхронного программирования, которое является фундаментальной частью Node.js:

  • Асинхронное программирование и функции обратного вызова (коллбэки)
  • Таймеры (счетчики)
  • Промисы (обещания)
  • Async/await
  • Замыкания
  • Цикл событий (стек вызовов)

Разница между Node.js и браузером


JavaScript может быть использован как в браузере, так и в Node.js.

Однако создание приложений для браузера сильно отличается от создания Node.js-приложений.

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

С точки зрения JavaScript-фронтендера (разработчика фронтенда клиентской части приложения), разработка приложений на Node.js имеет существенное преимущество, выражающееся в том, что везде, и на клиенте, и на сервере, используется один язык программирования JavaScript.

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

Единственное, что потребуется изучить это экосистема.

В браузере большую часть времени нам приходится иметь дело с DOM и другими веб-API, например, куки. Разумеется, их не существует в Node.js. В Node.js отсутствуют window, document и другие объекты, характерные для браузера.

С другой стороны, в браузерах отсутствуют замечательные API, предоставляемые Node.js посредством модулей, например, доступ к файловой системе.

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

Это означает, что вы можете писать код на JavaScript, поддерживаемом вашей версией Node.js.

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

Для транспиляции кода в ES5 вы можете использовать Babel, в Node.js такой необходимости не возникает.

Еще одним отличием является то, что в Node.js используется модульная система CommonJS, а в браузерах реализована поддержка ES-модулей.

На практике это означает, что в Node.js мы используем require(), а в браузере import.

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

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

Пара слов об именовании переменных и методов

01.07.2020 14:11:33 | Автор: admin


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


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


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


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


Переменные


Один из самых раздражающих видов переменных это такие переменные, что дают ложное представление о природе данных, которые они хранят. Эдакие переменные-мошенники.


В среде Python-разработчиков крайне популярна библиотека requests и, если вы когда-либо искали что-то связанное с requests, то наверняка натыкались на подобный код:


import requestsreq = requests.get('https://api.example.org/endpoint')req.json()

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


Когда вы делаете запрос (requests.Request), то получаете ответ (requests.Response), так отразите это у себя в коде:


response = requests.get('https://api.example.org/endpoint')response.json()

Не r, не res, не resp и уж точно не req, а именно response. res, r, resp (про req и вовсе молчу) это все переменные, содержание которых можно понять только взглянув на их объявление, а зачем прыгать к объявлению, когда можно изначально дать подходящее название?


Давайте рассмотрим еще один пример, но теперь из Django:


users_list = User.objects.filter(age__gte=22)

Когда вы видите где-то в коде users_list, то вы совершенно справедливо ожидаете, что сможете сделать так:


users_list.append(User.objects.get(pk=3))

Но нет, вы этого сделать не можете, так как .filter() возвращает QuerySet, а QuerySet далеко не list:


Traceback (most recent call last):# ...# ...AttributeError: 'QuerySet' object has no attribute 'append'

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


users_queryset = User.objects.all()users_queryset.order_by('-age')

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


users_list = list(User.objects.all())

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


users = User.objects.all()

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


users_seq = [1, 2, 3]# или users_seq = (1, 2, 3)# илиusers_seq = {1, 2, 3}

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


Еще одним видом раздражающих переменных являются переменные с сокращенными именами.


Вернемся к requests и рассмотрим этот код:


s = requests.Session()# ... # ... s.close()

Это яркий пример совершенно неоправданного сокращения названия переменной. Это ужасная практика и ее ужасы становятся еще более очевидны, когда такой код занимает больше 10-15 строк кода.


Конкретно в случае requests, со скрежетом в зубах можно простить подобное сокращение, когда код занимает не более 5-10 строк и записывается вот так:


with requests.Session() as s:    # ...    # ...    s.get('https://api.example.org/endpoint')

Тут контекстный менеджер позволяет дополнительно выделить объемлющий блок для переменной s.


Но гораздо лучше написать как есть, а именно:


session = requests.Session()# ...# ...session.get('https://api.example.org/endpoint')# ...# ...session.close()

или


with requests.Session() as session:    session.get('https://api.example.org/endpoint')

Вы можете возразить, ведь это более многословный вариант, но я вам отвечу, что это окупается, когда вы читаете код и сразу понимаете, что session это Session. Поймете ли вы это по переменной s, не взглянув на ее определение?


Рассмотрим еще один пример:


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_dict = list(info_dict)# ...# ...

Вы видите dict и можете захотеть сделать так:


for key, value in info_dict.items():    print(key, value)

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


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


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


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_keys = list(info_dict)# ...# ...

или даже так, что более идиоматично:


info_dict = {'name': 'Isaak', 'age': 25}# ...# ... info_keys = info_dict.keys()# ...# ...

Комментарии-кэпы


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


Возьмем небольшой пример из JavaScript:


// Remove first five lettersconst errorCode = errorText.substr(5)

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


Давайте попробуем сделать этот комментарий полезным:


// Remove "net::" from error textconst errorCode = errorText.substr(5)

А еще лучше прибегнуть к более декларативному подходу и избавиться от комментария вообще:


const errorCode = errorText.replace('net::', '')

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


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


Методы


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


Рассмотрим пример с методом:


>>> person = Person()>>> person.has_publications()['Post 1', 'Post 2', 'Post 3']

В коде мы выразили весьма однозначный вопрос: "Имеются ли у этого человека публикации?", но какой ответ мы получили?


Мы не спрашивали, какие у человека есть публикаций. Название этого метода подразумевает, что возвращаемое значение должно иметь булевый тип, а именно True или False:


>>> person = Person()>>> person.has_publications()True

А для получения постов вы можете использовать более подходящее название:


>>> person.get_publications()['Post 1', 'Post 2', 'Post 3']

или


>>> person.publications()['Post 1', 'Post 2', 'Post 3']

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


Список литературы для изучения вопроса


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


  1. Robert Martin Clean Code
  2. Robert Martin Clean Architecture
  3. Robert Martin The Clean Coder: A Code of Conduct for Professional Programmers
  4. Martin Fowler Refactoring: Improving the Design of Existing Code
  5. Colin J. Neill Antipatterns: Managing Software Organizations and People
Подробнее..

Перевод 5 типичных ошибок при создании React компонентов (с хуками) в 2020 году

01.07.2020 18:14:54 | Автор: admin
image

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


Оригинальный материал был написан немецким разработчиком Лоренцом Вайсом для личного блога, а позже собрал много позитивных отзывов на dev.to. Переведено командой Quarkly специально для комьюнити на Хабре.



React


React достаточно давно существует в мире веб-разработки, и его позиции как инструмента для гибкой разработки стремительно укрепились за последние годы. А после анонса и релиза нового хука api/concept создание React-компонентов стало ещё проще.


Несмотря на то, что команда, разработавшая React, и огромное сообщество, которое кодит на этом языке, попытались дать внушительное объяснение концепции React'а, я всё же нашел кое-какие недостатки и типичные ошибки во время работы с ним.


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


Дисклеймер


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


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


1. Использование useState, когда нет необходимости в повторном рендере


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


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


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


Так делать нехорошо:


function ClickButton(props) {  const [count, setCount] = useState(0);  const onClickCount = () => {    setCount((c) => c + 1);  };  const onClickRequest = () => {    apiCall(count);  };  return (    <div>      <button onClick={onClickCount}>Counter</button>      <button onClick={onClickRequest}>Submit</button>    </div>  );}

Проблема:


На первый взгляд, вы можете спросить: А в чем, собственно, проблема? Разве не для этого было создано это состояние? И будете правы: всё отлично сработает, и проблемы вряд ли возникнут. Однако в Reactе каждое изменение состояния влияет на компонент и, скорее всего, на его дочерние компоненты, то есть заставляет их выполнить повторный рендеринг.


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


Решение:


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


function ClickButton(props) {  const count = useRef(0);  const onClickCount = () => {    count.current++;  };  const onClickRequest = () => {    apiCall(count.current);  };  return (    <div>      <button onClick={onClickCount}>Counter</button>      <button onClick={onClickRequest}>Submit</button>    </div>  );}

2. Использование router.push вместо ссылки


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


Допустим, вы создаете кнопку, и пользователь при нажатии на неё должен быть перенаправлен на другую страницу. Так как это SPA, то это действие будет клиентским механизмом маршрутизации. Так что вам понадобится какая-нибудь библиотека. Самая популярная из них в React это react-router, и в следующем примере будет использована именно эта библиотека.


Значит ли это, что добавление слушателя события по клику правильно перенаправит пользователя на нужную страницу?


Так делать нехорошо:


function ClickButton(props) {  const history = useHistory();  const onClick = () => {    history.push('/next-page');  };  return <button onClick={onClick}>Go to next page</button>;}

Проблема:


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


Решение:


Ссылки на другие страницы при любом взаимодействии с пользователем должны, насколько это возможно, обрабатываться компонентом <Link> или обычным тегом <a>.


function ClickButton(props) {  return (    <Link to="/next-page">      <span>Go to next page</span>    </Link>  );}

Бонусы: это также делает код более читабельным и лаконичным!


3. Обработка действий с помощью useEffect


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


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


Так делать нехорошо:


function DataList({ onSuccess }) {  const [loading, setLoading] = useState(false);  const [error, setError] = useState(null);  const [data, setData] = useState(null);  const fetchData = useCallback(() => {    setLoading(true);    callApi()      .then((res) => setData(res))      .catch((err) => setError(err))      .finally(() => setLoading(false));  }, []);  useEffect(() => {    fetchData();  }, [fetchData]);  useEffect(() => {    if (!loading && !error && data) {      onSuccess();    }  }, [loading, error, data, onSuccess]);  return <div>Data: {data}</div>;}

Проблема:


Есть два хука useEffect: первый обрабатывает запрос данных к API во время первоначального рендеринга, а второй вызывает функцию onSuccess. То есть, если в состоянии нет загрузки или ошибки, но есть данные, то этот вызов будет успешным. Логично звучит, да?


Конечно, это почти всегда будет срабатывать с первым вызовом. Но вы также потеряете прямую связь между действием и вызываемой функцией. И нет 100% гарантии, что это произойдет только в том случае, если действие fetch будет успешным. А это именно то, что мы, разработчики, так сильно не любим.


Решение:


Самое простое решение установить функцию onSuccess туда, где вызов будет успешным.


function DataList({ onSuccess }) {  const [loading, setLoading] = useState(false);  const [error, setError] = useState(null);  const [data, setData] = useState(null);  const fetchData = useCallback(() => {    setLoading(true);    callApi()      .then((fetchedData) => {        setData(fetchedData);        onSuccess();      })      .catch((err) => setError(err))      .finally(() => setLoading(false));  }, [onSuccess]);  useEffect(() => {    fetchData();  }, [fetchData]);  return <div>{data}</div>;}

Теперь с первого взгляда понятно, что onSuccess вызывается только в случае успешного вызова API.


4. Компоненты с единой ответственностью


Составлять компоненты может быть довольно трудной задачей. Когда делить один компонент на несколько более мелких компонентов? Как структурировать дерево компонентов? Этими вопросами задаются все, кто каждый день работает с компонентным фреймворком. И самой распространенной ошибкой при создании компонентов является объединение двух случаев использования в один компонент.


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


Так делать нехорошо:


function Header(props) {  return (    <header>      <HeaderInner menuItems={menuItems} />    </header>  );}function HeaderInner({ menuItems }) {  return isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />;}

Проблема:


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


Решение:


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


function Header(props) {  return (    <header>{isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />}</header>  );}

5. useEffect с единой ответственностью


Помните времена, когда у нас были только методы componentWillReceiveProps или componentDidUpdate для подключения к процессу рендеринга React-компонента? В голову сразу приходят мрачные воспоминания, а ещё осознаешь всю прелесть использования хука useEffect и особенно то, что его можно использовать без ограничений.


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


Так делать нехорошо:


function Example(props) {  const location = useLocation();  const fetchData = useCallback(() => {    /*  Calling the api */  }, []);  const updateBreadcrumbs = useCallback(() => {    /* Updating the breadcrumbs*/  }, []);  useEffect(() => {    fetchData();    updateBreadcrumbs();  }, [location.pathname, fetchData, updateBreadcrumbs]);  return (    <div>      <BreadCrumbs />    </div>  );}

Проблема:


Существует два варианта использования хука: сбор данных (data-fetching) и отображение пути (displaying breadcrumbs). Оба обновляются с помощью хука useEffect. Этот самый хук useEffect сработает, когда fetchData и updateBreadcrumbs функционируют или меняется location. Основная проблема в том, что теперь мы также вызываем функцию fetchData при изменении location. Это может стать побочным эффектом, о котором мы и не подумали.


Решение:


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


function Example(props) {  const location = useLocation();  const updateBreadcrumbs = useCallback(() => {    /* Updating the breadcrumbs*/  }, []);  useEffect(() => {    updateBreadcrumbs();  }, [location.pathname, updateBreadcrumbs]);  const fetchData = useCallback(() => {    /*  Calling the api */  }, []);  useEffect(() => {    fetchData();  }, [fetchData]);  return (    <div>      <BreadCrumbs />    </div>  );}

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


Заключение


При создании React-компонентов натыкаешься на множество подводных камней. Никогда не удается досконально понять весь механизм, и вы обязательно столкнетесь хотя бы с маленьким, а, скорее всего, даже с большим косяком. Однако совершать ошибки тоже нужно, когда изучаешь какой-нибудь фреймворк или язык программирования. Никто не застрахован от них на 100%.


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

Подробнее..

Перевод Как получить размеры экрана, окна и веб-страницы в JavaScript

02.07.2020 16:18:36 | Автор: admin


Доброго времени суток, друзья!

Представляю Вашему вниманию перевод небольшой заметки How to Get the Screen, Window, and Web Page Sizes in JavaScript автора Dmitri Pavlutin.

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

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

Что означают эти размеры и, главное, как их получить? Именно об этом я и собираюсь рассказать.

1. Экран


1.1. Размер экрана

Размер экрана это ширина и высота всего экрана: монитора или мобильного дисплея.



Получить информацию о размере экрана можно с помощью свойства screen объекта window:

const screenWidth = window.screen.widthconst screenHeight = window.screen.height

1.2. Доступный размер экрана

Доступный размер экрана это ширина и высота активного экрана без панели инструментов операционной системы.



Для получения доступного размера экрана снова обращаемся к window.screen:

const availableScreenWidth = window.screen.availWidthconst availableScreenHeight = window.screen.availHeight

2. Окно


2.1. Размер внешнего окна (или внешний размер окна)

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



Получить информацию о размере внешнего окна можно с помощью свойств outerWidth и outerHeight объекта window:

const windowOuterWidth = window.outerWidthconst windowOuterHeight = window.outerHeight

2.2. Внутренний размер окна (или размер внутреннего окна)

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



Объект window предоставляет свойства innerWidth и innerHeight:

const windowInnerWidth = window.innerWidthconst windowInnerHeight = window.innerHeight

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

const windowInnerWidth = document.documentElement.clientWidthconst windowInnerHeight = document.documentElement.clientHeight

3. Размер веб-страницы


Размер веб-страницы это ширина и высота отображаемого содержимого (отрендеренного контента).



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

const pageWidth = document.documentElement.scrollWidthconst pageHeight = document.documentElement.scrollHeight

Если pageHeight больше, чем внутренняя высота окна, значит, присутствует вертикальная полоса прокрутки.

4. Заключение


Надеюсь, теперь Вы понимаете, как получать различные размеры.

Размер экрана это размер монитора (или дисплея), а доступный размер экрана это размер экрана без панелей инструментов ОС.

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

Наконец, размер веб-страницы это размер контента.

Благодарю за внимание, друзья!
Подробнее..

Redux store vs React state

02.07.2020 22:10:39 | Автор: admin
Как спроектировать хранение данных в React-приложении? Где хранить данные приложения: в глобальном хранилище (Redux store) или в локальном хранилище (component state)?
Такие вопросы возникают у разработчиков, начинающих использовать библиотеку Redux, и даже у тех, кто ей активно пользуется.
Мы в BENOVATE за 5 лет разработки на React опробовали на практике различные подходы к построению архитектуры таких приложений. В статье рассмотрим возможные критерии для выбора места хранения данных в приложении.

А может быть совсем без Redux? Да, если вы можете обойтись без него. На эту тему можете ознакомиться со статьей от одного из создателей библиотеки Дэна Абрамова. Если разработчик понимает, что без Redux не обойтись, то, можно выделить несколько критериев для выбора хранилища данных:

  1. Продолжительность жизни данных
  2. Частота использования
  3. Возможность отслеживания изменений в state


Продолжительность жизни данных


Можно выделить 2 категории:
  • Часто изменяющиеся данные.
  • Редко изменяющиеся данные. Такие данные редко изменяются во время непосредственной работы пользователя с приложением или между сеансами работы с приложением.


Часто изменяющиеся данные


К этой категории относятся, например, параметры фильтрации, сортировки и постраничной навигации компонента, реализующего работу со списком объектов, или флаг, отвечающий за отображение отдельных UI-элементов в приложении, например, выпадающий список или модальное окно (при условии, что оно не привязано к пользовательским настройкам). Сюда же можно отнести и данные заполняемой формы, пока они не отправлены на сервер.
Такие данные лучше хранить в state компонента, т.к. они захламляют глобальное хранилище и усложняют работу с ними: надо писать actions, reducers, инициализировать state и вовремя его очищать.

Bad example
import React from 'react';import { connect } from 'react-redux';import { toggleModal } from './actions/simpleAction'import logo from './logo.svg';import './App.css';import Modal from './elements/modal';const  App = ({                  openModal,                  toggleModal,              }) => {    return (        <div className="App">            <header className="App-header">                <img src={logo} className="App-logo" alt="logo" />            </header>            <main className="Main">                <button onClick={() => toggleModal(true)}>{'Open  Modal'}</button>            </main>            <Modal isOpen={openModal} onClose={() => toggleModal(false)} />        </div>    );}const mapStateToProps = (state) => {    return {        openModal: state.simple.openModal,    }}const mapDispatchToProps = { toggleModal }export default connect(    mapStateToProps,    mapDispatchToProps)(App)// src/constants/simpleConstants.jsexport const simpleConstants = {    TOGGLE_MODAL: 'SIMPLE_TOGGLE_MODAL',};// src/actions/simpleAction.jsimport { simpleConstants} from "../constants/simpleConstants";export const toggleModal = (open) => (    {        type: simpleConstants.TOGGLE_MODAL,        payload: open,    });// src/reducers/simple/simpleReducer.jsimport { simpleConstants } from "../../constants/simpleConstants";const initialState = {    openModal: false,};export function simpleReducer(state = initialState, action) {    switch (action.type) {        case simpleConstants.TOGGLE_MODAL:            return {                ...state,                openModal: action.payload,            };        default:            return state;    }}


Good example
import React, {useState} from 'react';import logo from './logo.svg';import './App.css';import Modal from './elements/modal';const  App = () => {  const [openModal, setOpenModal] = useState(false);  return (    <div className="App">      <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />      </header>      <main className="Main">          <button onClick={() => setOpenModal(true)}>{'Open  Modal'}</button>      </main>      <Modal isOpen={openModal} onClose={() => setOpenModal(false)} />    </div>  );}export default App;



Редко изменяющиеся данные


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

Bad example
// App.jsimport React from 'react';import './App.css';import Header from './elements/header';import ProfileEditForm from './elements/profileeditform';const  App = () => {  return (    <div className="App">      <Header />      <main className="Main">          <ProfileEditForm />      </main>    </div>  );}export default App;// src/elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default () => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu />    </header>)// src/elements/menu.jsimport React, {useEffect, useState} from "react";import { getUserInfo } from '../api';const Menu = () => {    const [userInfo, setUserInfo] = useState({});    useEffect(() => {        getUserInfo().then(data => {            setUserInfo(data);        });    }, []);    return (        <>            <span>{userInfo.userName}</span>            <nav>                <ul>                    <li>Item 1</li>                    <li>Item 2</li>                    <li>Item 3</li>                    <li>Item 4</li>                </ul>            </nav>        </>    )}export default Menu;// src/elements/profileeditform.jsimport React, {useEffect, useState} from "react";import {getUserInfo} from "../api";const ProfileEditForm = () => {    const [state, setState] = useState({        isLoading: true,        userName: null,    })    const setName = (e) => {        const userName = e.target.value;        setState(state => ({            ...state,            userName,        }));    }    useEffect(() => {        getUserInfo().then(data => {            setState(state => ({                ...state,                isLoading: false,                userName: data.userName,            }));        });    }, []);    if (state.isLoading) {        return null;    }    return (        <form>            <input type="text" value={state.userName} onChange={setName} />            <button>{'Save'}</button>        </form>    )}export default ProfileEditForm;


Good example
// App.jsimport React, {useEffect} from 'react';import {connect} from "react-redux";import './App.css';import Header from './elements/header';import ProfileEditForm from './elements/profileeditform';import {loadUserInfo} from "./actions/userAction";const  App = ({ loadUserInfo }) => {  useEffect(() => {      loadUserInfo()  }, [])  return (    <div className="App">      <Header />      <main className="Main">          <ProfileEditForm />      </main>    </div>  );}export default connect(    null,    { loadUserInfo },)(App);// src/elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default () => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu />    </header>)// src/elements/menu.jsimport React from "react";import { connect } from "react-redux";const Menu = ({userName}) => (    <>        <span>{userName}</span>        <nav>            <ul>                <li>Item 1</li>                <li>Item 2</li>                <li>Item 3</li>                <li>Item 4</li>            </ul>        </nav>    </>)const mapStateToProps = (state) => {    return {        userName: state.userInfo.userName,    }}export default connect(    mapStateToProps,)(Menu);// src/elements/profileeditform.jsimport React from "react";import { changeUserName } from '../actions/userAction'import {connect} from "react-redux";const ProfileEditForm = ({userName, changeUserName}) => {    const handleChange = (e) => {        changeUserName(e.target.value);    };    return (        <form>            <input type="text" value={userName} onChange={handleChange} />            <button>{'Save'}</button>        </form>    )}const mapStateToProps = (state) => {    return {        userName: state.userInfo.userName,    }}const mapDispatchToProps = { changeUserName }export default connect(    mapStateToProps,    mapDispatchToProps,)(ProfileEditForm);// src/constants/userConstants.jsexport const userConstants = {    SET_USER_INFO: 'USER_SET_USER_INFO',    SET_USER_NAME: 'USER_SET_USER_NAME',    UNDO: 'USER_UNDO',    REDO: 'USER_REDO',};// src/actions/userAction.jsimport { userConstants } from "../constants/userConstants";import { getUserInfo } from "../api/index";export const changeUserName = (userName) => (    {        type: userConstants.SET_USER_NAME,        payload: userName,    });export const setUserInfo = (data) => (    {        type: userConstants.SET_USER_INFO,        payload: data,    })export const loadUserInfo = () => async (dispatch) => {    const result = await getUserInfo();    dispatch(setUserInfo(result));}// src/reducers/user/userReducer.jsimport { userConstants } from "../../constants/userConstants";const initialState = {    userName: null,};export function userReducer(state = initialState, action) {    switch (action.type) {        case userConstants.SET_USER_INFO:            return {                ...state,                ...action.payload,            };        case userConstants.SET_USER_NAME:            return {                ...state,                userName: action.payload,            };        default:            return state;    }}



Частота использования


Второй критерий сколько компонентов в React-приложении должно иметь доступ к одному и тому же state. Чем больше компонентов используют одни и те же данные в state, тем больше пользы от использования Redux store.
Если вы понимаете, что для определенного компонента или небольшой части вашего приложения state изолирован, то лучше использовать React state отдельного компонента или HOC-компонент.

Глубина передачи state


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

Если же необходимо передать данные state дочерним компонентам на один-два уровня вложенности, то можно это сделать и без Redux.

Bad example
//App.jsimport React from 'react';import './App.css';import Header from './elements/header';import MainContent from './elements/maincontent';const  App = ({userName}) => {  return (    <div className="App">      <Header userName={userName} />      <main className="Main">          <MainContent />      </main>    </div>  );}export default App;// ./elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default ({ userName }) => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu userName={userName} />    </header>)// ./elements/menu.jsimport React from "react";export default ({userName}) => (    <>        <span>{userName}</span>        <nav>            <ul>                <li>Item 1</li>                <li>Item 2</li>                <li>Item 3</li>                <li>Item 4</li>            </ul>        </nav>    </>)


Good example
// App.jsimport React from 'react';import './App.css';import Header from './elements/header';import MainContent from './elements/maincontent';const  App = () => {  return (    <div className="App">      <Header />      <main className="Main">          <MainContent />      </main>    </div>  );}export default App;//./elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default () => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu />    </header>)//./elements/menu.jsimport React from "react";import { connect } from "react-redux";const Menu = ({userName}) => (    <>        <span>{userName}</span>        <nav>            <ul>                <li>Item 1</li>                <li>Item 2</li>                <li>Item 3</li>                <li>Item 4</li>            </ul>        </nav>    </>)const mapStateToProps = (state) => {    return {        userName: state.userInfo.userName,    }}export default connect(    mapStateToProps,)(Menu)



Несвязанные компоненты, оперирующие одинаковыми данными в state


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

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

Можно сделать проще: сохраняем данные профиля пользователя в Redux store, и позволяем компоненту контейнера header и компоненту редактирования профиля получать и изменять данные в Redux store.

image
Bad example
// App.jsimport React, {useState} from 'react';import './App.css';import Header from './elements/header';import ProfileEditForm from './elements/profileeditform';const  App = ({user}) => {  const [userName, setUserName] = useState(user.user_name);  return (    <div className="App">      <Header userName={userName} />      <main className="Main">          <ProfileEditForm onChangeName={setUserName} userName={userName} />      </main>    </div>  );}export default App;// ./elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default ({ userName }) => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu userName={userName} />    </header>)// ./elements/menu.jsimport React from "react";const Menu = ({userName}) => (    <>        <span>{userName}</span>        <nav>            <ul>                <li>Item 1</li>                <li>Item 2</li>                <li>Item 3</li>                <li>Item 4</li>            </ul>        </nav>    </>)export default Menu;// ./elements/profileeditform.jsimport React from "react";export default ({userName, onChangeName}) => {    const handleChange = (e) => {        onChangeName(e.target.value);    };    return (        <form>            <input type="text" value={userName} onChange={handleChange} />            <button>{'Save'}</button>        </form>    )}


Good example
// App.jsimport React from 'react';import './App.css';import Header from './elements/header';import ProfileEditForm from './elements/profileeditform';const  App = () => {  return (    <div className="App">      <Header />      <main className="Main">          <ProfileEditForm />      </main>    </div>  );}export default App;//./elements/header.jsimport React from "react";import logo from "../logo.svg";import Menu from "./menu";export default () => (    <header className="App-header">        <img src={logo} className="App-logo" alt="logo" />        <Menu />    </header>)//./elements/menu.jsimport React from "react";import { connect } from "react-redux";const Menu = ({userName}) => (    <>        <span>{userName}</span>        <nav>            <ul>                <li>Item 1</li>                <li>Item 2</li>                <li>Item 3</li>                <li>Item 4</li>            </ul>        </nav>    </>)const mapStateToProps = (state) => {    return {        userName: state.userInfo.userName,    }}export default connect(    mapStateToProps,)(Menu)//./elements/profileeditformimport React from "react";import { changeUserName } from '../actions/userAction'import {connect} from "react-redux";const ProfileEditForm = ({userName, changeUserName}) => {    const handleChange = (e) => {        changeUserName(e.target.value);    };    return (        <form>            <input type="text" value={userName} onChange={handleChange} />            <button>{'Save'}</button>        </form>    )}const mapStateToProps = (state) => {    return {        userName: state.userInfo.userName,    }}const mapDispatchToProps = { changeUserName }export default connect(    mapStateToProps,    mapDispatchToProps,)(ProfileEditForm)



Возможность отслеживания изменений в state


Другой случай: вам требуется реализовать возможность отменять/повторять пользовательские операции в приложении или вы просто хотите логировать изменения state.
Такая необходимость возникла у нас при разработке конструктора учебных пособий, с помощью которого пользователь может добавлять и настраивать блоки с текстом, изображением и видео на страницу пособия, а также может выполнять операции Undo/Redo.
В подобных случаях Redux отличное решение, т.к. каждый созданный action является атомарным изменением state. Redux упрощает все эти задачи, сосредотачивая их в одном месте Redux store.

Undo/redo example
// App.jsimport React from 'react';import './App.css';import Header from './elements/header';import ProfileEditForm from './elements/profileeditform';const  App = () => {  return (    <div className="App">      <Header />      <main className="Main">          <ProfileEditForm />      </main>    </div>  );}export default App;// './elements/profileeditform.js'import React from "react";import { changeUserName, undo, redo } from '../actions/userAction'import {connect} from "react-redux";const ProfileEditForm = ({ userName, changeUserName, undo, redo, hasPast, hasFuture }) => {    const handleChange = (e) => {        changeUserName(e.target.value);    };    return (        <>            <form>                <input type="text" value={userName} onChange={handleChange} />                <button>{'Save'}</button>            </form>            <div>                <button onClick={undo} disabled={!hasPast}>{'Undo'}</button>                <button onClick={redo} disabled={!hasFuture}>{'Redo'}</button>            </div>        </>    )}const mapStateToProps = (state) => {    return {        hasPast: !!state.userInfo.past.length,        hasFuture: !!state.userInfo.future.length,        userName: state.userInfo.present.userName,    }}const mapDispatchToProps = { changeUserName, undo, redo }export default connect(    mapStateToProps,    mapDispatchToProps,)(ProfileEditForm)// src/constants/userConstants.jsexport const userConstants = {    SET_USER_NAME: 'USER_SET_USER_NAME',    UNDO: 'USER_UNDO',    REDO: 'USER_REDO',};// src/actions/userAction.jsimport { userConstants } from "../constants/userConstants";export const changeUserName = (userName) => (    {        type: userConstants.SET_USER_NAME,        payload: userName,    });export const undo = () => (    {        type: userConstants.UNDO,    });export const redo = () => (    {        type: userConstants.REDO,    });// src/reducers/user/undoableUserReducer.jsimport {userConstants} from "../../constants/userConstants";export function undoable(reducer) {    const initialState = {        past: [],        present: reducer(undefined, {}),        future: [],    };    return function userReducer(state = initialState, action) {        const {past, present, future} = state;        switch (action.type) {            case userConstants.UNDO:                const previous = past[past.length - 1]                const newPast = past.slice(0, past.length - 1)                return {                    past: newPast,                    present: previous,                    future: [present, ...future]                }            case userConstants.REDO:                const next = future[0]                const newFuture = future.slice(1)                return {                    past: [...past, present],                    present: next,                    future: newFuture                }            default:                const newPresent = reducer(present, action)                if (present === newPresent) {                    return state                }                return {                    past: [...past, present],                    present: newPresent,                    future: []                }        }    }}// src/reducers/user/userReducer.jsimport { undoable } from "./undoableUserReducer";import { userConstants } from "../../constants/userConstants";const initialState = {    userName: 'username',};function reducer(state = initialState, action) {    switch (action.type) {        case userConstants.SET_USER_NAME:            return {                ...state,                userName: action.payload,            };        default:            return state;    }}export const userReducer = undoable(reducer);



Резюмируя


Рассмотреть вариант хранения данных в Redux store стоит в следующих случаях:
  1. Если эти данные редко изменяются;
  2. Если одни и те же данные используются в нескольких (больше 2-3) связанных компонентах или в несвязанных компонентах;
  3. Если требуется отслеживать изменения данных.

Во всех остальных случаях лучше использовать React state.

P.S. Большое спасибо mamdaxx111 за помощь в подготовке статьи!
Подробнее..
Категории: Javascript , Node.js , Reactjs , Frontend , Redux , React.js

Sentry удаленый мониторинг багов в фронтенд приложениях React

03.07.2020 10:08:54 | Автор: admin

Мы изучаем использование Sentry с React.



Эта статья является частью серии, начинающейся с сообщения об ошибках Sentry на примере: Часть 1.


Реализация React


Сначала нам нужно добавить новый проект Sentry для этого приложения; с сайта Sentry. В этом случае мы выбираем React.


Мы вновь реализуем наши две кнопки, Hello и Error, приложение с React. Мы начинаем с создания нашего стартового приложения:


npx create-react-app react-app

Затем мы импортируем пакет Sentry:


yarn add @sentry/browser

и инициализируем его:


react-app / src / index.js


...import * as Sentry from '@sentry/browser';const RELEASE = '0.1.0';if (process.env.NODE_ENV === 'production') {  Sentry.init({    dsn: 'https://303c04eac89844b5bfc908ceffc6757c@sentry.io/1289887',    release: RELEASE,  });}...

Наблюдения:


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

Затем мы реализуем наши кнопки Hello и Error и добавляем их в приложение:


react-app / src / Hello.js


import React, { Component } from 'react';import * as Sentry from '@sentry/browser';export default class Hello extends Component {  state = {    text: '',  };  render() {    const { text } = this.state;    return (      <div>        <button          onClick={this.handleClick}        >          Hello        </button>        <div>{text}</div>      </div>    )  }  handleClick = () => {    this.setState({      text: 'Hello World',    });    try {      throw new Error('Caught');    } catch (err) {      if (process.env.NODE_ENV !== 'production') {        return;      }      Sentry.captureException(err);    }  }}

react-app / src / MyError.js


import React, { Component } from 'react';export default class MyError extends Component {  render() {    return (      <div>        <button          onClick={this.handleClick}        >          Error        </button>      </div>    )  }  handleClick = () => {    throw new Error('Uncaught');  }}

react-app / src / App.js


...import Hello from './Hello';import MyError from './MyError';class App extends Component {  render() {    return (      <div className="App">        ...        <Hello />        <MyError />      </div>    );  }}export default App;

Проблема (Исходные Карты)


Мы можем протестировать Sentry с производственной сборкой, введя:


yarn build

и из build папки введите:


npx http-server -c-1

Проблема, с которой мы немедленно столкнемся, заключается в том, что записи об ошибке Sentry ссылаются на номера строк в уменьшенном пакете; не очень полезно.



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


Решения (Исходные Карты)


Решение этой проблемы сводится к запуску приложения с общедоступного веб-сервера. Одна простая кнопка ответа на него, чтобы использовать сервис GitHub Pages (бесплатно). Шаги для использования обычно следующие:


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


  2. Включите GitHub Pages в репозитории (из GitHub), чтобы использовать папку docs в master ветви


  3. Перенесите изменения на GitHub



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


"homepage": "https://larkintuckerllc.github.io/hello-sentry/"

Окончательная версия запущенного приложения доступна по адресу:


https://larkintuckerllc.github.io/hello-sentry/


Иллюстрация Пойманных Ошибок


Давайте пройдем через нажатие кнопки Hello.



С ошибкой, появляющейся следующим образом:



Наблюдения:


  • Этот отчет об ошибке не может быть более ясным, BRAVO.

Иллюстрация Неучтенных Ошибок


Аналогично, давайте пройдем через нажатие кнопки Error.



С ошибкой, появляющейся следующим образом:



Лучшая обработка неучтенных ошибок (рендеринг)


Введение Границ Ошибок

Ошибка JavaScript в части пользовательского интерфейса не должна нарушать работу всего приложения. Чтобы решить эту проблему для пользователей React, React 16 вводит новое понятие "границы ошибок".

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



Новое поведение для необнаруженных ошибок

Это изменение имеет важное значение. Начиная с React 16, ошибки, которые не были пойманы какой-либо границей ошибок, приведут к размонтированию всего дерева компонентов React.

Dan Abramov Error Handling in React 16

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


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


react-app / src / MyRenderError


import React, { Component } from 'react';export default class MyRenderError extends Component {  state = {    flag: false,  };  render() {    const { flag } = this.state;    return (      <div>        <button          onClick={this.handleClick}        >          Render Error        </button>        { flag && <div>{flag.busted.bogus}</div> }      </div>    )  }  handleClick = () => {    this.setState({      flag: true,    });  }}

Наблюдение:


  • При нажатии кнопки, React будет отображаться flag.busted.bogus, которая порождает ошибку


  • Без границы ошибки все дерево компонентов будет размонтировано



Затем мы пишем наш код границы ошибки (использует новый метод жизненного цикла componentDidCatch); это, по сути, пример, приведенный в статье Дэна Абрамова:


react-app / src / ErrorBoundary.js


import React, { Component } from 'react';import * as Sentry from '@sentry/browser';export default class ErrorBoundary extends Component {  constructor(props) {    super(props);    this.state = { hasError: false };  }  componentDidCatch(err, info) {    this.setState({ hasError: true });    Sentry.captureException(err);  }  render() {    if (this.state.hasError) {      return <h1>Something went wrong.</h1>;    }    return this.props.children;  }}

Наконец, мы используем этот компонент:


react-app / src / App.js


...import MyRenderError from './MyRenderError';class App extends Component {  render() {    return (      <ErrorBoundary>        <div className="App">          ...        </div>      </ErrorBoundary>    );  }}...

При этом нажатие кнопки Render Error отображает резервный пользовательский интерфейс и сообщает об ошибке Sentry.




Завершение


Надеюсь, вам было это полезно.


P.S. Телеграм чат по Sentry https://t.me/sentry_ru

Подробнее..

Перевод Как обойти запрет доступа к страницам с помощью Chrome в headless-режиме

03.07.2020 10:08:54 | Автор: admin

Некоторые сайты блокируют Chrome в headless-режиме, и мы рассмотрим, как обойти эту блокировку.


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


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


image


В данном примере сам сервер даже не отправил соответствующую веб-страницу.


Первоначальный ответ страница Доступ запрещен, и это все, что можно получить при работе Chrome в headless-режиме. Чего вовсе не происходит в управляемом режиме.


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


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


Сравнение заголовков HTTP-запроса


Поскольку есть (должна быть) небольшая разница между Chrome, запущенном в headless-режиме, и Chrome, запущенном в обычном режиме, логично предположить, что основной сетевой стек один и тот же, и нет никакой разницы, как браузер передает запросы на уровне пакетов. Это говорит о том, что нужно заострить внимание только на содержимом запроса. Можно воспользоваться сервисом, который возвращает нам же наши HTTP-запросы (эхо-сервисом), чтобы найти отличия между запросом, сделанным в headless-режиме, и запросом, сделанным в обычном режиме. Скрипт ниже использует http://scooterlabs.com/echo.json для получения JSON-ответа, который представляет запрос, полученный сервером.


const puppeteer = require('puppeteer');(async() => {  const browser = await puppeteer.launch({  });  const page = (await browser.pages())[0];  const response = await page.goto('http://scooterlabs.com/echo.json');  console.log(await response.json());  await browser.close();})()

Запуская его как в headless-режиме (по умолчанию), так и в обычном режиме (с помощью добавления headless:false в параметры запуска), можно сравнить вывод в консоли, чтобы найти отличия, если таковые присутствуют.


image


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


Заголовок Accept-Language отсутствует в случае headless-режима. На самом деле это хороший сигнал того, что кто-то использует нестандартный браузер (или режим браузера), и что браузер мог бы использовать отсутствие этого заголовка для того, чтобы блокировать нас. Это могло бы быть моей первой догадкой, если бы у нас также не было последнего отличающегося заголовка User-Agent.


User-Agent явно выделяется. Это отличие выявляет важную деталь, при этом с помощью этого заголовка headless-режим выдает себя:


image


Заголовок для управляемого человеком Chrome по большей части такой же, если убрать Headless. User-Agent долгое время был основным, бесхитростным способом блокировки нежелательного трафика. Это хорошее отправная точка для получения ответа на вопрос, получаем ли мы то, что нам нужно.


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


Решение (много текста, не читал)


Решить проблему блокировки так же просто, как и поменять заголовок User-Agent. Его можно переопределить на постраничном уровне методом page.setUserAgent(). Вы можете установить пользовательский агент на агент для Chrome в обычном режиме, который, на момент написания этой статьи, выглядит так: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36.


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

Подробнее..

Перевод Устройство ленивой загрузки в популярных фронтенд-фреймворках

03.07.2020 12:17:33 | Автор: admin
Snail steampunk by Avi-li

Команда Mail.ru Cloud Solutions перевела статью о том, что означает ленивая загрузка в трех фронтенд-фреймворках: Angular, React и Vue.js. Далее текст от лица автора.

Один из моих любимых терминов на сегодняшний день ленивая загрузка. Честно говоря, когда несколько лет назад я его услышал, то улыбнулся. В этой статье рассмотрю, что именно означает этот термин применительно к трем наиболее часто используемым фронтенд-фреймворкам: Angular, React и Vue.js.

Нетерпеливая загрузка против ленивой


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

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

О типовых проектах для примера


Примеры приложений, созданных во всех трех фреймворках, очень похожи. Каждый из них показывает следующие две вещи:

  • как использовать ленивую загрузку компонента внутри страницы;
  • как использовать ленивую загрузку компонента с помощью маршрутизации.

Чтобы эффективно визуализировать ленивую загрузку, демо-приложение (компонент) будет вычислять 42-е число Фибоначчи. Математические операции считаются блокирующими это означает, что наша программа не может продвинуться дальше; она должна вернуть результат вычисления. Эта стратегия используется только для имитации того, что происходит, если существует фрагмент кода, выполнение которого занимает много времени, и какое влияние он оказывает на общий опыт использования приложения.

Чтобы получить доступ к проекту, пожалуйста, посетите репозиторий GitHub.

Angular


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

Компонент Фибоначчи


Вот как наш компонент выглядит в Angular:


Поскольку это отдельный компонент и он уже принадлежит корневому компоненту Angular, мы можем загрузить его на страницу.

Загрузка компонента на странице


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


Вместе со следующим TypeScript-кодом:


Что действительно интересно в этой ситуации, так это то, что компонент Fibonacci будет загружаться только в том случае, если значение showFibonacci равно true. Это означает, что управлять ленивой загрузкой можно только с помощью директивы ngIf. Это происходит потому, что Angular не просто показывает или скрывает компонент в DOM он добавляет или удаляет его на основе указанного условия.

Ленивая загрузка или роутинг


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

Создать функциональный модуль в нашем приложении вместе со вторым компонентом можно с помощью Angular CLI: ng g m fibonacci && ng g c --module=fibonacci fibonacci.

После создания модуля мы можем назначить ему компонент, а затем добавить его в основной модуль маршрутизации (app-routing.module.ts):


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

Сравните код выше с этим:


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

Тут можно прочитать больше про ленивую загрузку в Angular.

Vue


Теперь давайте рассмотрим, как добиться ленивой загрузки при разработке с помощью фреймворка Vue.js. Давайте создадим Vue-приложение с помощью интерфейса командной строки Vue CLI и добавим новый компонент. Взгляните на то, как будет выглядеть часть компонента <script>:


Обратите внимание: причина, по которой нам нужно выполнить вычисление вне блока export default {}, в том, что иначе мы не сможем имитировать операцию блокировки. Естественно, Vue.js имеет как свойство mounted, так и свойство method, доступные для компонентов, что позволит вызывать код только при создании компонента.

Ленивая загрузка одиночного компонента


В Vue.js мы можем использовать директиву v-if для добавления или удаления элемента из DOM, и так лениво загружать компонент. Однако есть еще много вещей, которые нам нужно сделать, когда речь заходит о сравнении Vue.js и Angular. Взгляните на следующий код:


Это может показаться логичным способом сделать ленивую загрузку, однако при открытии страницы становится очевидным, что начальное время загрузки действительно велико. Это происходит потому, что компонент загружается сразу независимо от условия v-if. Другими словами, мы говорим Vue загрузить все компоненты независимо от их добавления в DOM.

Производительность загрузки существенно изменится, если мы внесем следующие изменения в элемент <script>:


Добавляя выражение import в инлайн-стиле, как часть свойства компонента, мы включаем ленивую загрузку для компонента Fibonacci. Теперь обновление приложения будет означать, что главная страница загружается действительно быстро. Только когда компонент Fibonacci отображается на главной странице, есть некоторая задержка.

Ленивая загрузка компонентов или роутинг


Ленивая загрузка компонентов в Vue.js следует аналогичной схеме, которую мы обсуждали ранее. Посмотрите на роутер:


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

Чтобы устранить эту проблему, мы можем прибегнуть к привычному паттерну и импортировать компонент в определение маршрута:


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

Тут больше информации про ленивую загрузку в Vue.js.

React


И последнее, но не менее важное: давайте рассмотрим, как добиться ленивой загрузки в React. Приложение было создано с помощью CLI create-react-app и, как и в предыдущих примерах, у нас есть компонент с некоторой блокирующей операцией:


Ленивая загрузка одиночного компонента


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


В приведенном выше примере, даже если компонент Fibonacci не отображается, загрузка главной страницы приложения все равно занимает много времени. Чтобы исправить это, нужно сказать React о ленивой загрузке компонента после знака вопроса. В React есть несколько вспомогательных инструментов, таких как компонент Suspense для отображения плейсхолдера во время загрузки компонента и метод lazy (), который загружает компонент лениво:


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

Ленивая загрузка или роутинг


Тот же подход применим и к ленивой загрузке компонента с помощью маршрутизации, включая использование Suspense и lazy():


Учитывая вышеприведенный маршрутизатор, в сочетании с оператором import это означает, что компонент Fibonacci будет загружен сразу. Теперь, надеюсь, понятно, почему это не идеально. Чтобы включить ленивую загрузку компонентов через маршрутизацию, нужно изменить код, чтобы использовать вышеупомянутый компонент Suspense и метод lazy ():


Тут больше о ленивой загрузке в React.

Проверки через Инструменты разработчика


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

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

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

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

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







Еще одна вещь


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

Существуют определенные стратегии, которые помогут преодолеть эту проблему. Со всеми фреймворками мы можем использовать волшебные комментарии через Webpack для динамического добавления prefetch (или preload) через тег <link rel="prefetch" /> на страницу. Просто поместите волшебные комментарии перед именем компонента, внутри импорта:


Это добавит в DOM тег <link rel="prefetch" as="script" href="http://personeltest.ru/aways/habr.com/static/js/fibonacci.chunk.js">.

Больше о волшебных комментариях и параметрах preload/prefetch в Webpack.

В заключение


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

Что еще почитать по теме:

  1. Переиспользуемые компоненты React: как перестать писать одно и то же.
  2. Как избежать ошибок при разработке на React.
  3. Наш телеграм-канал с новостями о цифровой трансформации.
Подробнее..

50200 вопросов по JavaScript

06.07.2020 10:05:56 | Автор: admin


Доброго времени суток, друзья!

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

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

Предисловие


Данная часть основана на этом репозитории. Его автор, Lydia Hallie, позиционирует свой проект как список продвинутых вопросов и, действительно, среди них есть такие, которые, как мне кажется, даже опытному JavaScript-разработчику покажутся непростыми. Однако среди этих вопросов есть и такие, для ответа на которые достаточно владеть базовыми знаниями. В репозитории имеется русский перевод, но, мягко говоря, он оставляет желать лучшего, поэтому большую часть ответов (объяснений) пришлось переводить заново.

Следует отметить, что приводимые пояснения (ответы) не всегда в полной мере раскрывают суть проблемы. Это объясняется формой проекта он представляет собой чеклист, а не учебник. Ответы, скорее, являются подсказкой для дальнейших поисков на MDN или Javascript.ru. Впрочем, многие из объяснений содержат исчерпывающие ответы.

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

Собственно, это все, что я хотел сказать в качестве предисловия.

Правила


Правила простые: 50 вопросов, 3-4 варианта ответа, рейтинг: количество правильных и неправильных ответов, прогресс: номер и количество вопросов.

По результатам определяется процент правильных ответов и делается вывод об уровне владения JavaScript: больше 80% отлично, больше 50% неплохо, меньше 50% ну, Вы понимаете.

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

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

Но довольно слов, пора переходить к делу.

Викторина



Код проекта находится здесь.

Механика


Несколько слов о том, как реализована викторина для тех, кому интересно.

Разметка выглядит так:

<head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>200+ вопросов по JavaScript</title>    <!-- шрифт -->    <link href="http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">    <!-- стили -->    <link rel="stylesheet" href="style.css">    <!-- основной скрипт с типом "модуль" -->    <script type="module" src="script.js"></script></head><body></body>

Добавляем минимальные стили.
CSS:
* {    margin: 0;    padding: 0;    box-sizing: border-box;    font-family: Ubuntu, sans-serif;    font-size: 1em;    text-align: center;    letter-spacing: 1.05px;    line-height: 1.5em;    color: #111;    user-select: none;}@media (max-width: 512px) {    * {        font-size: .95em;    }}html {    position: relative;}body {    padding: 1em;    min-height: 100vh;    background: radial-gradient(circle, skyblue, steelblue);    display: flex;    flex-direction: column;    justify-content: start;    align-items: center;}h1 {    margin: .5em;    font-size: 1.05em;}output {    margin: .5em;    display: block;}.score {    font-size: 1.25em;}form {    text-align: left;}form p {    text-align: left;    white-space: pre;}form button {    position: relative;    left: 50%;    transform: translateX(-50%);}button {    margin: 2em 0;    padding: .4em .8em;    outline: none;    border: none;    background: linear-gradient(lightgreen, darkgreen);    border-radius: 6px;    box-shadow: 0 1px 2px rgba(0, 0, 0, .4);    font-size: .95em;    cursor: pointer;    transition: .2s;}button:hover {    color: #eee;}label {    cursor: pointer;}input {    margin: 0 10px 0 2em;    cursor: pointer;}details {    font-size: .95em;    position: absolute;    bottom: 0;    left: 50%;    transform: translateX(-50%);    width: 90%;    background: #eee;    border-radius: 4px;    cursor: pointer;}details h3 {    margin: .5em;}details p {    margin: .5em 1.5em;    text-align: justify;    text-indent: 1.5em;}.right {    color: green;}.wrong {    color: red;}


Исходники (assets) представляют собой массив объектов, где каждый объект имеет свойства question (вопрос), answers (ответы), rightAnswer (правильный ответ) и explanation (объяснение):

[{    question: `        function sayHi() {            console.log(name);            console.log(age);            var name = "Lydia";            let age = 21;        }        sayHi();    `,    answers: `        A: Lydia и undefined        B: Lydia и ReferenceError        C: ReferenceError и 21        D: undefined и ReferenceError    `,    rightAnswer: `D`,    explanation: `        Внутри функции мы сначала определяем переменную name с помощью ключевого слова var. Это означает, что name поднимется в начало функции. Name будет иметь значение undefined до тех пор, пока выполнение кода не дойдет до строки, где ей присваивается значение Lydia. Мы не определили значение name, когда пытаемся вывести ее в консоль, поэтому будет выведено undefined. Переменные, определенные с помощью let (и const), также поднимаются, но в отличие от var, не инициализируются. Доступ к ним до инициализации невозможен. Это называется "временной мертвой зоной". Когда мы пытаемся обратиться к переменным до их определения, JavaScript выбрасывает исключение ReferenceError.    `},...]

Основной скрипт.
JavaScript
// импортируем массив объектов - исходникиimport assets from './assets.js'// IIFE;((D, B) => {    // заголовок - вопрос    const title = D.createElement('h1')    B.append(title)    // рейтинг: количество правильных и неправильных ответов    const score = D.createElement('output')    score.className = 'score'    B.append(score)    // прогресс: порядковый номер вопроса    const progress = D.createElement('output')    progress.className = 'progress'    B.append(progress)    // контейнер для вопроса, вариантов ответа и кнопки для отправки формы    const div = D.createElement('div')    B.append(div)    // получаем значения правильных и неправильных ответов из локального хранилища    // или присваиваем переменным 0    let rightAnswers = +localStorage.getItem('rightAnswers') || 0    let wrongAnswers = +localStorage.getItem('wrongAnswers') || 0    // получаем значение счетчика из локального хранилища    // или присваиваем ему 0    let i = +localStorage.getItem('i') || 0    // рендерим вопрос    showQuestion()    // обновляем рейтинг и прогресс    updateScoreAndProgress()    function showQuestion() {        // если значение счетчика равняется количеству вопросов        // значит, игра окончена,        // показываем результат        if (i === assets.length) {            return showResult()        }        // заголовок-вопрос зависит от значения счетчика - номера вопроса        switch (i) {            case 4:                title.textContent = `Что не является валидным?`                break;            case 9:                title.textContent = `Что произойдет?`                break;            case 12:                title.textContent = `Назовите три фазы распространения событий`                break;            case 13:                title.textContent = `Все ли объекты имеют прототипы?`                break;            case 14:                title.textContent = `Каким будет результат?`                break;            case 20:                title.textContent = `Чему равно sum?`                break;            case 21:                title.textContent = `Как долго будет доступен cool_secret?`                break;            case 23:                title.textContent = `Каким будет результат?`                break;            case 25:                title.textContent = `Глобальный контекст исполнения создает две вещи: глобальный объект и this`                break;            case 27:                title.textContent = `Каким будет результат?`                break;            case 29:                title.textContent = `Каким будет результат?`                break;            case 30:                title.textContent = `Что будет в event.target после нажатия на кнопку?`                break;            case 33:                title.textContent = `Каким будет результат?`                break;            case 34:                title.textContent = `Какие из значений являются "ложными"?`                break;            case 38:                title.textContent = `Все в JavaScript это`                break;            case 39:                title.textContent = `Каким будет результат?`                break;            case 40:                title.textContent = `Каким будет результат?`                break;            case 41:                title.textContent = `Что возвращает setInterval?`                break;            case 42:                title.textContent = `Каким будет результат?`                break;            case 42:                title.textContent = `Каково значение num?`                break;            case 49:                title.textContent = `Каким будет результат?`                break;            default:                title.textContent = `Что будет выведено в консоль?`                break;        }        // поскольку каждый элемент массива - это объект,        // мы можем его деструктурировать, получив вопрос, правильный ответ и объяснение        const {            question,            rightAnswer,            explanation        } = assets[i]        // поскольку варианты ответа - это input type="radio",        // строку необходимо преобразовать в массив (критерием является перенос строки - \n)        // первый и последний элементы - пустые строки,        // избавляемся от них с помощью slice(1, -1),        // также удаляем пробелы        const answers = assets[i].answers            .split('\n')            .slice(1, -1)            .map(i => i.trim())        // HTML-шаблон        const template = `        <form action="#">            <p><em>Вопрос:</em><br> ${question}</p>            <p><em>Варианты ответов:</em></p><br>            ${answers.reduce((html, item) => html += `<label><input type="radio" name="answer" value="${item}">${item}</label><br>`, '')}            <button type="submit">Ответить</button>        </form>        <details>            <summary>Показать правильный ответ</summary>            <section>                <h3>Правильный ответ: ${rightAnswer}</h3>                <p>${explanation}</p>            </section>        </details>`        // помещаем шаблон в контейнер        div.innerHTML = template        // находим форму        const form = div.querySelector('form')        // выбираем первый инпут        form.querySelector('input').setAttribute('checked', '')        // обрабатываем отправку формы        form.addEventListener('submit', ev => {            // предотвращаем перезагрузку страницы            ev.preventDefault()            // определяем выбранный вариант ответа            const chosenAnswer = form.querySelector('input:checked').value.substr(0, 1)            // проверяем ответ            checkAnswer(chosenAnswer, rightAnswer)        })    }    function checkAnswer(chosenAnswer, rightAnswer) {        // индикатор правильного ответа        let isRight = true        // если выбранный ответ совпадает с правильным,        // увеличиваем количество правильных ответов,        // записываем количество правильных ответов в локальное хранилище,        // иначе увеличиваем количество неправильных ответов,        // записываем количество неправильных ответов в локальное хранилище        // и присваиваем индикатору false        if (chosenAnswer === rightAnswer) {            rightAnswers++            localStorage.setItem('rightAnswers', rightAnswers)        } else {            wrongAnswers++            localStorage.setItem('wrongAnswers', wrongAnswers)            isRight = false        }        // находим кнопку        const button = div.querySelector('button')        // если ответ был правильным        if (isRight) {            // сообщаем об этом            title.innerHTML = `<h1 class="right">Верно!</h1>`            // выключаем кнопку            button.disabled = true            // через секунду вызываем функции            // обновления рейтинга и прогресса и рендеринга следующего вопроса            // отключаем таймер            const timer = setTimeout(() => {                updateScoreAndProgress()                showQuestion()                clearTimeout(timer)            }, 1000)            // если ответ был неправильным        } else {            // сообщаем об этом            title.innerHTML = `<h1 class="wrong">Неверно!</h1>`            // выключаем инпуты            div.querySelectorAll('input').forEach(input => input.disabled = true)            // раскрываем объяснение            div.querySelector('details').setAttribute('open', '')            // меняем текст кнопки            button.textContent = 'Понятно'            // по клику на кнопке вызываем функции            // обновления рейтинга и прогресса и рендеринга следующего вопроса            // удаляем обработчик            button.addEventListener('click', () => {                updateScoreAndProgress()                showQuestion()            }, {                once: true            })        }        // увеличиваем значение счетчика        i++        // записываем значение счетчика в локальное хранилище        localStorage.setItem('i', i)    }    function updateScoreAndProgress() {        // обновляем рейтинг        score.innerHTML = `<span class="right">${rightAnswers}</span> - <span class="wrong">${wrongAnswers}</span>`        // обновляем прогресс        progress.innerHTML = `${i + 1} / ${assets.length}`    }    function showResult() {        // определяем процент правильных ответов        const percent = (rightAnswers / assets.length * 100).toFixed()        // объявляем переменную для результата        let result        // в зависимости от процента правильных ответов        // присваиваем result соответствующее значение        if (percent >= 80) {            result = `Отличный результат! Вы прекрасно знаете JavaScript.`        } else if (percent > 50) {            result = `Неплохой результат, но есть к чему стремиться.`        } else {            result = `Вероятно, вы только начали изучать JavaScript.`        }        // рендерим результаты        B.innerHTML = `        <h1>Ваш результат</h1>        <div>            <p>Правильных ответов: <span class="right">${rightAnswers}</span></p>            <p>Неправильных ответов: <span class="wrong">${wrongAnswers}</span></p>            <p>Процент правильных ответов: ${percent}</p>            <p>${result}</p>            <button>Заново</button>        </div>        `        // при нажатии на кнопку        // очищаем хранилище        // и перезагружаем страницу,        // удаляем обработчик        B.querySelector('button').addEventListener('click', () => {            localStorage.clear()            location.reload()        }, {            once: true        })    }})(document, document.body)


Благодарю за внимание, друзья.

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

Canvas и геометрия. Это почти просто

06.07.2020 14:13:22 | Автор: admin

Трехмерную графику можно реализовать в браузере не только применяя WebGL или
библиотеки созданные на основе WebGL, но и путем простой отрисовки на 2D-холсте используя
для этого функции HTML5 Canvas.


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


Остановимся на этом чуть подробнее.

Если создается трехмерная компьютерная игра, например 3D-шутер, то разработчики стараются сделать ее
как можно более реалистичной и используют для этого графические ускорители. В этом случае без соответствующих
программных инструментов обойтись не удастся. Для таких игр в браузере есть прямой смысл
использовать WebGL и Three.js. Самое главное, что с помощью шейдеров предназначенных для этих
программных инструментов можно осуществить реалистичное освещение сцены и находящихся на сцене объектов.


Но если требуется просто показать трехмерный объект со всех сторон, то можно обойтись
без WebGL и шейдеров. Например некоторые программы CAD/CAM предназначены только для моделирования
формы объектов и не предполагают использования реалистичного освещения.
На сайте Canvas и геометрия
я последовательно и очень подробно объясняю как используя исключительно HTML5 Canvas можно
создать изображения 3D-моделей многогранников.




Полгода тому назад была опубликована моя статья Three.js и геометрия
которая относилась к серии из 30 глав (уроков) на одноименном сайте
Three.js и геометрия где
мной описывалось отображение 3D-моделей многогранников при помощи three.js. Теперь все модели
на новом сайте созданы только с помощью HTML5 Canvas. Однако сам расчет координат вершин моделей
на обеих сайтах сделан абсолютно одинаково. Отличие заключается лишь в способе вывода изображения на экран.


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



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

Просмотр нумерации вершин модели.

Масштабирование модели.

Возможность выбора любой грани модели мышью
(при использовании three.js это делается с помощью RayCaster'а).

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

Проверка корректности построенной модели.
При желании можно выключать/включать проверку корректности построения модели.

Показано как на 3D-модели отобразить размеры ее частей
при помощи выносных и размерных линий. На сайте сделанном при помощи three.js
эта возможность отсутствует.

Имитация простейшего освещения.

Задание цвета и яркости падающего на модель света.

Задание прозрачности модели.



Если используется WebGL/three.js программист может
не заботиться об удалении невидимых линий это
делается автоматически при помощи Z-буфера.
На сайте Canvas и геометрия
показано как можно отображать только внешние (направленные к наблюдателю) грани модели
двумя различными способами.


При создании online-программ находящихся на сайте использовались лишь самые простые конструкции языка JavaScript.
Самая сложная конструкция этого языка, которая используется на сайтах это prototype в моей библиотеке WebGeometry,
при помощи которой производятся геометрические расчеты. Хотя можно было бы в принципе при создании этой библиотеки обойтись и
без prototype. В самом начале моей работы по отображению трехмерных моделей в браузере мне было весьма непривычно
пользоваться JavaScript. Мне казалось диким, что в языке нет заранее прдопределенных типов данных. Да и некоторые
другие его особенности также казались весьма странными.
В течение многих лет я программировал на C/C++ и поэтому поначалу JavaScript мне показался ущербным языком.
Но спустя некоторое время до меня дошло, что имея только браузер (я в основном использую Chrome и иногда Firefox)
и Notepad++ работать на JavaScript очень удобно результат виден сразу особенно это относится к небольшим
графическим программам.


Если не относться заранее предвзято к особенностям языка, то он оказывается достаточно комфортен.
Поэтому сейчас я изменил свое отношение к этому языку в гораздо лучшую для него сторону.
Иногда правда я с тоской вспоминаю замечательный (это мое личное мнение) отладчик в Visual Studio.
Поэтому, когда требуется найти сложную ошибку в геометрических вычислениях,
я обращаюсь к Visul C++ и этому отладчику. Но отладчики в Chrome и Firefox тоже
производят самое хорошее впечатление.


Сейчас для создания больших программ на JavaScript все время появляются новые инструменты такие как React,
Angular, Vue.
Освоение этих программ требует значительных усилий. К счастью для создания простых графических программ можно обойтись
без них. У меня даже пока не возникала потребность в использовании новых (на данный момент уже и не новых) возможностей ES6.


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



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

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

Несколько слов про использование библиотеки WebGeometry. Эта библиотека сделана таким
образом, чтобы программист котрый создает 3D-модель с нуля не задумывался о том как решить очередную задачу
аналитической геометрии на плоскости и в пространстве для прямых и плоскостей. В основном для решения таких
задач требуются неплохие знания работы с матрицами, и если у вас нет соответствующих готовых функций
то перед вами возникают дополнительные проблемы. В библиотеке WebGeometry нет функций
для перспективных преобразований в пространстве, которые производятся при помощи не трехмерных, а четырехмерных матриц.
Перспективные преобразования не требуются при создании трехмерных моделей. Поэтому они и не включены
в состав библиотеки WebGeometry. Если потребуется выполнить такие преобразования, то соответствующие
функции можно найти, если вы используете three.js, то в самой этой библиотеке или в популярной библиотеке
glMatrix

.
Однако в этих двух упомянутых библиотеках практически нет функций для решения задач аналитической геометрии.
Поэтому и была сделана библиотека функций WebGeometry. В библиотеке
Sylvester есть некоторые (но не все требуемые)
такие функции и поэтому я решил сделать библиотеку полностью подходящую под мои задачи с нуля
на основе уже существующих функций на C/C++.

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



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



На сайте Three.js и геометрия все 16 моделей многогранников расчитаны с помощью библиотеки
WebGeometry (так же как и на текущем сайте), но отображены они на холст с помощью three.js.



На сайте Diamond Cuts модели 7 многогранников также расчитаны спомощью библиотеки
WebGeometry и отображены на холст с помощью three.js. При этом используется
несколько типов шейдеров и кубические текстуры. Все программы на этом сайте снабжены
подробными комментариями и поэтому на их примере можно посмотреть как
шейдеры встраиваются в three.js.



На страницах
Diamond Cuts collection in environments
и Diamond Cuts with Dispersion Light
можно найти множество моделей различных многогранников отображенных при помощи шейдеров и кубических текстур.
Вывод на экран моделей с этих страниц осуществляется при помощи three.js.
При определении координат вершин всех этих моделей координаты предварительно расчитывались на Visual C++
и их значения записывались в файлы. Эти файлы с сохраненными в них значениями координат используются
в программах отображения моделей на экран (three.js и API WebGL).

По ссылке
Pages можно перейти на страницы этого сайта на которых находятся 36 моделей различных огранок.
Вывод на экран на этих страницах осуществляется при помощи чистого API WebGL.
Также отсюда можно скачать exe-файл моей программы под Windows сделаной на C/C++
и OpenGL для просмотра моделей огранок. DLL-файлы нескольких моделей скачиваются с этой же страницы.

На страницах
Shine on You Crazy Diamond ! того же сайта можно под музыку Pink Floyd и других музыкальных групп
двигаться по сцене и рассматривать модели огранок. Управление движением мышь и клавиатура (, , , , <, > )
как в компьютерных играх. Вывод на экран осуществляется при помощи three.js.
На мой взгляд получилось весьма интересно. Не забудьте при прсмотре включить звук!



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

Спасибо всем за просмотр моей статьи!

Подробнее..

Магия WebPush в Mozilla Firefox. Взгляд изнутри

07.07.2020 10:10:24 | Автор: admin

Безусловно одной из самых популярных технологий доставки оповещений на устройства пользователей являются Push уведомления. Технология такова, что для её работы необходим постоянный доступ к интернету, а именно доступ к серверам, на которых регистрируются устройства пользователя для получения уведомлений. В данной статье мы рассмотрим весь спектр механизмов технологии WebPush уведомлений, спрятанных за словами WebSocket, ServiceWorker, vapid, register, broadcast, message encryption и т.д. Основной причиной побудившей меня к реверсу и изучению механизма, являлась необходимость доставки уведомлений мониторинга на рабочие места техподдержки, находящиеся в закрытом сегменте сети без доступа в интернет. И да, это возможно! Подробности под катом.


Disclaimer


В статье рассматривается режим доставки уведомлений пользователям в рамках использования браузера Mozilla Firefox. Это связано с тем, что на данный момент это единственный продукт позволяющий менять настройки push серверов используемых по умолчанию. Настройки браузеров Google Chrome, Chromium и производных в целях безопасности жёстко "зашиты" производителем в коде продукта.


Статья делится на две части


  • Теоретическая информация
  • Практические заметки для реализации механизма WebPush уведомлений

Используемые технологии и термины


WebSocket


Транспортным ядром системы Push уведомлений является протокол WebSocket, позволяющий в рамках стандартного HTTP/HTTPS подключения к Web серверу установить постоянный двусторонний канал связи между клиентом и сервером. В рамках установленного канала связи могут использоваться любые, в том числе бинарные, протоколы клиент-серверного взаимодействия заложенные разработчиками сервиса.


ServiceWorker


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


VAPID


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


WebPush


Механизм доставки сообщений до получателя.
Набор документов и спецификаций по WebPush


Workflow


Документации по WebPush довольно много (см. спойлер), но она существует только в парадигме
Client <-> Push Service <-> Application


Подробные спецификации по работе механизма в продуктах Google и Mozilla

Модель взаимодействия предполагает следующую схему.
image


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


Фаза обработки сообщения


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


Блок кода расставил все точки над И
    try {      reply = JSON.parse(message);    } catch (e) {      console.warn("wsOnMessageAvailable: Invalid JSON", message, e);      return;    }    // If we receive a message, we know the connection succeeded. Reset the    // connection attempt and ping interval counters.    this._retryFailCount = 0;    let doNotHandle = false;    if (      message === "{}" ||      reply.messageType === undefined ||      reply.messageType === "ping" ||      typeof reply.messageType != "string"    ) {      console.debug("wsOnMessageAvailable: Pong received");      doNotHandle = true;    }    // Reset the ping timer.  Note: This path is executed at every step of the    // handshake, so this timer does not need to be set explicitly at startup.    this._startPingTimer();    // If it is a ping, do not handle the message.    if (doNotHandle) {      return;    }    // A whitelist of protocol handlers. Add to these if new messages are added    // in the protocol.    let handlers = [      "Hello",      "Register",      "Unregister",      "Notification",      "Broadcast",    ];    // Build up the handler name to call from messageType.    // e.g. messageType == "register" -> _handleRegisterReply.    let handlerName =      reply.messageType[0].toUpperCase() +      reply.messageType.slice(1).toLowerCase();    if (!handlers.includes(handlerName)) {      console.warn(        "wsOnMessageAvailable: No whitelisted handler",        handlerName,        "for message",        reply.messageType      );      return;    }    let handler = "_handle" + handlerName + "Reply";

Ни одно сообщение отправленное через websocket в сторону браузера не будет обработано, если оно не является системным сообщением проверки доступности конечной стороны "{}" или ответом на запрос от Push сервера. Это означает, что Push сервер не имеет никакого способа воздействия на работу клиентской стороны, кроме проверки её доступности. Аналогично, кроме 5 типов ответных сообщений, ничего обработано не будет.


Фаза инициализации


При запуске браузера Firefox, его внутренний механизм автоматически инициирует соединение с WebSocket(WS) сервером находящимся в системной настройке dom.push.serverURL с сообщением следующего формата.


{  "messageType": "hello",  "broadcasts":    {      "remote-settings/monitor_changes": "v923"    },    "use_webpush": True}

При первичной инициализации соединения(первый запуск браузера после установки/запуск нового профиля), поле "uaid" отсутствует, что является сигналом Push серверу о необходимости регистрации нового идентификатора. Как мы видим в разделе "broadcasts" присутствует некая пара "remote-settings/monitor_changes": "v923". Данная пара используется как буфер для хранения информации, отправляемой в сторону сервера при установлении соединения. В продукте Mozilla autopush, промышленной версии webpush сервера используемого на стороне серверов Mozilla, данная переменная используется как идентификатор последнего полученного пользователем сообщения из глобальной очереди сервера. Об изменении данного идентификатора мы поговорим позже. Итак, после принятия сообщения от клиента, сервер отвечает сообщением следующего вида


{  "messageType": "hello",  "status": 200,  "uaid": "b4ab795089784bbb978e6c894fe753c0",  "use_webpush": True}

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


Фаза регистрации


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


  • Проверка разрешения пользователя на получение информации
  • Регистрация ServiceWorker
  • Получение параметров подписки
  • Формирование ключей шифрования для обслуживания подписки

Проверка разрешений пользователя на получение информации


На данном этапе браузер перед установкой ServiceWorker, запрашивает пользователя и системные настройки: "готов ли пользователь получать сообщения о подписке?"
В случае одного из отказов, установка ServiceWorker прерывается


Регистрация ServiceWorker


Как мы ранее оговаривали, ServiceWorker это автономная страница с обработчиками событий в отдельном пространстве браузера недоступном пользователю.
На работу с этим компонентом накладываются довольно серъёзные ограничения:


  • Загрузка компонента ServiceWorker должна производиться через защищённое соединение(HTTPS), либо в целях отладки с localhost. Возможно включение флагов на "небезопасное" использование внешних ресурсов, но это не рекомендуется
  • соединение WebSocket должно устанавливаться по защищённому соединению(WSS), либо в целях отладки по обычному WS соединению с localhost
  • если в локальной сети имя сервера(ресурса) с которого происходит регистрация ServiceWorker, отличается от полного fqdn ресурса на котором находится ServiceWorker, то будет вызвано исключение о небезопасном вызове
    Жизненный цикл ServiceWorker от Google
    Жизненный цикл ServiceWorker от Mozilla

Процесс подписки


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


  • получение публичного ключа шифрования с Web сервера, являющегося промежуточным звеном между Push сервером и приложением осуществляющем отправку сообщений
  • формирование браузером ключей шифрования для защиты сообщений
  • вызов механизма подписки через канал WebSocket и получение точки для отправки сообщений

Получение публичного ключа


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


Запуск процесса генерации ключей шифрования


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


Получение точки для отправки сообщений


После формирования ключей шифрования вызывается процесс внутри браузера называемый register. В сторону Push сервера через WebSocket браузер отправляет запрос вида


{  "channelID": "f9cb8f1c-05e0-403f-a09b-dd7864a03eb7",  "messageType": "register",  "key": "BO_C-Ou.......zKu2U4HZ9XeElUIdRfc6EBbRudAjq4="}

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


{  "messageType": "register",  "channelID": "f9cb8f1c-05e0-403f-a09b-dd7864a03eb7",  "status": 200,  "pushEndpoint": "https://webpush.example.net/wpush/f9cb8f1c-05e0-403f-a09b-dd7864a03eb7/",  "scope": "https://webpush.example.net/"}

В данном сообщении содержится адрес конечной точки сформированный Push(WebSocket) сервером, на которую необходимо отправить зашифрованное сообщение для получения его пользователем. Для передачи сообщения получателю, между WEB сервером, принимающим внешние запросы и WS сервером, отправляющим оповещения, должна быть организована логическая связь.


Итого по окончании процесса регистрации и подписки мы имеем следующий набор данных:


Браузер:


  • приватный ключ шифрования сообщений
  • публичный ключ шифрования сообщений
  • ключ авторизации(DH)
  • конечная точка для доставки сообщений получателю
  • номер канала зарегистрированный на WebSocket сервере
  • идентификатор клиента внутри WS соединения
  • публичный ключ WebPush сервера

WebPush сервер:


  • публичный ключ WebPush сервера
  • приватный ключ WebPush сервера

Push(WebSocket) сервер:


  • публичный ключ WebPush сервера
  • адрес конечной точки клиента
  • номер канала клиента привязанный к конечной точке
  • идентификатор клиента внутри WS соединения

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


  • некто хочет отправить сообщение в браузер пользователя
  • для защиты сообщения необходимо извлечь из браузера настройки текущей подписки к Push серверу (конечную точку для отправки сообщения, публичный ключ шифрования сообщения, ключ авторизации)
  • полученные настройки передаются на промежуточный WebPush сервер вместе с текстом сообщения
  • промежуточный WebPush сервер формирует авторизационный JWT токен, содержащий время создания сообщения, адрес администратора WebPush сервера, время действия сообщения и подписывает его при помощи своего приватного ключа
  • промежуточный WebPush сервер производит шифрование сообщения при помощи публичного ключа и ключа авторизации из браузера
  • промежуточный WebPush сервер вызывает конечную точку полученную из браузера, передавая в неё связку JWT токен+публичный ключ для их проверки в заголовке Authorization, а также бинарный массив зашифрованного сообщения в теле запроса
  • Push сервер по вызываемой конечной точке производит привязку запроса к каналу получателя
  • Push сервер проверяет валидность JWT токена
  • Push сервер конвертирует бинарный массив принятых данных в base64, формирует сообщение типа "notification" с каналом получателя, ставит сообщение в очередь, после чего механизм контроля очереди отправляет сообщение по WebSocket каналу в сторону клиента

Здесь мы прервём процесс для описания формата сообщения типа "notification".
Дело в том, что формат сообщения типа "notification" имеет два варианта. От того, что получил браузер и передал в ServiceWorker зависит логика работы по получению и отображению сообщения. Первый вариант, это "пустое" сообщение:


{  "messageType": "notification",  "channelID": "f7dfeed8-f868-47ca-a066-fbe629879fbf",  "version": "bf82eea1-69fd-4be0-b943-da96ff0041fb"}

"Пустое" сообщение как бы говорит браузеру "Эй, тебя тут ждут данные, приходи за ними". Браузер по логике работы должен сделать GET запрос на URL конечной точки и получить первую запись из очереди для отображения её пользователю. Схема конечно хорошая, только совсем небезопасная. В большинстве случаев она не применяется.
Вторым вариантом является передача данных совместно с сообщением.


{  "messageType": "notification",  "channelID": "f7dfeed8-f868-47ca-a066-fbe629879fbf",  "version": "bf82eea1-69fd-4be0-b943-da96ff0041fb",  "data": "I_j8p....eMlYK6jxE2-pHv-TRhqQ",  "headers":  {    "encoding": "aes128gcm"  }}

Браузер реагирует на поле headers в структуре сообщения типа "notification". При наличии этого поля автоматически включается механизм обработки зашифрованных данных из поля "data". На основании номера канала, событийная машина браузера выбирает набор ключей шифрования и пытается расшифровать полученные данные. После расшифровки расшифрованные данные передаются в обработчик "push" сообщений ServiceWorker. Как вы успели заметить, сообщение типа "notification" имеет поле "version", которое представляет собой уникальный номер сообщения. Уникальный номер сообщения используется в системе доставки и отображения сообщений для дедупликации данных.
Она работает следующим образом:


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

Продолжим разбор процесса.


  • Если сообщение принято и расшифровано, от браузера в сторону Push сервера формируется новое сообщение с типом "ack", включающее в себя номер канала и номер обработанного сообщения. Это является сигналом удаления сообщения из очереди сообщений для данного канала
  • Если сообщение по какой-то причине не может быть обработано, от браузера в сторону Push сервера формируется новое сообщение с типом "noack", включающее в себя номер канала и номер отвергнутого сообщения. Это является сигналом постановки сообщения на повторную доставку через 60 секунд

Вернёмся к сообщениям с типом "broadcast". Продукт "autopush" от Mozilla использует их в качестве хранилища на стороне клиента, для определения последнего отправленного клиенту сообщения. Дело в том, что отправка сообщения типа "broadcast" со сменой значения ключа "remote-settings/monitor_changes", приводит к срабатыванию механизма, сохраняющего полученное значение в хранилище браузера. При потере соединения или каком-то программном сбое, сохранённое значение будет автоматически передано на сторону Push сервера в момент инициализации соединения и будет являться начальной точкой для последующей переотправки пропущенных сообщений из очереди.


Описывать сообщения типа "unregister" смысла не имеет, т.к. оно ни на что, кроме удаления сессии не влияет.


К чему же было приведено подробное описание всех процессов происходящих при Push оповещениях?
Смысл в том, что на основании этих данных можно довольно быстро построить свой Push сервер с необходимым функционалом. Продукт "autopush" от Mozilla является продуктом промышленного масштаба, который рассчитан на многомилионные подключения клиентов. В его составе присутствует TornadoDB, PyPy, CPython. К сожалению движок написан на Python 2.7, который массово выводится из эксплуатации.
Нам же нужен небольшой сервер с простым, желательно асинхронным кодом. А именно, без промежуточного WebPush сервера, VAPID, лишних межсерверных проверок и прочего. Сервер должен уметь привязывать клиентские подключения Push сервера к именам пользователей, а также иметь возможность организации эндпоинтов и webhook'ов для отправки сообщений этим пользователям.


Пишем свой сервер


У нас есть следующие данные:


  • Пользователь с браузером Mozilla Firefox;
  • Точка регистрации пользователя на сервере уведомлений для получения этих самых уведомлений;
  • WebSocket сервер, обслуживающий подключения движка уведомлений, встроенного в браузер;
  • Web сервер, формирующий интерфейс для пользователя и обслуживающий точки для отправки уведомлений;

Шаг 1
Первым делом мы должны подготовить WebSocket сервер, обслуживающий описанную ранее логику работы и подключения к нему клиентов.
В качестве фреймворка для реализации логики сервера используется AsyncIO Python.
Изначально стоит сразу разделить понятие "регистрация" для WebSocket движка браузера и понятие "регистрация" на сервере уведомлений. Разница заключается в том, что "регистрация" WebSocket движка браузера происходит автоматически без участия пользователя, в то время как разрешение на "регистрацию" на сервере уведомлений это осознанное действие со стороны пользователя.
Первичной задачей WebSocket сервера является принятие входящего соединения и его контроль на протяжении всего времени подключения браузера к серверу. Поэтому мы должны принять внешнее соединение, сделать его привязку к каналу и сохранить для дальнейшей работы.


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


WebSocket Handler
# внешнее имя сервераSERVERNAME='webpush.example.net'# вебсокетыWS = set()# каналыCHANNELS = dict()async def register(websocket):    try:        WS.add(websocket)        websocket.handler = PushConnectionHandler(websocket)    except Exception as ex:        logger.error('Register exception: %s' % ex)async def unregister(websocket):    try:        CHANNELS.remove(websocket.handler.channel_id)        WS.remove(websocket)        logger.debug('UnregisterWebsocket[websocket]: %s'%websocket)    except Exception as ex:        logger.error('Unregister exception: %s' % ex)async def pushserver(websocket, path):    await register(websocket)    try:        await websocket.send(json.dumps({}))        async for message in websocket:            data = json.loads(message)            logger.info('Incoming message[data]: %s => %s '%(message, data))            if message == '{}':                await websocket.send(json.dumps({}))            elif 'messageType' in data:                logger.info('Processing WebSocket Data')                # Подключение к вебсокету из браузера                if data['messageType'] == 'hello':                    # Если это первичное подключение, то нужно задать идентификатор подключения и вернуть его браузеру                    if 'uaid' not in data:                        data['uaid'] = '%s' % uuid.uuid4()                    # Принудительно включить webpush                    if 'use_webpush' not in data:                        data['use_webpush'] = True                    helloreturn = {                        "messageType": "hello",                        "status": 200,                        "uaid": data['uaid'],                        "use_webpush": data['use_webpush']                        }                    websocket.handler.uaid = data['uaid']                    if 'broadcasts' in data:                        websocket.handler.register_broadcasts(data['broadcasts'])                    logger.debug('Hello websocket: %s' % vars(websocket.handler))                    CHANNELS.update({ data['uaid'] : websocket.handler })                    await websocket.send(json.dumps(helloreturn))                elif data['messageType'] == 'register':                    # Регистрация serviceWorker                    logger.debug('Register[data]: %s'%data)                    registerreturn = {                        "messageType": "register",                        "channelID": data['channelID'],                        "status": 200,                        "pushEndpoint": "https://%s/wpush/%s/" % (SERVERNAME,data['channelID']),                        "scope": "https://%s/" % SERVERNAME                    }                    websocket.handler.channel_id = data['channelID']                    if 'key' in data:                        websocket.handler.server_public_key = data['key']                    logger.debug('Register[registerreturn]: %s'%registerreturn)                    CHANNELS.update({ data['channelID'] : websocket.handler })                    await websocket.send(json.dumps(registerreturn))                elif data['messageType'] == 'unregister':                    unregisterreturn = {                        "messageType": "unregister",                        "channelID": data['channelID'],                        "status": 200                    }                    if data['channelID'] in CHANNELS:                        del CHANNELS[data['channelID']]                    logger.debug('Unregister[unregisterreturn]: %s'%unregisterreturn)                    logger.debug('Unregister[CHANNELS]: %s'%CHANNELS)                    await websocket.send(json.dumps(unregisterreturn))                elif data['messageType'] == 'ack':                    logger.debug('Ack: %s' % data)                    for update in data['updates']:                        if CHANNELS[update['channelID']].mqueue.count(update['version']) > 0:                            CHANNELS[update['channelID']].mqueue.remove(update['version'])                    logger.debug('Mqueue for channel %s is %s' % (websocket.handler.channel_id, websocket.handler.mqueue))                    await websocket.send('{}')                elif data['messageType'] == 'nack':                    await websocket.send('{}')            else:                logger.error("unsupported event: {}", data)    finally:        await unregister(websocket)

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


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


main.js
'use strict';let isSubscribed = false;let swRegistration = null;var wait = ms => new Promise((r, j)=>setTimeout(r, ms));function urlB64ToUint8Array(base64String) {    const padding = '='.repeat((4 - base64String.length % 4) % 4);    const base64 = (base64String + padding)        .replace(/\-/g, '+')        .replace(/_/g, '/');    const rawData = window.atob(base64);    const outputArray = new Uint8Array(rawData.length);    for (let i = 0; i < rawData.length; ++i) {        outputArray[i] = rawData.charCodeAt(i);    }    return outputArray;}function subscribeUser() {    const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);    swRegistration.pushManager.subscribe({            userVisibleOnly: true,            applicationServerKey: applicationServerKey        })        .then(function(subscription) {            console.log('User is subscribed.', JSON.stringify(subscription));            localStorage.setItem('sub_token',JSON.stringify(subscription));            isSubscribed = true;            fetch(subscription.endpoint, {                method: 'POST',                cache: 'no-cache',                body: JSON.stringify(subscription)            })            .then(function(response) {                console.log('Push keys Update Response: ' + JSON.stringify(response));            })        })        .catch(function(err) {            console.log('Failed to subscribe the user: ', err);        });}function unsubscribeUser() {    swRegistration.pushManager.getSubscription()        .then(function(subscription) {            if (subscription) {                return subscription.unsubscribe();            }        })        .catch(function(error) {            console.log('Error unsubscribing', error);        })        .then(function() {            console.log('User is unsubscribed.');            isSubscribed = false;        });}function initializeUI() {    // Set the initial subscription value    swRegistration.pushManager.getSubscription()        .then(function(subscription) {            isSubscribed = !(subscription === null);            if (isSubscribed) {                console.log('User IS subscribed. Unsubscribing.');                subscription.unsubscribe();            } else {                console.log('User is NOT subscribed. Subscribing.');                subscribeUser();            }        });    (async () => {    await wait(2000);    console.warn('Wait for operation is ok');         swRegistration.pushManager.getSubscription()                .then(function(subscription) {                        isSubscribed = !(subscription === null);                        if (!isSubscribed) {                                console.log('ReSubscribe user');                                subscribeUser();                        }                })    })()}console.log(navigator);console.log(window);if ('serviceWorker' in navigator && 'PushManager' in window) {    console.log('Service Worker and Push is supported');    navigator.serviceWorker.register("/sw.js")        .then(function(swReg) {            console.log('Service Worker is registered', swReg);            swRegistration = swReg;            initializeUI();        })        .catch(function(error) {            console.error('Service Worker Error', error);        });} else {    console.warn('Push messaging application ServerPublicKey is not supported');}$(document).ready(function(){    $.ajax({        type:"GET",        url:'/subscription/',        success:function(response){            console.log("response",response);            localStorage.setItem('applicationServerPublicKey',response.public_key);        }    })});

Основная точка на которую стоит обратить внимание это автоматическая переподписка пользователя при посещении страницы. Работа сервера рассчитана на отправку уведомления по имени пользователя или любому другому идентификатору пользователя, поэтому механизм переподписки всегда будет формировать новую подписку для идентификатора пользователя. Это избавляет нас от проблем с "потерянными" подписками на сервере.


sw.js
'use strict';/* eslint-disable max-len *//* eslint-enable max-len */function urlB64ToUint8Array(base64String) {  const padding = '='.repeat((4 - base64String.length % 4) % 4);  const base64 = (base64String + padding)    .replace(/\-/g, '+')    .replace(/_/g, '/');  const rawData = window.atob(base64);  const outputArray = new Uint8Array(rawData.length);  for (let i = 0; i < rawData.length; ++i) {    outputArray[i] = rawData.charCodeAt(i);  }  return outputArray;}function getEndpoint() {  return self.registration.pushManager.getSubscription()  .then(function(subscription) {    if (subscription) {      return subscription.endpoint;    }    throw new Error('User not subscribed');  });}self.popNotification = function(title, body, tag, icon, url) {  console.debug('Popup data:', tag, body, title, icon, url);  self.registration.showNotification(title, {      body: body,      tag: tag,      icon: icon    });  self.onnotificationclick = function(event){      console.debug('On notification click: ', event.notification.tag);      event.notification.close();      event.waitUntil(        clients.openWindow(url)      );  };}var wait = ms => new Promise((r, j)=>setTimeout(r, ms));self.addEventListener('push', function(event) {   console.log('[Push]', event);  if (event.data) {    var data = event.data.json();    var evtag = data.tag || 'notag';    self.popNotification(data.title || 'Default title', data.body || 'Body is not present', evtag, data.icon || '/static/images/default.svg', data.url || '/getevent?tag='+evtag);  }  else {    event.waitUntil(      getEndpoint().then(function(endpoint) {        return fetch(endpoint);      }).then(function(response) {          return response.json();      }).then(function(payload) {          console.debug('Payload',JSON.stringify(payload), payload.length);          var evtag = payload.tag || 'notag';          self.popNotification(payload.title || 'Default title', payload.body || 'Body is not present', payload.tag || 'notag', payload.icon || '/static/images/default.svg', payload.url || '/getevent?tag='+evtag);      })    );  }});self.addEventListener('pushsubscriptionchange', function(event) {  console.log('[Service Worker]: \'pushsubscriptionchange\' event fired.');  const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);  event.waitUntil(    self.registration.pushManager.subscribe({      userVisibleOnly: true,      applicationServerKey: applicationServerKey    })    .then(function(newSubscription) {      // TODO: Send to application server      console.log('[Service Worker] New subscription: ', newSubscription);    })  );});

Согласно представленного кода, Javascript файл main.js инициирует при своём запуске получение публичного VAPID ключа и принудительно вызывает подписку браузера на оповещения.
Для простоты отладки WebSocket сервер во время регистрации подписки отдаёт URL вида: https://webpush.example.net/wpush/ChannelGuid.
Откуда же берётся имя пользователя в сервере уведомлений. Вся суть в том, что инициирование подписки /subscription/ происходит полуавтоматически. Соответственно в зависимости от того, что вы хотите увидеть в качестве идентификатора пользователя, вы можете передать после оформления подписки в момент передачи ключей.
Это происходит путём вызова метода POST по адресу WebPush endpoint присланного сервером из модуля ServiceWorker.


function subscribeUser() {    const applicationServerPublicKey = localStorage.getItem('applicationServerPublicKey');    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);    swRegistration.pushManager.subscribe({            userVisibleOnly: true,            applicationServerKey: applicationServerKey        })        .then(function(subscription) {            console.log('User is subscribed.', JSON.stringify(subscription));            localStorage.setItem('sub_token',JSON.stringify(subscription));            isSubscribed = true;            fetch(subscription.endpoint, {                method: 'POST',                cache: 'no-cache',                body: JSON.stringify(subscription)            })            .then(function(response) {                console.log('Push keys Update Response: ' + JSON.stringify(response));            })        })        .catch(function(err) {            console.log('Failed to subscribe the user: ', err);        });}

Как было написано ранее, в сервере используется обработчик точек подключения. Это отдельная часть кода в скрипте сервера, но обрабатывающая вместо WebSocket, клиентский WEB трафик от браузера.
В качестве обрабатываемого заголовка, содержащего идентификатор пользователя, в базовом варианте сервиса использовался basiclogin полученный при авторизации пользователя в LDAP.


        location ~ /subscription|/pushdata|/getdata|/wpush|/notify {            proxy_pass http://localhost:8090;            proxy_set_header LDAP-AuthUser $remote_user;            proxy_set_header 'X-Remote-Addr' $remote_addr;            add_header "Access-Control-Allow-Origin" "*";            add_header Last-Modified $date_gmt;        proxy_hide_header "Authorization";            add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';            if_modified_since off;            expires off;            etag off;        }

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


USERIDHEADERNAME='X-Remote-Addr'async def update_channel_keys(request, data):    channel = request.path.replace('wpush','').replace('/','')    logger.debug('update channel keys data: %s'%data)    logger.debug('Update Channel keys Headers: %s' % request.headers)    if USERIDHEADERNAME not in set(request.headers):        return False    basiclogin = request.headers[USERIDHEADERNAME]    logger.debug('Login %s' % basiclogin)    if basiclogin not in LOGINS_IN_CHANNELS:        LOGINS_IN_CHANNELS.update({ '%s'%basiclogin : {} })    LOGINS_IN_CHANNELS['%s'%basiclogin].update({'%s' % channel : {} })    logger.debug('LOGINS_IN_CHANNELS: %s' % LOGINS_IN_CHANNELS)    try:        jdata = json.loads(data)        if 'endpoint' in jdata and 'keys' in jdata:            logger.debug('Saving Keys for Channel: %s => %s' % (channel, jdata))            CHANNELS[channel].register_keys(jdata['keys'])            logger.debug('Registered channel keys %s:' % vars(CHANNELS[channel]))        return True    except Exception as ex:        logger.error('Exception %s'%ex)        return False

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


Шаг 3
Сессия зарегистрирована, ключи на сервер переданы, пора отправлять и получать сообщения.
Как я описывал в самом начале статьи, у сервиса оповещений существует два способа доставки сообщений:


  • пустое push уведомление, когда браузер "приходит" в очередь сообщений сам
  • push уведомление содержащее зашифрованные данные.

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


self.addEventListener('push', function(event) {   console.log('[Push]', event);  if (event.data) {    var data = event.data.json();    var evtag = data.tag || 'notag';    self.popNotification(data.title || 'Default title', data.body || 'Body is not present', evtag, data.icon || '/static/images/default.svg', data.url || '/getevent?tag='+evtag);  }  else {    event.waitUntil(      getEndpoint().then(function(endpoint) {        return fetch(endpoint);      }).then(function(response) {          return response.json();      }).then(function(payload) {          console.debug('Payload',JSON.stringify(payload), payload.length);          var evtag = payload.tag || 'notag';          self.popNotification(payload.title || 'Default title', payload.body || 'Body is not present', payload.tag || 'notag', payload.icon || '/static/images/default.svg', payload.url || '/getevent?tag='+evtag);      })    );  }});

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


Блок шифрования сообщений передаваемых был взят из кода сервера "autopush", дабы не нарушать совместимости.


Блок шифрования сообщения
    def encrypt_message(self, data, content_encoding="aes128gcm"):        """Encrypt the data.        :param data: A serialized block of byte data (String, JSON, bit array,            etc.) Make sure that whatever you send, your client knows how            to understand it.        :type data: str        :param content_encoding: The content_encoding type to use to encrypt            the data. Defaults to RFC8188 "aes128gcm". The previous draft-01 is            "aesgcm", however this format is now deprecated.        :type content_encoding: enum("aesgcm", "aes128gcm")        """        # Salt is a random 16 byte array.        if not data:            logger.error("PushEncryptMessage: No data found...")            return        if not self.auth_key or not self.receiver_key:            raise WebPushException("No keys specified in subscription info")        logger.debug("PushEncryptMessage: Encoding data...")        salt = None        if content_encoding not in self.valid_encodings:            raise WebPushException("Invalid content encoding specified. "                                   "Select from " +                                   json.dumps(self.valid_encodings))        if content_encoding == "aesgcm":            logger.debug("PushEncryptMessage: Generating salt for aesgcm...")            salt = os.urandom(16)        # The server key is an ephemeral ECDH key used only for this        # transaction        server_key = ec.generate_private_key(ec.SECP256R1, default_backend())        crypto_key = server_key.public_key().public_bytes(            encoding=serialization.Encoding.X962,            format=serialization.PublicFormat.UncompressedPoint        )        if isinstance(data, str):            data = bytes(data.encode('utf8'))        if content_encoding == "aes128gcm":            logger.debug("Encrypting to aes128gcm...")            encrypted = http_ece.encrypt(                data,                salt=salt,                private_key=server_key,                dh=self.receiver_key,                auth_secret=self.auth_key,                version=content_encoding)            reply = CaseInsensitiveDict({                'data': base64.urlsafe_b64encode(encrypted).decode()            })        else:            logger.debug("Encrypting to aesgcm...")            crypto_key = base64.urlsafe_b64encode(crypto_key).strip(b'=')            encrypted = http_ece.encrypt(                data,                salt=salt,                private_key=server_key,                keyid=crypto_key.decode(),                dh=self.receiver_key,                auth_secret=self.auth_key,                version=content_encoding)            reply = CaseInsensitiveDict({                'crypto_key': crypto_key,                'data': base64.urlsafe_b64encode(encrypted).decode()            })            if salt:                reply['salt'] = base64.urlsafe_b64encode(salt).strip(b'=')        reply['headers'] = { 'encoding': content_encoding }        return reply

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


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


WebPush AsyncIO server


Для развёртывания сервера необходимо:


  • Установить необходимые Python модули, а также настроить nginx по примеру приложенного конфигурационного файла.
  • Поместить содержимое директории web в корень ранее настроенного виртуального сервера
  • Перезапустить/перечитать конфиг nginx
  • В браузере через about:config поменять параметр dom.push.serverURL на адрес wss://ваш.сервер/ws
  • Перед сменой адреса push сервера можно очистить поле dom.push.userAgentID, которое автоматически заполнится если ваш Push сервер работает корректно и принимает соединения.
  • Для тестирования оповещений необходимо зайти на страницу https://ваш.сервер/indexpush.html и открыв окно отладки удостовериться в корректной регистрации ServiceWorker
  • Нажать кнопку "Check Push Notify"
  • Если всё правильно настроено, появится всплывающее сообщение

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


var req = new CurlHttpRequest();req.AddHeader('Content-Type: application/x-www-form-urlencoded');var jv = JSON.parse(value);if (jv.recovery_nstatus == '{EVENT.RECOVERY.VALUE}') {jv.icon = '/static/images/problem/' + jv.event_severity + '.svg';}else{jv.icon = '/static/images/recovery/' + jv.event_severity + '.svg';}value = JSON.stringify(jv);Zabbix.Log(2, 'webhook request value='+value);req.Post('https://webpush.server.net/pushdata/',  value);Zabbix.Log(2, 'response code: '+req.Status());return JSON.stringify({  'tags': {    'endpoint': 'webpush'  }});

c параметрами


Ключ Значение
url /zabbix/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID}
recipient {ALERT.SENDTO}
title {ALERT.SUBJECT}
body {ALERT.MESSAGE}
event_severity {EVENT.NSEVERITY}
recovery_nstatus {EVENT.RECOVERY.VALUE}

Если добавить красивых картинок из FontAwesome, то получится вот так


WebPush сервер поддерживает следующие вызовы :


  • POST https://webpush.example.net/wpush/channelId сохранение ключей шифрования и имени пользователя
  • GET https://webpush.example.net/wpush/channelId получение тестового сообщения
  • GET https://webpush.example.net/subscription получение публичного VAPID ключа
  • POST https://webpush.example.net/pushdata отправка JSON структуры передаваемой в качестве сообщения в браузер
    {        "url": "http://personeltest.ru/away/habr.com/", // URL на который необходимо перейти при клике        "recipient": login, // Логин или идентификатор пользователя        "title": "Заголовок сообщения",        "body": "Тело сообщения",         "icon": "/static/images/new-notification.png", // путь к иконке сообщения        "version": uuid, // идентификатор сообщения        "tag": uuid, // тег сообщения для получения        "mtime": parseInt(new Date().getTime()/1000) //Время }
    
  • GET https://webpush.example.net/getdata Получение очереди сообщений
  • POST https://webpush.example.net/notify/login Отправка пустого оповещения пользователю
  • POST https://webpush.example.net/notifychannel/channelId Отправка пустого оповещения в канал

Вот в принципе и всё. Надеюсь у вас снялась часть вопросов с тем, как работает WebPush. Спасибо за потраченное время на чтение материала.


Aborche 2020
Aborche

Подробнее..

Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий

07.07.2020 14:13:48 | Автор: admin

Я расскажу о тонкостях внедрения электронной цифровой подписи (ЭЦП) в информационные системы (ИС) на базе веб технологий в контексте Национального Удостоверяющего Центра Республики Казахстан (НУЦ РК).


В центре внимания будет формирование ЭЦП под электронными документами и, соответственно, NCALayer предоставляемое НУЦ РК криптографическое программное обеспечение. В частности уделю внимание вопросам связанным с UX и объемом поддерживаемого функционала NCALayer.


Процесс разделю на следующие этапы:


  • формирование неизменного представления подписываемого документа (под подписываемым документом я буду подразумевать любые данные которые необходимо подписать, такие как: договор, бланк заказа, форма аутентификации и т.п.);
  • подписание документа в веб интерфейсе с помощью NCALayer;
  • проверка подписи на стороне сервера;
  • (при необходимости) подготовка подписи к долгосрочному хранению.

Формирование неизменного представления подписываемого документа


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


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


  • извлечь все поля записи, привести их к строкам и соединить в одну строку;
  • сформировать XML или JSON представление;
  • сформировать PDF документ на базе шаблона с каким-то оформлением содержащий данные из записи;
  • и т.п.

Далее возможны два сценария каждый из которых имеет свои подводные камни:


  • ИС не хранит сформированного представления и каждый раз для проверки подписи под документом (в частности проверки неизменности данных) формирует его;
  • ИС хранит сформированное представление и использует его для проверки подписи.

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


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

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


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


Подписание документа в веб интерфейсе с помощью NCALayer


Для формирования цифровых подписей на стороне клиента НУЦ РК предоставляет единственный инструмент сертифицированное программное обеспечение NCALayer которое представляет из себя WebSocket сервер, запускаемый на 127.0.0.1, которому можно отправлять запросы на выполнение криптографических (а так же некоторых смежных) операций. При выполнении некоторых операций NCALayer отображает диалоговые окна то есть берет часть работы по получению пользовательского ввода на себя.


Описание API NCALayer доступно в составе комплекта разработчика. Для того, чтобы поэкспериментировать со взаимодействием с NCALayer по WebSocket можно воспользоваться страницей интерактивной документации KAZTOKEN mobile (KAZTOKEN mobile повторяет API NCALayer).


Взаимодействовать с NCALayer из браузера можно напрямую с помощью класса WebSocket, либо можно воспользоваться библиотекой ncalayer-js-client которая оборачивает отправку команд и получение ответов в современные async вызовы.


Замечу что весь основной функционал NCALayer доступен в модуле kz.gov.pki.knca.commonUtils, использовать модуль kz.gov.pki.knca.applet.Applet (наследие Java аплета) не рекомендую, так как, на мой взгляд, это не даст никаких преимуществ, но шансов выстрелить себе в ногу с ним больше к примеру можно случайно разработать интерфейс который не будет поддерживать аппаратных носителей (токенов или смарт-карт) с несколькими ключевыми парами.


Модуль kz.gov.pki.knca.commonUtils берет на себя взаимодействие с пользователем связанное с выбором конкретного хранилища, которое нужно использовать для выполнения операции (так же он берет на себя выбор конкретного сертификата и соответствующего ключа, а так же ввод пароля или ПИН кода), но ему необходимо указать какой тип хранилищ нужно использовать. Типы хранилищ стоит разделить на два класса:


  • файловые, поддерживается единственный тип заданный константой 'PKCS12',
  • аппаратные (токены и смарт-карты), для перечисления тех типов, экземпляры которых в данный момент подключены в ПК пользователя, следует использовать запрос getActiveTokens.

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


  • гибкий отправить запрос getActiveTokens, в ответ получить массив имен доступных а данный момент типов хранилищ, добавить в него 'PKCS12' и спросить у пользователя каким он хотел бы в данный момент воспользоваться;
  • простой отправить запрос getActiveTokens, в том случае, если он вернул массив несколькими именами, попросить пользователя отключить лишнее, если в массиве один элемент, использовать его, если массив пустой, использовать 'PKCS12'.

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


  • createCAdESFromBase64 вычислить подпись под данными и сформировать CMS (CAdES);
  • createCMSSignatureFromBase64 вычислить подпись под данными, получить на подпись метку времени (TSP) и сформировать CMS (CAdES) с внедренной меткой времени;
  • signXml вычислить подпись под XML документом, сформированную подпись добавить в результирующий документ (XMLDSIG);
  • signXmls аналогично signXml, но позволяет за один раз подписать несколько XML документов.

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


Модуль kz.gov.pki.knca.commonUtils поддерживает следующие типы сертификатов:


  • 'AUTHENTICATION' сертификаты для выполнения аутентификации;
  • 'SIGNATURE' сертификаты для подписания данных.

NCLayer предоставит пользователю выбирать только из тех сертификатов, которые соответствуют указанному типу.


Упрощенный пример подписания произвольного блока данных с использованием ncalayer-js-client:


async function connectAndSign(base64EncodedData) {  const ncalayerClient = new NCALayerClient();  try {    await ncalayerClient.connect();  } catch (error) {    alert(`Не удалось подключиться к NCALayer: ${error.toString()}`);    return;  }  let activeTokens;  try {    activeTokens = await ncalayerClient.getActiveTokens();  } catch (error) {    alert(error.toString());    return;  }  const storageType = activeTokens[0] || NCALayerClient.fileStorageType;  let base64EncodedSignature;  try {    base64EncodedSignature = await ncalayerClient.createCAdESFromBase64(storageType, base64EncodedData);  } catch (error) {    alert(error.toString());    return;  }  return base64EncodedSignature;}

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


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


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


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


Выполнять проверки необходимо с применением сертифицированных средств, к примеру с помощью библиотек входящих в состав комплекта разработчика НУЦ РК, либо можно воспользоваться готовым решением SIGEX.


Подготовка подписи к долгосрочному хранению


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


Для фиксации момента подписания принято использовать метки времени TSP. Метку времени на подпись можно получить либо на клиенте (запрос createCMSSignatureFromBase64 интегрирует метку времени в CMS), либо на сервере. Метка времени позволит удостовериться в том, что момент подписания попадает в срок действия сертификата.


Для того, чтобы удостовериться в том, что сертификат не был отозван в момент подписания, следует использовать CRL или OCSP ответ. Этот нюанс и рекомендации по реализации описаны в разделе APPENDIX B Placing a Signature At a Particular Point in Time документа RFC 3161.

Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru