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

Web-программирование

Перевод Восемь забавных вещей, которые могут с вами произойти, если у вас нет защиты от CSRF-атак

12.04.2021 18:13:34 | Автор: admin

Восемь забавных вещей, которые могут с вами произойти, если у вас нет защиты от CSRF-атак



Введение


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


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


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


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


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


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


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


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


2. Сделать запрос и узнать его продолжительность


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


Однако никто серьезно не защищается от атак по времени, скажем, при поиске. Например, предположим, что у вас есть сайт службы знакомств, а злоумышленник делает запрос через браузер жертвы, скажем, Сары, по маршрутам messages/search?query=kevin%20mitchell и messages/search?query=blurghafest. Если достоверно известно, что первый запрос занимает больше времени, чем второй (причем необязательно намного), может пролиться кровь. Или, как минимум, где-то далеко произойдет неприятность и появится недовольный клиент.


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


3. Заставить пользователя выйти из системы


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


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


4. Заставить пользователя выйти из системы и снова войти


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


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


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


5. Изменить адрес электронной почты пользователя и запросить восстановление пароля


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


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


6. Превратить уязвимость Self-XSS в XSS


CSRF-атака может превратить уязвимость self-XSS, которая является мелкой проблемой, в XSS, которая является огромной проблемой.


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


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


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


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


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


7. Замести следы


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


Если пользователь не обладает по-настоящему глубоким пониманием, он никогда не узнает, откуда пришел этот CSRF-удар.


8. Распространять вредоносное ПО


Позволяет ли ваш сервис пользователям хранить файлы? Многие типы файлов, такие как .docx, pdf, jpeg и другие исторически используются для сокрытия вредоносного ПО из-за багов программ, которые их читают.


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


Заключительные замечания


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

Подробнее..

CSS Grid понятно для всех

05.08.2020 14:08:45 | Автор: admin

Что такое Grid?


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

Поддержка браузерами


В 2020 году поддержка браузерами достигает 94 %



Grid контейнер


Мы создаем grid контейнер, объявляя display: grid или display: inline-grid на элементе. Как только мы это сделаем, все прямые дети этого элемента станут элементами сетки.

<body> <div class="row">  <div class="row__item header">   <h1>Header</h1>  </div>  <div class="row__item nav">   <h1>Navbar</h1>  </div>  <div class="row__item article">   <h1>Article</h1>  </div>  <div class="row__item ads">   <h1>Ads</h1>  </div> </div></body>

.row { display: grid; margin: auto; grid-template-rows: 60px 1fr ; grid-template-columns: 20% 1fr 15%; grid-gap: 10px; width: 1000px; height: 1000px; justify-items: center; justify-content: space-between; grid-template-areas: "header header header" "nav article ads"; }

grid-template-rows это CSS свойство, которое определяет названия линий и путь размера функции grid rows.

CSS свойство grid-row определяет с какой строки в макете сетки будет начинаться элемент, сколько строк будет занимать элемент, или на какой строке завершится элемент в макете сетки. Является сокращенным свойством для свойств grid-row-start и grid-row-end.

Свойство CSS grid-gap является сокращенным свойством для grid-row-gap и grid-column-gap, определяющего желоба между строками и столбцами сетки.

Свойство grid-template-areas определяет шаблон сетки ссылаясь на имена областей, которые заданы с помощью свойства grid-area.

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

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

.header{grid-area: header;}.nav{grid-area: nav;}.article{grid-area: article;}.ads{grid-area: ads;}

Создаем шаблон сайта с CSS Grid:




Изменяем шаблон


Вы можете изменить шаблон просто перераспределив грид-области в grid-template-areas.

Таким образом, если мы сменим на это:

grid-template-areas: "nav header header" "nav article ads"; }

То в результате получим такой шаблон:



Гриды с медиа запросами


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

Это делает CSS Grid идеальным для медиа запросов. Мы можем просто переназначить значения в ASCII-графике и обернуть результат в конечный медиа запрос.

@media all and (max-width: 575px) {.row {grid-template-areas:"header""article""ads""nav";grid-template-rows: 80px 1fr 70px 1fr ;grid-template-columns: 1fr;}}

В результате получим:



Таким образом, все дело состоит в переназначении значений в свойстве grid-template-areas.

Заключение


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

Я вам советую обратить внимание на данную спецификацию и потратить немного своего времени на ее изучение. Поверьте, в будущем вам это точно пригодится и не важно, пишете вы на React, Angular, Vue (вставьте свое). Gridы пришли надолго.
Подробнее..

Перевод Искусство компонентов. Пишем карточку контакта Facebook Messenger

13.01.2021 18:13:38 | Автор: admin
Вполне возможно оценить компонент и сказать, что он легко пишется на HTML и CSS. Соглашусь, это легко, когда вы работаете, только чтобы практиковаться, но в реальном проекте всё по-другому. Идеальный адаптивный компонент, который вы только что создали, быстро перестаёт работать, когда сталкивается с реальным контентом настоящего проекта. Почему? Потому, что, пока вы рассуждаете о разработке компонента, вы можете упустить крайние случаи. Я покажу простой на первый взгляд компонент, за которым стоит огромная работа. Ради реалистичности это будет пример прямо из Facebook Messenger.




Начнем


Я беру очень простой компонент Facebook Messenger, посмотрите на скриншот ниже:


В этом сайдбаре списком карточек перечисляются люди, которым я писал на Facebook. Здесь меня интересует только карточка. Как вы напишете её на HTML/CSS? Да очень легко, правда? Есть соблазн сказать, что это всего лишь картинка и слой рядом с ней. Вот о чём можно подумать:



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

<div class="card">  <img class="card__image" src="assets/shadeed.jpg" alt="" />  <div><h3>Ahmad Shadeed</h3><p>You: Thanks, sounds good! . 8hr</p><img class="card__seen" src="assets/shadeed.jpg" alt="" />  </div></div>

.card {  position: relative;  display: flex; /* [1] */  align-items: center; /* [2] */  background-color: #fff;  padding: 8px;  border-radius: 7px;  box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.05);}.card h3 {  font-size: 15px;}.card p {  font-size: 13px;  color: #65676b;}.card__image {  width: 56px;  height: 56px;  border-radius: 50%;  margin-right: 12px;}.card__seen {  position: absolute; /* [3] */  right: 16px;  top: 50%;  transform: translateY(-50%);  width: 16px;  height: 16px;  border-radius: 50%;}

Я выделил несколько строчек, их я хочу объяснить:

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

Ломаем компонент


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



Синий значок справа означает, что пришло новое сообщение, которое я ещё не открывал. Зелёный цвет на аватаре показывает, что пользователь сейчас в сети.

Обратите внимание: у нас есть два новых значка. Как лучше добавить их на карточку? Если вы обратитесь к CSS, который я написал для самого первого компонента, то увидите, что там есть класс .card_seen для маленьких аваторов пользователей справа. В этом варианте .card_seen должен быть заменён синим значком. С уже написанными HTML и CSS, не изменив HTML, написать такое невозможно.

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

Все вариации


Ниже я показываю все вариации компонента. Очень старался описать их все (да, я нарисовал всё это вручную).



Но и этого мало: мы должны учитывать стили тёмной темы.



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

Интервалы


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



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

Области компонента


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

Аватар




Чтобы работать над HTML аватара, сначала нужно разобраться с его состояниями. Вот возможные варианты:

  • Один аватар.
  • Один аватар со значком онлайн-статуса.
  • Несколько аватаров для группового чата.
  • Несколько аватаров со значком онлайн-статуса.

Учитывая HTML ниже, мы хотим удостовериться, что .card__avatar работает со всеми вариантами аватаров выше.

<div class="card">  <div class="card__avatar"></div>  <div class="card__content"><!-- Name, message, badge.. -->  </div></div>

Один аватар


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



В CSS невозможно применить внутреннюю box-shadow к элементу img. У нас есть два варианта:

  • Дополнительный div с прозрачным border.
  • Можно написать svg.

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

  • Полностью белый аватар светлой темы;
  • Полностью чёрный аватар тёмной темы.



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



div для внутренней границы


В этом решении дополнительный элемент (здесь это div) абсолютно расположен над изображением с непрозрачностью 0.1.

<div class="card__avatar">  <img src="assets/shadeed.jpg" alt="" />  <div class="border"></div></div>

.card__avatar {  position: relative;}.card__avatar img {  width: 56px;  height: 56px;  border-radius: 50%;}.border {  position: absolute;  width: 56px;  height: 56px;  border: 2px solid #000;  border-radius: 50%;  opacity: 0.1;}

Это решение работает, но у него есть ограничения, о которых я скоро расскажу.

Работаем с svg


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

<svg role="none" style="height: 56px; width: 56px">  <mask id="circle"><circle cx="28" cy="28" fill="white" r="28"></circle>  </mask>  <g mask="url(#circle)"><image  x="0"  y="0"  height="100%"  preserveAspectRatio="xMidYMid slice"  width="100%"  xlink:href="http://personeltest.ru/aways/habr.com/assets/shadeed.jpg"  style="height: 56px; width: 56px"></image><circle class="border" cx="28" cy="28" r="28"></circle>  </g></svg>

.border {  stroke-width: 3;  stroke: rgba(0, 0, 0, 0.1);  fill: none;}

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

Единственный аватар со значком онлайн-статуса


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



Как это сделать? Оказывается, если воспользоваться SVG-решением для того самого единственного аватара, проблема легко решается при помощи маски SVG.

<svg role="none" style="height: 56px; width: 56px">  <mask id="circle"><!-- [1] --><circle cx="28" cy="28" fill="white" r="28"></circle><!-- [2] --><circle cx="48" cy="48" fill="black" r="7"></circle>  </mask>  <!-- [3] -->  <g mask="url(#circle)"><image  x="0"  y="0"  height="100%"  preserveAspectRatio="xMidYMid slice"  width="100%"  xlink:href="http://personeltest.ru/aways/habr.com/assets/shadeed.jpg"  style="height: 56px; width: 56px"></image><circle class="border" cx="28" cy="28" r="28"></circle>  </g></svg>

Позвольте объяснить этот код:

  1. Круг маскирует аватар.
  2. В правом нижнем углу аватара вырезается маленький кружок.
  3. Группа, которая содержит circle и image для прозрачной внутренней границы.

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



Так выглядит HTML аватара со значком онлайн-статуса.

<div class="card__avatar">  <svg role="none" style="height: 56px; width: 56px"><mask id="circle">  <circle cx="28" cy="28" fill="white" r="28"></circle>  <circle cx="48" cy="48" fill="black" r="7"></circle></mask><g mask="url(#circle)">  <imagex="0"y="0"height="100%"preserveAspectRatio="xMidYMid slice"width="100%"xlink:href="http://personeltest.ru/aways/habr.com/assets/shadeed.jpg"style="height: 56px; width: 56px"  ></image>  <circle class="border" cx="28" cy="28" r="28"></circle></g>  </svg>  <div class="badge"></div></div>

.card__avatar {  position: relative;  display: flex;  margin-right: 12px;}.badge {  position: absolute;  right: 3px;  bottom: 3px;  width: 10px;  height: 10px;  background: #5ad539;  border-radius: 50%;}

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

:root {  --primary-text: #050505;  --secondary-text: #65676b;  --bg-color: #fff;}html.is-dark {  --primary-text: #e4e6eb;  --secondary-text: #b0b3b8;  --bg-color: #242526;}.card {  background-color: var(--bg-color);}.card__title {  color: var(--primary-text);}.card__subtitle {  color: var(--secondary-text);}



Несколько аватаров в групповом чате


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

.card__avatar {  width: 56px;  height: 56px;}


Этот вариант требует изменить разметку вот так:

<div class="card__avatar card__avatar--multiple">  <svgclass="avatar avatar-1"role="none"style="height: 36px; width: 36px"  ></svg>  <svgclass="avatar avatar-2"role="none"style="height: 36px; width: 36px"  ></svg>  <div class="badge"></div></div>

.card__avatar--multiple {  position: relative;  width: 56px;  height: 56px;}.card__avatar--multiple .avatar {  position: absolute;}.card__avatar--multiple .avatar-1 {  right: 0;  top: 0;}.card__avatar--multiple .avatar-2 {  left: 0;  bottom: 0;}.card__avatar--multiple .badge {  right: 6px;  bottom: 6px;}



Контент


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



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



Первая часть


Давайте внимательно посмотрим на разметку области контента.

<div class="card__content">  <div class="card__content__start"><h3>Ahmad Shadeed</h3><div class="row">  <p>You: Thanks, sounds good. What about doing a webinar, too?</p>  <span class="separator">.</span>  <time>8hr</time></div>  </div>  <div class="card__content__end"><!-- The indicator (new message, seen, muted, sent) -->  </div></div>

.card__content {  display: flex;  flex: 1;}.card__content__start {  display: flex;  flex: 1;}.card__content__start .row {  display: flex;  align-items: center;}.card__content__end {  display: flex;  justify-content: center;  align-items: center;  margin-left: 12px;}.separator {  margin-left: 4px;  margin-right: 4px;}

С кодом выше область содержимого выглядит, как показано ниже (это скриншот из Firefox).



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



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

  • Установите min-width: 0 для дочерних элементов flex. Зачем? Я расскажу позже.
  • Обрежьте текст через свойства overflow, white-space, и text-overflow. Я уже писал подробнее об обработке короткого и длинного контентов.

Я добавил к имени и абзацу код ниже:

.card__content__start h3,.card__content__start p {  overflow-x: hidden;  white-space: nowrap;  text-overflow: ellipsis;}

Но этот код не решает нашу проблему автоматически, когда мы используем flexbox. Обратите внимание на то, что делает приведённый выше CSS:



И вот причина: flex-элементы не сжимаются сильнее минимального размера контента. Чтобы решить эту проблему, нужно установить min-width: 0 в .card__content и card__content__start.



Вторая часть


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



В этой части сосредоточимся на .card__content__end и на содержании внутри него.

<div class="card__content">  <div class="card__content__start"><!-- The name and message -->  </div>  <div class="card__content__end"><!-- The indicator (new message, seen, muted, sent) -->  </div></div>

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

Новое сообщение


Я посмотрел, как Facebook работает с индикатором нового сообщения; оказалось, что это кнопка с надписью Mark as read.

<div role="button" aria-label="Mark as read" tabindex="0"></div>



Не знаю, почему команда Facebook выбрала div, а не button. С встроенной кнопкой не нужны атрибуты role, aria-label и tabindex. Все они встроены в кнопку.

Единственный аватар около поста


Такой аватар ничем не отличается от аватара пользователя. В нем применяется элемент svg с атрибутом aria-label, который показывает имя пользователя.


<svg aria-label="Ahmad Shadeed" role="img">  <!-- Mask and image --></svg>


Несколько аватаров около поста


Если честно, это мой любимый вариант. Мне очень нравится, как это сделала команда Facebook.


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

Граница сделана при помощи маски SVG. Да, вы не ослышались!


Маска работает так:



Невероятно. Конкретно здесь мне нравится пользоваться SVG.

Контент справа налево


Когда макет LTR (слева направо), а текст сообщения написан на арабском языке, направление текста тоже должно быть RTL (справа налево).



Элемент .card__content__start это flex-контейнер, поэтому дочерние элементы будут автоматически переворачиваться в зависимости от значения свойства direction у компонента или корневого элемента. Такое поведение можно добавить динамически, в зависимости от языка текста.

<div class="card__content">  <div class="card__content__start" style="direction: rtl"></div>  <div class="card__content__end"></div></div>

Переворачиваем компонент


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



Элементы размещаются с помощью flexbox, поэтому нужно только перевернуть поля.

/* LTR */.card__content__end {  margin-left: 12px;}/* LTR */.card__content__end {  margin-right: 12px;}


Доступность


Работа с клавиатуры


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

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



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



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

Список карточек


В списке карточек прописаны некоторые роли ARIA. Этот список содержит строки и выглядит как сетка. В каждой строке может быть одна или несколько ячеек.

<div role="grid">  <div role="row"><div role="gridcell">  <a href="#"><!-- The component lives here -->  </a></div>  </div>  <div role="row"><div role="gridcell">  <a href="#"><!-- The component lives here -->  </a></div>  </div></div>

Несколько аватаров


Для группового чата есть несколько аватарок индикатора просмотров. Здесь роли ARIA располагают ячейки в ряд.
<div role="grid">  <div role="row"><!-- 1st avatar --><div role="cell"></div><!-- 2nd avatar --><div role="cell"></div>  </div></div>

Посмотрите на демо с сайта Codepen. Всех вариантов здесь нет, я просто проверял их.

Заключение


В этой статье я хотел бы подчеркнуть, что простейший компонент требует огромной работы. Между прочим, все объяснения выше касались только HTML и CSS. А как насчёт JavaScript? Это уже другая история.

Я наслаждался работой, пока писал эту статью, и обязательно поработаю над чем-то подобным в будущем. И еще рад сообщить вам, что я написал электронную книгу об отладке CSS. Если вам интересно, кликните по ссылке debuggingcss.com и посмотрите книгу бесплатно. Вам понравился мой контент? Тогда вы можете заплатить за мой кофе. Большое спасибо!




Подробнее..

Живые интерактивные логи визуализация логов в 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 и креатива наших разработчиков появились живые логи. Обязательно протестируйте их самостоятельно, если вы этого еще не сделали :)

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

Перевод Чем фрагменты могут помочь в Веб-разработке на примере Malina.js

01.08.2020 22:22:07 | Автор: admin


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

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

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

1. Переиспользование фрагмента шаблона


Когда вы рабрабатываете шаблон компоненета, особенно если это форма, элементы ввода, панельки и т.п., у вас может быть повторяющиеся блоки, которые могли бы быть переиспользованы. Ниже кусок шаблона из js-framework-benchmark, в котором есть ряд одинаковых кнопок:


С использованием fragment это может выглядеть так:

Тут объявлен фрагмент button с двумя аргументами id, name, и проброс события click от кнопки @click. Данный ряд кнопок можно было сделать с помощю директивы for/each, но фрагмент может использоватся не только в одну линию.

2. Замыкания


Т.к. фрагмент компиллируется в функцию, значит он может быть объявлен почти где угодно, хоть внутри цикла for/each, таким образом он может использовать замыкания. В примере ниже фрагмент box имеет аргумент text, замкнутую переменную color из цикла, и ссылку на функцию click из корня компонента, которая вызывается при клике: @click={click(text, color)}


3. Рекурсия


Fragment может вызывать сам себя что повзоляет делать рекурсию и строить деревья, так же fragment может распологаться внинзу компонента, т.к. в JavaScript функция доступна по всей (текущей) области видимости, даже если объявлена в конце.
Ниже пример фрагмента draw который вызывает сам себя и строит дерево:


Заключение


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

Все примеры можно попробовать в online редакторе REPL, примеры в gist.

Спасибо за внимание.
Подробнее..

Дружим Angular с Google

27.08.2020 18:04:37 | Автор: admin

Дружим Angular с Google


Google ненавидит SPA


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


Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский опыт приносится в жертву SEO оптимизации. Для сеошника сайт на angular это такая себе проблема поискаовикам трудно индексировать страницы с динамическим контентом.


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


site preview


Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен с на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы наткнулись на Angular Universal. Это модуль Angular для рендеринга на стороне сервера. Сначала нам показалось что вот оно решение, но радость была преждевременной и отсутствие больших проектов, с его применением, было доказательством этого. Шесть месяцев назад мы надеялись найти production ready решение, но поняли, что нет больших проектов, написанных на Universal.


В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2, и ждали, когда Universal будет объединен с Angular Core. На данный момент слияния проектов еще не поизошло, и пока не ясно, когда это произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal уже перекочевал в github репозиторий Angular.


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


Что такое Angular Universal


Прежде всего, давайте обсудим, что такое Angular Universal. Когда мы запустим наше приложение на Angular 2 и откроем исходный код, увидим что-то вроде этого:


<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>Angular 2 app</title>  <!-- base url -->  <base href="http://personeltest.ru/aways/habr.com/"><body>  <app></app></body></html>

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

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


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


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


Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.


Подводные камни Angular Universal


Не трогайте DOM


Когда мы начали тестировать компоненты нашего магазина с помощью Universal, нам пришлось потратить некоторое время, чтобы понять, почему наш сервер падает при запуске без вывода серверной страницы. Например, у нас есть компонент Session Flow component, который отслеживает активность пользователя во время сессии (перемещения пользователя, клики, рефферер, информация об устройстве пользователя и т.д.). После поиска информации в issues на GitHub мы поняли, что в Universal нет обертки над DOM.


DOM на сервере не существует.


Если вы склонируете этот Angular Universal стартер и откроете browser.module.ts вы увидите, что в массиве providers разработчики Universal предоставляют дваboolean значения:


providers: [    { provide: 'isBrowser', useValue: isBrowser },    { provide: 'isNode', useValue: isNode },    ...  ]

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


@Injectable()export class SessionFlow{    private reffererUrl : string;    constructor(@Inject('isBrowser') private isBrowser){        if(isBrowser){            this.reffererUrl = document.referrer;        }    }}

Universal автоматически добавляет false, если это сервер, и true, если браузер. Может быть, позже разработчики Universal пересмотрят эту реализацию и нам не придется беспокоиться об этом.


Если вы хотите активно взаимодействовать с элементами DOM, используйте сервисы Angular API, такие какElementRef, Renderer или ViewContainer.


Правильный роутинг


Поскольку сервер отражает наше приложение, у нас была проблема с роутингом.


Ваш роутинг на клиенте, написанный на Angular, должен соответствовать роутингу на сервере.


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


    [      { path: '', redirectTo: '/home', pathMatch: 'full' },      { path: 'products', component: ProductsComponent },      { path: 'product/:id', component: ProductComponent}    ]

Тогда нужно создать файл server.routes.ts с массивом роутов сервера. Корневой маршрут можно не добавлять:


export const routes: string[] = [  'products',  'product/:id'];

Наконец, добавьте роуты на сервер:


import { routes } from './server.routes';... other server configurationapp.get('/', ngApp);routes.forEach(route => {  app.get(`/${route}`, ngApp);  app.get(`/${route}/*`, ngApp);});

Пререндеринг стартовой страницы


Одной из наиболее важных особенностей Angular Universal является пререндеринг. Из исследования Kissmetrics 47% потребителей ожидают, что веб-страница загрузится за 2 секунды или даже менее. Для нас было очень важно отобразить страницу как можно быстрее. Таким образом, пререндеринг в Universal как раз про нашу задачу. Давайте подробнее рассмотрим, что это такое и как его использовать?


Когда пользователь открывает URL нашего магазина, Universal немедленно возвращает предварительно подготовленную HTML страничку с контентом, а уже затем затем начинает загружать все приложение в фоновом режиме. Как только приложение полностью загрузится, Universal подменяет изначальную страницу нашим приложением. Вы спросите, что будет, если пользователь начнет взаимодействовать со страницей до загрузки приложения? Не беспокойтесь, библиотека Preboot.js запишет все события, которые выполнит пользователь и после загрузки приложения выполнит их уже в приложении.


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


res.render('index', {      req,      res,      preboot: true,      baseUrl: '/',      requestUrl: req.originalUrl,      originUrl: `http://localhost:${ app.get('port') }`    });  });

Добавление мета-тегов


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


Команда Angular Universal создала сервис angular2-meta, чтобы легко манипулировать мета-тегами. Вставьте мета-сервис в ваш компонент и несколько строк кода добавлят мета-теги в вашу страницу:


import { Meta, MetaDefinition } from './../../angular2-meta';@Component({  selector: 'main-page',  templateUrl: './main-page.component.html',  styleUrls: ['./main-page.component.scss']})export class MainPageComponent {  constructor(private metaService: Meta){    const name: MetaDefinition = {      name: 'application-name',      content: 'application-content'    };    metaService.addTags(name);  }}

В следующей версии Angular этот сервис будет перемещен в @angular/platform-server


Кэширование данных


Angular Universal запускает ваш XHR запрос дважды: один на сервере, а другой при загрузке приложения магазина.


Но зачем нам нужно запрашивать данные на сервере дважды? PatricJs создал пример, как сделать Http-запрос на сервере один раз и закэшировать полученные данные для клиента. Посмотреть исходный код примера можно здесь. Чтобы использовать его заинжекте Model service и вызовите метод get для выполнения http-вызовов с кешированием:


    public data;    constructor(public model: ModelService) {        this.universalInit();    }    universalInit() {        this.model.get('/data.json').subscribe(data => {        this.data = data;        });    }

Выводы


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

Подробнее..

Fastify.js не только самый быстрый веб-фреймворк для node.js

04.05.2021 18:23:50 | Автор: admin
Последние 10 лет среди веб-фреймворков для node.js самой большой популярностью пользуется Express.js. Всем, кто с ним работал, известно, что сложные приложения на Express.js бывает сложно структурировать. Но, как говорится, привычка вторая натура. От Express.js бывает сложно отказаться. Как, например, сложно бросить курить. Кажется, что нам непременно нужна эта бесконечная цепь middleware, и если у нас забрать возможность создавать их по любому поводу и без повода проект остановится.

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

Таблица. Показатели популярности пакетов по данным npmjs.org, github.com
Пакет Количество загрузок Количество звезд
1 connect 4 373 963 9 100
2 express 16 492 569 52 900
3 koa 844 877 31 100
4 nestjs 624 603 36 700
5 hapi 389 530 13 200
6 fastify 216 240 18 600
7 restify 93 665 10 100
8 polka 71 394 4 700


Express.js по-прежнему работает в более чем в 2/3 веб-приложений для node.js. Более того, 2/3 наиболее популярных веб-фреймворков для node.js используют подходы Express.js. (Точнее было бы сказать, подходы библиотеки Connect.js, на которой до версии 4 базировался Express.js).

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


Критика фреймворков, основаных на синхронных middleware



Что же плохого может быть в таком коде?

app.get('/', (req, res) => {  res.send('Hello World!')})


1. Функция, которая обрабатывает роут, не возвращает значение. Вместо этого необходимо вызвать один из методов объекта response (res). Если это метод не будет вызван явно, даже после возврата из функции клиент и сервер останутся в состоянии ожидания ответа сервера пока для каждого из них не истечет таймаут. Это только прямые убытки, но есть еще и упущенная выгода. То что эта функция не возвращает значения, делает невозможным просто реализовать востребованную функциональность, например валидацию или логирование возвращаемых клиенту ответов.

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

app.get('/', async (req, res, next) => {   try {      ...   } catch (ex) {      next(ex);   }})


или так:

app.get('/', (req, res, next) => {   doAcync().catch(next)})


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

app.get('/', (req, res) => {  res.send('Hello World!')})


4. Ну и наконец, последнее но немаловажное. В большинстве Express.js приложений работет примерно такой код:

app.use(someFuction);app.use(anotherFunction());app.use((req, res, nexn) => ..., next());app.get('/', (req, res) => {  res.send('Hello World!')})


Когда Вы разрабатываете свою часть приложения, то можете быть уверенным что до вашего кода уже успели отработать 10-20 middleware, которые вешают на объект req всевозможные свойства, и, даже, могут модифицировать исходный запрос, ровно как и в том что столько же если не больше middleware может бтоь добавлено после того, как вы разработаете свою часть приложения. Хотя, к слову сказать, в документации Express.js для навешивания дополнительных свойств неоднозначно рекомендуется объект res.locals:

// из документации Express.jsapp.use(function (req, res, next) {  res.locals.user = req.user  res.locals.authenticated = !req.user.anonymous  next()})


Исторические попытки преодоления недостатков Express.js



Не удивительно, что основной автор Express.js и Connect.js TJ Holowaychuk оставил проект, чтобы начать разработку нового фреймворка Koa.js. Koa.js добавляет асинхронность в Express.js. Например, такой код избавляет от необходимости перехватывать асинхронные ошибки в коде каждого роута и выносит обработчик в один middleware:

app.use(async (ctx, next) => {  try {    await next();  } catch (err) {    // will only respond with JSON    ctx.status = err.statusCode || err.status || 500;    ctx.body = {      message: err.message    };  }})


Первые версии Koa.js имели замысел внедрить генераторы для обработки асинхронных вызовов:

// from http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/var request = Q.denodeify(require('request')); // Example of calling library code that returns a promisefunction doHttpRequest(url) {    return request(url).then(function(resultParams) {        // Extract just the response object        return resultParams[];    });}app.use(function *() {    // Example with a return value    var response = yield doHttpRequest('http://example.com/');    this.body = "Response length is " + response.body.length;});


Внедрение async/await свело на нет полезность этой части Koa.js, и сейчас подобных примеров нет даже в документации фреймворка.

Почти ровесник Express.js фреймворк Hapi.js. Контроллеры в Hapi.js уже возвращают значение, что является шагом вперед, по сравнению с Express.js. Не получив популярность сравнимую с Express.js, мега-успешной стала составная часть проекта Hapi.js библиотека Joi, которая имеет количество загрузок с npmjs.org 3 388 762, и сейчас используется как на бэкенде, так и на фронтенде. Поняв, что валидация входящих объектов это не какой-то особый случай, а необходимый атрибут каждого приложения валидация в Hapi.js была включена как составляющая часть фреймворка, и как параметр в определении роута:

server.route({    method: 'GET',    path: '/hello/{name}',    handler: function (request, h) {        return `Hello ${request.params.name}!`;    },    options: {        validate: {            params: Joi.object({                name: Joi.string().min(3).max(10)            })        }    }});


В настоящее время, библиотека Joi выделена в самостоятельный проект.

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

На сегодняшний день, одно из лучших решений в документации API swagger/openAPI. Было бы очень удачно, если бы схема, описания с учетом требований swagger/openAPI, могла быть использована и для валидации и для формирования документации.

Fastify.js



Подитожу те требования, который мне кажутся существенными при выборе веб-фреймворка:

1. Наличие полноценных контроллеров (возвращаемое значение функции возвращется клиенту в теле ответа).
2. Удобная обработка синхронных и асинхронных ошибок.
3. Валидация входных параметров.
4. Самодокуметирование на основании определений роутов и схем валидации входных/выходных параметров.
5. Инстанциирование асинхронных сервисов.
6. Расширяемость.

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

Поэтому альтернативой может стать фреймворк Fastify.js, особенности применения которого я сейчас разберу.

Fastify.js поддерживает и привычный для разработчиков на Express.js стиль формирования ответа сервера, и более перспективный в форме возвращаемого значения функции, при этом оставляя возможность гибко манипулировать другими параметрами ответа (статусом, заголовками):

// Require the framework and instantiate itconst fastify = require('fastify')({  logger: true})// Declare a routefastify.get('/', (request, reply) => {  reply.send({ hello: 'world' })})// Run the server!fastify.listen(3000, (err, address) => {  if (err) throw err  // Server is now listening on ${address}})


const fastify = require('fastify')({  logger: true})fastify.get('/',  (request, reply) => {  reply.type('application/json').code(200)  return { hello: 'world' }})fastify.listen(3000, (err, address) => {  if (err) throw err  // Server is now listening on ${address}})


Обработка ошибок может быть встроенной (из коробки) и кастомной.

const createError = require('fastify-error');const CustomError = createError('403_ERROR', 'Message: ', 403);function raiseAsyncError() {  return new Promise((resolve, reject) => {    setTimeout(() => reject(new CustomError('Async Error')), 5000);  });}async function routes(fastify) {  fastify.get('/sync-error', async () => {    if (true) {      throw new CustomError('Sync Error');    }    return { hello: 'world' };  });  fastify.get('/async-error', async () => {    await raiseAsyncError();    return { hello: 'world' };  });}


Оба варианта и синхронный и асинхронный отрабатывают одинаково встроенным обработчиком ошибок. Конечно, встроенных возможностей всегда мало. Кастомизируем обработчик ошибок:

fastify.setErrorHandler((error, request, reply) => {  console.log(error);  reply.status(error.status || 500).send(error);});  fastify.get('/custom-error', () => {    if (true) {      throw { status: 419, data: { a: 1, b: 2} };    }    return { hello: 'world' };  });


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

Для валидации Fastify.js использует библиотеку Ajv.js, которая реализует интерфенйс swagger/openAPI. Этот факт делает возможным интеграцию Fastify.js со swagger/openAPI и самодокументирвоание API.

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

const fastify = require('fastify')({  logger: true,  ajv: {    customOptions: {      removeAdditional: false,      useDefaults: true,      coerceTypes: true,      allErrors: true,      strictTypes: true,      nullable: true,      strictRequired: true,    },    plugins: [],  },});  const opts = {    httpStatus: 201,    schema: {      description: 'post some data',      tags: ['test'],      summary: 'qwerty',      additionalProperties: false,      body: {        additionalProperties: false,        type: 'object',        required: ['someKey'],        properties: {          someKey: { type: 'string' },          someOtherKey: { type: 'number', minimum: 10 },        },      },      response: {        200: {          type: 'object',          additionalProperties: false,          required: ['hello'],          properties: {            value: { type: 'string' },            otherValue: { type: 'boolean' },            hello: { type: 'string' },          },        },        201: {          type: 'object',          additionalProperties: false,          required: ['hello-test'],          properties: {            value: { type: 'string' },            otherValue: { type: 'boolean' },            'hello-test': { type: 'string' },          },        },      },    },  };  fastify.post('/test', opts, async (req, res) => {    res.status(201);    return { hello: 'world' };  });}


Поскольку схема входящих объектов уже определена, генерация документации swagger/openAPI сводится к инсталляции плагина:

fastify.register(require('fastify-swagger'), {  routePrefix: '/api-doc',  swagger: {    info: {      title: 'Test swagger',      description: 'testing the fastify swagger api',      version: '0.1.0',    },    securityDefinitions: {      apiKey: {        type: 'apiKey',        name: 'apiKey',        in: 'header',      },    },    host: 'localhost:3000',    schemes: ['http'],    consumes: ['application/json'],    produces: ['application/json'],  },  hideUntagged: true,  exposeRoute: true,});


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

fastify.register(require('fastify-response-validation'));


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

Код связанный с написание статьи можно найти здесь.

Дополнительные источники информации

1. blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators
2. habr.com/ru/company/dataart/blog/312638

apapacy@gmail.com
4 мая 2021 года
Подробнее..

Учим ASP.NET Core новым трюкам на примере Json Rpc 2.0

06.04.2021 04:10:16 | Автор: admin

Хотите добиться нестандартного поведения от aspnet core? Мне вот понадобилось добавить прозрачную поддержку Json Rpc. Расскажу о том, как я искал решения для всех хотелок, чтобы вышло красиво и удобно. Может быть, вам пригодятся знания о разных точках расширения фреймворка. Или о тонкостях поддержки Json Rpc, даже на другом стеке/языке.


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


Введение


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


В тексте под Aspnet подразумевается ASP.Net Core MVC, в частности все писалось на 2.2, с прицелом на то, что выйдет 5.x и допилим под него.

И Json Rpc протокол JSON RPC 2.0 поверх HTTP.

Еще для чтения стоит ознакомиться с терминами протокола: method, params, request, notification...


Зачем все это?


Я .NET техлид в банке Точка и работаю над инфраструктурой для шарповых сервисов. Стараюсь сделать так, чтобы разработчикам было удобно, а бизнесу быстро и без ошибок. Добрался до причесывания обменов под корпоративные стандарты, и тут началось...


У нас для синхронного общения по HTTP принят Json Rpc 2.0. А у шарпистов основной фреймворк ASP.NET Core MVC, и он заточен под REST. И на нем уже написано некоторое количество сервисов. Если немного абстрагироваться, то REST, JSON RPC, и любой RPC вообще об одном и том же: мы хотим, чтобы на удаленной стороне что-то произошло, передаем параметры, ожидаем результат. А еще транспорт совпадает, все по HTTP. Почему бы не воспользоваться привычным aspnet для работы с новым протоколом? Хочется при этом поддержать стандарт полностью: в компании много разных стеков, и у всех Json Rpc клиенты работают немного по-разному. Будет неприятно нарваться на ситуацию, когда запросы например от питонистов не заходят, и нужно что-то костылить.


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


Еще немаловажный момент: желательно, чтобы опыт у разработчиков не терял актуальность, то есть не затачивался на внутренние костыли и самописные фреймворки. Если ты три года пишешь веб-сервисы на шарпе, претендуешь на мидлосеньора, а потом не можешь сделать, например, авторизацию общепринятыми способами в пустом проекте, потому что у вас было принято писать в коде контроллера if(cookie.Contains(userName)) это беда.


Конечно, протокол уже реализован на C#, и не раз. Ищем готовые библиотеки. Выясняется, что они либо тащат свои концепции, то есть придется долго вникать, как это готовить. Либо делают почти то, что нужно, но тяжело кастомизируются и переизобретают то, что в aspnet уже есть.

Собираем хотелки и пишем код


Чего хочется добиться? Чтобы как обычно писать контроллеры, накидывать фильтры и мидлвари, разбирать запрос на параметры. Чтобы наработанные best practices и библиотеки для aspnet подходили as-is. И при этом не мешать работать существующему MVC коду. Так давайте научимся обрабатывать Json Rpc теми средствами, что нам предоставляет фреймворк!


Request Routing


Казалось бы, HTTP уже есть, и нам от него надо только обрабатывать POST и возвращать всегда 200 OK. Контент всегда в JSON. Все прекрасно, сейчас напишем middleware и заживем.
Но не тут-то было! Мидлварь написать можно, только потом будем получать все запросы в один action, а в нем придется switch(request.Method) или запускать хендлеры из DI каким-нибудь костылем. А кто будет авторизацию и прочие фильтры прогонять в зависимости от метода? Переизобретать все это заново ящик Пандоры: делаешь свой аналог пайплайна, а потом придется поддерживать общий код и для aspnet, и для своего пайплайна. Ну, чтобы не было внезапных различий между тем, как вы проверяете роли или наличие какого-то заголовка.
Значит, придется влезть в роутинг и заставить его выбирать controller и action, глядя на тело HTTP запроса, а не только на url.


ActionMethodSelectorAttribute


К сожалению, все часто используемые инструменты для роутинга не позволяют парсить запрос или выполнять произвольный код. Есть стандартный роутер, но его нельзя без лишних проблем расширить или что-то в нем перегрузить. IRouter целиком писать, конечно же, не надо, если мы хотим гарантировать что обычный MVC не сломается. Казалось бы, есть Endpoint Routing, но в 2.2 сделана его начальная и неполная реализация, и чего-то полезного с ним напрямую не сделаешь. Костыли типа переписывания еndpoint на свой (после того, как Endpoint Routing отработал) почему-то не взлетели. Можно, конечно, прямо в middleware сделать редирект на нужный url, и это будет работать, только url испортится.


После долгих поисков был найден ActionMethodSelectorAttribute, который делает как раз то, что нужно: позволяет вернуть true/false в момент выбора controller и action! У него есть контекст с именем текущего метода-кандидата и его контроллера. Очень удобно.


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


Conventions


Атрибуты на контроллеры можно расставлять кодогенерацией, но это слишком сложно. У фреймворка и на этот случай есть решение: IControllerModelConvention, IActionModelConvention. Это что-то вроде знакомых многим Startup Filters: запускаются один раз на старте приложения и позволяют сделать все что угодно с метаданными контроллеров и методов, например переопределить роутинг, атрибуты, фильтры.


С помощью conventions мы можем решить сразу несколько задач. Сначала определимся, как мы будем отличать Json Rpc контроллеры от обычных. Не долго думая, идем по тому же пути, что и Microsoft: сделаем базовый класс по аналогии с ControllerBase.


public abstract class JsonRpcController : ControllerBase {}

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


полезный код в ControllerConvention
public void Apply(ControllerModel controllerModel){    if (!typeof(JsonRpcController).IsAssignableFrom(controllerModel.ControllerType))    {        return;    }    controllerModel.Selectors.Clear();    controllerModel.Filters.Insert(0, new ServiceFilterAttribute(typeof(JsonRpcFilter)));}

Selectors отвечают за роутинг, и я честно не смог найти, почему это коллекция. В любом случае, нам не нужен стандартный роутинг по правилам MVC, поэтому удаляем все, что есть. Забегая вперед, применяем JsonRpcFilter, который будет отвечать за оборачивание ActionResult.


А вот ActionConvention
public void Apply(ActionModel actionModel){    if (!typeof(JsonRpcController).IsAssignableFrom(actionModel.Controller.ControllerType))    {        return;    }    actionModel.Selectors.Clear();    actionModel.Selectors.Add(new SelectorModel()    {        AttributeRouteModel = new AttributeRouteModel() {Template = "/api/jsonrpc"},        ActionConstraints = {new JsonRpcAttribute()}    });}

Здесь на каждый метод в наших контроллерах мы повесим один и тот же route, который потом вынесем в настройки.


И добавим тот самый атрибут
class JsonRpcAttribute : ActionMethodSelectorAttribute{    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)    {        var request = GetRequest();  // пока не понятно как        // return true если action подходит под запрос, например:        return request.Method == action.DisplayName;    }}

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


Middleware


Пишем middleware, которая будет проверять, что запрос похож на Json Rpc: правильный Content-Type, обязательно POST, и тело содержит подходящий JSON. Запрос можно десериализовать в объект и сложить в HttpContext.Items. После этого его можно будет достать в любой момент.


Есть одна загвоздка: у middleware еще нет информации о типе, в который нужно десериализовать params, поэтому мы пока оставим их в виде JToken.


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


Parameter Binding


Мы научились подбирать контроллер и метод под запрос. Теперь нужно что-то делать с аргументами. Можно достать то, что десериализовала middleware из HttpContext.Items, и десериализовать JToken вручную в нужный тип, но это бойлерплейт и ухудшение читаемости методов. Можно взять JSON целиком из тела запроса с помощью [FromBody], но тогда всегда будет присутствовать шапка протокола: id, версия, метод. Придется каждую модель оборачивать этой шапкой: Request<MyModel> или class MyModel: RequestBase, и снова получим бойлерплейт.


Эти решения были бы еще терпимы, если бы протокол не вставлял палок в колеса.


Разные params


Json Rpc считает, что параметры, переданные массивом [] это одно и то же, что и параметры, переданные объектом {}! То есть, если нам прислали массив, нужно подставлять их в свой метод по порядку. А если прислали объект, то разбирать их по именам. Но вообще, оба сценария должны работать для одного и того же метода. Например, вот такие params эквивалентны и должны одинаково биндиться:


{"flag": true, "data": "value", "user_id": 1}[1, "value", true]

public void DoSomething(int userId, string data, bool flag)

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


Реализация


Посмотрим, что нам доступно для управления биндингом. Есть IModelBinder и IModelBinderProvider, но они смотрят на тип объекта. Заранее мы не знаем, какой тип пользователь захочет биндить. Может быть, int или DateTime. Мы не хотим конфликтовать с aspnet, поэтому просто добавить свой биндер для всех типов нельзя. Есть IValueProvider, но он возвращает только строки. Наконец, есть атрибуты FromBody, FromQuery и так далее. Смотрим в реализацию, находим интерфейс IBinderTypeProviderMetadata. Он нужен, чтобы возвращать нужный binder для параметра. Как раз то, что нужно!


Пишем свой FromParamsAttribute
 [AttributeUsage(AttributeTargets.Parameter)]public class FromParamsAttribute : Attribute, IBindingSourceMetadata, IBinderTypeProviderMetadata{    public BindingSource BindingSource => BindingSource.Custom;    public Type BinderType => typeof(JsonRpcModelBinder);}

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


`BindingInfo` с той же информацией
public void Apply(ParameterModel parameterModel){    if (!typeof(JsonRpcController).IsAssignableFrom(parameterModel.Action.Controller.ControllerType))    {        return;    }    if (parameterModel.BindingInfo == null)    {        parameterModel.BindingInfo = new BindingInfo()        {            BinderType = typeof(JsonRpcModelBinder),            BindingSource = BindingSource.Custom        };    }}

Проверка на BindingInfo == null позволяет использовать другие атрибуты, если нужно. То есть можно смешивать FromParams и штатные FromQuery, FromServices. Ну а по умолчанию, если ничего не указано, convention применит BindingInfo, аналогичный FromParams.


Удобства


Стоит учесть сценарий, когда неудобно разбирать params на отдельные аргументы. Что, если клиент просто прислал свой объект "как есть", а в нем очень много полей? Нужно уметь биндить params целиком в один объект:


{"flag": true, "data": "value", "user_id": 1}

public void DoSomething(MyModel model)

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


BindingStyle
public enum BindingStyle { Default, Object, Array }...public FromParamsAttribute(BindingStyle bindingStyle){    BindingStyle = bindingStyle;}

Default поведение по умолчанию, когда содержимое params биндится в аргументы. Object когда пришел json-объект, и мы биндим его в один параметр целиком. Array когда пришел json-массив и мы биндим его в коллекцию. Например:


// это успешно сбиндится: {"flag": true, "data": "value", "user_id": 1}// а это будет ошибкой: [1, "value", true]public void DoSomething1([FromParams(BindingStyle.Object)] MyModel model)// это успешно сбиндится: [1, "value", true]// а это будет ошибкой: {"flag": true, "data": "value", "user_id": 1}public void DoSomething2([FromParams(BindingStyle.Array)] List<object> data)

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


Как сопоставить имя аргумента и ключ в json-объекте?


JsonSerizlizer не позволяет "в лоб" десериализовать ключ json объекта как шарповое имя property или аргумента. Зато позволяет сериализовать имя в ключ.


// вот так не получится{"user_id": 1} => int userId//  зато можно наоборот и запомнить это в метаданныхint userId => "user_id"

То есть нужно для каждого аргумента узнать его "json-имя" и сохранить в метаданных. У нас уже есть conventions, там и допишем нужный код.


Учимся у aspnet


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


Регистрация в DI-контейнере


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


резолвить зависимости вручную
public Task BindModelAsync(ModelBindingContext context){    var service = context.HttpContext.RequestServices.GetServices<IService>();    // ...}

Error handling


Все ошибки протокол предлагает возвращать в виде специального ответа, в котором могут быть любые детали. Еще там описаны некоторые крайние случаи и ошибки для них. Придется перехватывать все exception-ы, заворачивать их в Json Rpc ответ, уметь прятать stack trace в зависимости от настроек (мы же не хотим высыпать все подробности на проде?). А еще нужно дать пользователю возможность вернуть свою Json Rpc ошибку, вдруг у кого-то на этом логика построена. В общем, ошибки придется перехватывать на разных уровнях. После написания десятого try/catch внутри catch поневоле начинаешь задумываться, что неплохо бы иметь возможность писать код с гарантией отсутствия exception-ов, или хотя бы с проверкой, что ты перехватил все, что можно...


Action вернул плохой ActionResult или Json Rpc ошибку


Возьмем IActionFilter.OnResultExecuting и будем проверять, что вернулось из метода: нормальный объект завернем в Json Rpc ответ, плохой ответ, например 404, завернем в Json Rpc ошибку. Ну или метод уже вернул ошибку по протоколу.


Binding failed


Нам пригодится IAlwaysRunResultFilter.OnActionExecuting: можно проверить context.ModelState.IsValid и понять, что биндинг упал. В таком случае вернем ошибку с сообщением, что не получилось у биндера. Если ничего не делать, то в action попадут кривые данные, и придется проверять каждый параметр на null или default.


Схожим образом работает стандартный ApiControllerAttribute: он возвращает 400, если биндинг не справился.


Что-то сломалось в pipeline


Если action или что-нибудь в pipeline выбросит exception, или решит записать HttpResponse, то единственное место, где мы еще можем что-то сделать с ответом, это middleware. Придется и там проверять, что получилось после обработки запроса: если HTTP статус не 200 или тело не подходит под протокол, придется заворачивать это в ошибку. Кстати, если писать ответ прямо в HttpResponse.Body, то сделать с ним уже ничего не получится, но для этого тоже будет решение чуть ниже.


Ошибки это сложно


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


class JsonRpcErrorFactory{    IError NotFound(object errorData){...}    IError InvalidRequest(object errorData){...}    IError Error(int code, string message, object errorData){...}    IError Exception(Exception e){...}    // и так далее}

Batch


Batch-запросы aspnet не поддерживает никак. А они требуются стандартом. И хочется, чтобы на каждый запрос из батча был свой пайплайн, чтобы в тех же фильтрах не городить огород. Можно, конечно, сделать прокси, который будет разбирать батч на отдельные запросы, отправлять их на localhost, потом собирать ответ. Но это кажется безумным оверхедом из-за сериализации HTTP body в байты, установления соединения После долгих путешествий по issues в Github, находим грустный тред о том, что батчи когда-то были, но пока нет и неизвестно когда вернутся. А еще они есть в OData, но это целый отдельный мир, фреймворк поверх фреймворка погодите-ка, мы же тоже пишем что-то такое!. Там же находим идею и репозиторий с реализацией: можно скопировать HttpContext и в middleware позвать next() со своим контекстом, а потом собрать результат и отправить все вместе уже в настоящий HttpContext. Это поначалу немного ломает мозг, потому что мы привыкли к мантре: нужно передавать управление вызовом next(context), и по-другому никто эту штуку не использует.


Таким образом, middleware будет парсить Json Rpc запрос, создавать копию контекста, и вызывать пайплайн дальше. Это же пригодится для перехвата ошибок, если кто-то решит писать прямо в HttpResponse.Body: мы вместо настоящего body подсунем MemoryStream и проверим, что там валидный JSON.


У этого подхода есть минус: мы ломаем стриминг для больших запросов/ответов. Но что поделать, JSON не подразумевает потоковую обработку. Для этого, конечно, есть разные решения, но они гораздо менее удобны, чем Json.NET.


ID


Протокол требует поле id в запросе, при чем там могут быть число, строка или null. В ответе должен содержаться такой же id. Чтобы одно и то же поле десериализовалось как число или строка, пришлось написать классы-обертки, интерфейс IRpcId и JsonConverter, который проверяет тип поля и десериализует в соответствующий класс. В момент, когда мы сериализуем ответ, из HttpContext.Items достаем IRpcId и прописываем его JToken-значение. Таким образом, пользователю не надо самому заморачиваться с проставлением id и нет возможности забыть об этом. А если нужно значение id, можно достать из контекста.


Notification


Если id отсутствует, то это не запрос, а уведомление (notification). На notification не должен уходить ответ: ни успешный, ни с ошибкой, вообще никакой. Ну, то есть по HTTP-то мы вернем 200, но без тела. Чтобы все работало одинаково для запросов и нотификаций, пришлось выделить абстракцию над ними, и в некоторых местах проверять, запрос ли это и нужно ли сериализовать ответ.


Сериализация


Aspnet умеет сериализовать JSON. Только у него свои настройки, а у нас свои. Сериализация настраивается с помощью Formatters, но они смотрят только на Content-Type. У Json Rpc он совпадает с обычным JSON, поэтому просто так добавить свой форматтер нельзя. Вложенные форматтеры или своя реализация плохая идея из-за сложности.


Решение оказалось простым: мы уже оборачиваем ActionResult в фильтре, там же можно


подставить нужный форматтер
...var result = new ObjectResult(response){    StatusCode = 200,};result.Formatters.Add(JsonRpcFormatter);result.ContentTypes.Add(JsonRpcConstants.ContentType);...

Здесь JsonRpcFormatter это наследник JsonOutputFormatter, которому переданы нужные настройки.


Configuration


Нужно дать пользователю разные "ручки". При чем удобно дать настройку по умолчанию (например дефолтный route) и возможность умолчание обойти, например, атрибутом: когда вот для этого контроллера нужен свой особенный route.


Имя метода


У Json Rpc запросов есть поле method, которым определяется, что должно быть вызвано на сервере. И это поле просто строка. Ей пользуются как угодно. Придется научить сервер понимать распространенные варианты.


public enum MethodStyle {ControllerAndAction, ActionOnly}

ControllerAndAction будет интерпретировать method как class_name.method_name.


ActionOnly просто method_name.


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


Сериализация


Еще встает вопрос с JSON-сериализацией. Формат "шапки" строго обозначен в протоколе, то есть переопределять его нужно примерно никогда. А вот формат полей params, result и error.data оставлен свободным. Пользователь может захотеть сериализацию со своими особыми настройками. Нужно дать такую возможность, при этом не позволяя сломать сериализацию шапки и не накладывая особых требований на пользователя. Например, для работы с шапкой используются хитрые JsonConverterы, и не хотелось бы чтобы они как-то торчали наружу. Для этого сделана минимальная обертка поверх JsonSeralizer, чтобы пользователь мог зарегистрировать в DI свой вариант и не сломать REST/MVC.


Нестандартные ответы


Бывает, что нужно вернуть бинарный файл по HTTP, или Redirect для авторизации. Это явно идет в разрез с Json Rpc, но очень удобно. Такое поведение нужно разрешать при необходимости.


Объединяем все вместе


Добавим классы с опциями, чтобы рулить умолчаниями
public class JsonRpcOptions{    public bool AllowRawResponses { get; set; }  // разрешить ответы не по протоколу?    public bool DetailedResponseExceptions { get; set; }  // маскировать StackTrace у ошибок?    public JsonRpcMethodOptions DefaultMethodOptions { get; set; }  // см. ниже    public BatchHandling BatchHandling { get; set; }  // задел на параллельную обработку батчей в будущем}public class JsonRpcMethodOptions{        public Type RequestSerializer { get; set; }  // пользовательский сериалайзер    public PathString Route { get; set; }  // маршрут по умолчанию, например /api/jsonrpc    public MethodStyle MethodStyle { get; set; }  // см. выше}

И атрибуты, чтобы умолчания переопределять:


  • FromParams про который было выше
  • JsonRpcMethodStyle чтобы переопределить MethodStyle
  • JsonRpcSerializerAttribute чтобы использовать другой сериалайзер.

Для роутинга свой атрибут не нужен, все будет работать со стандартным [Route].


Подключаем


Пример кода, который использует разные фичи. Важно заметить, что это никак не мешает обычному коду на aspnet!


Startup.cs
services.AddMvc()    .AddJsonRpcServer()    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);// или с опциямиservices.AddMvc()    .AddJsonRpcServer(options =>    {        options.DefaultMethodOptions.Route = "/rpc";        options.AllowRawResponses = true;    })    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Контроллер
public class MyController : JsonRpcController{    public ObjectResult Foo(object value, bool flag)    {        return Ok(flag ? value : null);    }    public void BindObject([FromParams(BindingStyle.Object)] MyModel model)    {    }    [Route("/test")]    public string Test()    {        return "test";    }    [JsonRpcMethodStyle(MethodStyle.ActionOnly)]    public void SpecialAction()    {    }    [JsonRpcSerializer(typeof(CamelCaseJsonRpcSerializer))]    public void CamelCaseAction(int myParam)    {    }}

Клиент


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


HttpClient


В .net core HttpClient научили работать с DI, типизировать и вообще все стало гораздо удобнее. Грех не воспользоваться!


Batch


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


Обработка ошибок


Снова сложности с обработкой ошибок. Дело в том, что мы не знаем, с какими правилами сериализации сервер вернул ошибку: со стандартными, как в "шапке", или с кастомными, например когда мы договорились на клиенте и на сервере использовать camelCase, но у сервера что-то сломалось в middleware и дело до нашего action не дошло вообще. Поэтому придется пробовать десериализовать и так, и так. Здесь нет очевидно хорошего решения, поэтому интерфейс response содержит


Разные методы для интерпретации ответа
T GetResponseOrThrow<T>();  // достать успешный ответ, если нет - достать ошибку и выбросить ее как исключениеT AsResponse<T>(); // только достать ответError<JToken> AsAnyError(); // достать ошибку, не десериализуя ееError<T> AsTypedError<T>(); // достать ошибку по правилам сериализации как в запросе или по дефолтным, если не получилосьError<ExceptionInfo> AsErrorWithExceptionInfo(); // достать ошибку с деталями exception-а с сервера

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


Developer Experience


Я считаю, что получилось решение, когда разработчик может подключить и забыть, что там какой-то Json Rpc. При этом можно полагаться на свой опыт работы с aspnet, использовать привычные подходы и без проблем подключать любые сторонние библиотеки к приложению. С другой стороны, есть возможность переопределять какое-то поведение, мало ли какие потребности возникнут. Часто используемые штуки вынесены в параметры и атрибуты, а если не хватает, можно посмотреть код и подменить что-то: все методы виртуальные, сервисы используются по интерфейсам. Можно расширить или написать полностью свою реализацию.


TODO


В планах: добавить поддержку aspnetcore 5.x, добить покрытие тестами до 100%, перевести документацию на русский и добавить параллельную обработку батчей. Ну и, конечно же, поддерживать проект как нормальный open source: любой фидбек, пуллреквесты и issues приветствуются!


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


Ссылки


Исходники


Документация


Бонус


Статья лежала в черновиках почти год, за это время к библиотеке была добавлена поддержка автодокументации Swagger и OpenRpc. А еще сейчас в разработке поддержка OpenTelemetry. Кому-нибудь интересны подробности? Там местами такая жуть...

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru