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

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

Перевод Три малоизвестных факта об AVIF

25.12.2020 12:18:15 | Автор: admin
AVIF графический формат, основанный на видеокодеке AV1, представляет собой один из самых современных форматов хранения изображений. Судя по ранним публикациям и исследованиям, AVIF показывает достойные результаты в сравнении с JPEG и WebP. Но, даже учитывая то, что этот формат хорошо поддерживается браузерами, AVIF, в плане кодирования и декодирования изображений, всё ещё представляет собой ультрасовременную технологию, которой свойственны определённые проблемы.



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

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


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

Вероятно, это вполне поддаётся объяснению, а систему кодирования изображений можно настроить так, чтобы решить эту проблему. Но большинство тех, кто пользуется AVIF, не станет заниматься чем-то подобным. Эти люди, вероятно, просто воспользуются возможностями оптимизаторов изображений, вроде squoosh.app, или функциями CDN, ориентированных на работу с изображениями, вроде ImageEngine. На следующем графике приведено сравнение размеров AVIF-изображений, получаемых средствами двух вышеупомянутых сервисов, с размерами оптимизированных WebP-изображений.


Зависимость эффективности сжатий изображений от их размера в пикселях

Видно, что обычно байтовые размеры WebP-изображений оказываются больше размеров AVIF-изображений. На изображениях, имеющих большие пиксельные размеры, AVIF показывает себя лучше, при этом с большими изображениями ImageEngine работает значительно эффективнее, чем squoosh.app.

А теперь обратим внимание на одну примечательную деталь, видную в начале графиков. На изображениях размером примерно 100x100 пикселей squoosh.app работает лучше ImageEngine, а затем, на изображениях размером примерно 80x80 пикселей, как и на изображениях меньшего размера, первенство оказывается за WebP.

Этот тест показывает сравнительно однородные результаты на изображениях разных типов. В данном случае использовалось это изображение с Picsum.
Размер (пиксели) Исходный JPEG-файл (байты) Оптимизированный WebP-файл (байты) ImageEngine, AVIF-файл (байты) squoosh.app, AVIF-файл (байты)
50 1,475 598 888 687
80 2,090 1,076 1,234 1,070
110 3,022 1,716 1,592 1,580
150 4,457 2,808 2,153 2,275
170 5,300 3,224 2,450 2,670
230 7,792 4,886 3,189 3,900
290 10,895 6,774 4,056 5,130

2. Формат AVIF, возможно, не очень хорошо подходит для хранения высокоэнтропийных изображений товаров


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

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

Ниже показан увеличенный фрагмент фотографии товара (вот её полные JPEG и AVIF-варианты), на котором чётко видна разница между обычным оптимизированным JPEG-изображением и AVIF-изображением, оптимизированным с помощью squoosh.app.


Сравнение JPEG и AVIF

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

Более того, AVIF, в отличие от JPEG, не поддерживает прогрессивный рендеринг изображений. При создании типичной страницы товара прогрессивный рендеринг может стать важнейшим механизмом по улучшению ключевых показателей производительности страниц. Например, это нечто вроде показателя Largest Contentful Paint (LCP, время отображения наибольшего элемента страницы) и других метрик Core Web Vitals. Даже если на загрузку JPEG-версии изображения требуется немного больше времени, что объясняется тем, что JPEG-файл больше AVIF-файла, JPEG-файл, скорее всего, начнёт рендериться раньше, чем AVIF-файл. Понаблюдать за подобным поведением изображений разных форматов можно в этом видео.

3. Формат JPEG 2000 это сильный конкурент AVIF


Главной сильной стороной AVIF называют чрезвычайно маленький размер файлов при приемлемом визуальном качестве изображений. В ранних публикациях об AVIF и в отчётах об исследованиях этого формата основное внимание уделялось именно этому моменту. Но в некоторых случаях те же задачи, для решения которых применяют AVIF, можно более эффективно решить, прибегнув к формату JPEG 2000 (JP2). JPEG 2000 это сравнительно старый формат, он не привлекает к себе столько же внимания, сколько AVIF, даже учитывая то, что его уже поддерживает компания Apple.

Для того чтобы на практике разобраться в том, о чём идёт речь, давайте посмотрим на этого очаровательного щенка. Размер файла AVIF-версии изображения, созданный средствами squoosh.app с применением стандартных настроек, имеет размер 27,9 Кб. Преобразование того же изображения в формат JPEG 2000 средствами ImageEngine даёт файл размером 26,7 Кб. Разница небольшая, но достаточная для демонстрации возможностей JPEG 2000.

А как насчёт визуального качества изображений? Для сравнения как-либо обработанных версий изображений с их оригиналами часто используется оценка их структурных различий (DSSIM, Structural Dissimilarity). При расчёте показателя DSSIM исходное изображение сравнивается с его вариантом, преобразованным в другой формат. Чем ниже этот показатель тем выше качество нового изображения. Инструмент для вычисления показателя DSSIM, ссылка на который дана выше, работает с PNG-файлами. Для проведения сравнения файлы форматов JPEG 2000 и AVIF без потерь преобразовали в формат PNG. Результаты эксперимента обобщены в следующей таблице.
Формат DSSIM (0 неотличим от оригинала) Размер файла (Кб)
JPEG 2000 0,019 26,7
AVIF 0,012 27,9

AVIF отличается немного лучшим показателем DSSIM, но разница в качестве изображений практически незаметна.


Слева направо: исходное JPEG-изображение, AVIF-изображение, JPEG 2000-изображение

Итоги


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

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

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

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

Пользуетесь ли вы графическим форматом AVIF?



Подробнее..

Перевод Нет серверным веб-приложениям

27.01.2021 12:10:40 | Автор: admin
В 1993 году, когда появилась Всемирная паутина, World-Wide-Web, веб-страницы были представлены статическими HTML-файлами, содержащими ссылки на другие такие же файлы. Но вскоре, благодаря таким технологиям, как CGI, Perl и Python, веб-сайты стали оснащать динамическим функционалом, который серьёзно расширил их возможности.

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

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


Прекратите писать серверные веб-приложения

Веб-запрос: путь позора


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

Как вам, возможно, известно, любое приложение, вышедшее за пределы Hello World, очень быстро начинает сильно усложняться. Часто один запрос инициирует несколько обращений к базе данных или к другим внешним API. А чем больше обращений к базе данных и к различным службам тем выше сетевая задержка. На разных уровнях информационных систем могут присутствовать кеши, нацеленные на решение проблем с производительностью, но усложняющие устройство систем. То, что мы называем сервером, на самом деле, может быть представлено несколькими серверами, находящимися за балансировщиком нагрузки, что усложняет развёртывание систем.


Веб-запрос: путь позора

Обычно монолитные серверные приложения превращаются в монстров. А разбиение монолитного приложения на микросервисы означает необходимость выполнения больших объёмов работы. Это, кроме того, ещё сильнее усложняет информационные системы и плохо отражается на величинах задержек. Что же делать веб-разработчикам?

Веб-разработчикам стоит присмотреться к JAMstack


В последнее время то и дело встречаешься с понятием JAMstack. Может, и вы что-то об этом слышали? Если предельно просто объяснить суть этого понятия, то окажется, что в JAMstack используются генераторы статических сайтов, с помощью которых создают страницы, содержащие HTML, CSS, JavaScript и другие материалы, а потом размещают это всё на CDN (Content Delivery Network, сеть доставки контента).

Но это же статический HTML? Это всё равно, что размещать на серверах простые HTML-файлы? Шутите, что ли?

На первый взгляд JAMstack выглядит как шаг назад. Правда?

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

В общем, я попросту сбросил JAMstack со счетов. Прошу вас не поступайте так, как я.

Правда о JAMstack


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


Самый быстрый и безопасный код это его отсутствие

Буквы JAM в слове JAMstack это сокращение от JavaScript, API и Markup (заранее отрендеренная разметка). За JAMstack стоит сравнительно простая идея. Абсолютно всё, что можно, нужно превратить в заранее отрендеренную разметку. Затем, после того, как эта разметка достигнет браузера, нужно, прогрессивно используя JavaScript и обращения к различным API, сделать приложение настолько динамическим и персонализированным, насколько это соответствует нуждам конкретного проекта. Подход к созданию сайтов, реализуемый в рамках JAMstack, перемещает вычисления, необходимые для приведения приложения в работоспособное состояние, с сервера в браузер.

В последнее время генераторы статических сайтов стали чрезвычайно функциональными. Они поддерживают массу возможностей, которые способны удовлетворить широкому спектру потребностей разработчиков. Кроме того, для тех, кому нужны простые инструменты для управления содержимым сайтов, появился новый рынок CMS (Content Management System, система управления контентом) без пользовательского интерфейса. Такие системы отлично дополняют подход к разработке проектов, применяемый в JAMstack. В ходе выполнения сборки проекта генератор статических сайтов может обращаться к API, к базам данных, к CMS, имеющим API, или к чему угодно другому, необходимому для максимизации объёма заранее отрендеренной разметки.

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

Лучшее из двух миров


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

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

  • Упрощение архитектуры проектов.
  • Использование лучших API сторонних разработчиков.
  • Применение возможностей CDN.
  • Возможность уделять больше внимания удобству приложений для пользователей и соответствию веб-проектов нуждам заказчиков приложений.

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

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

Пользуетесь ли вы JAMstack?
Подробнее..

Перевод 14 полезных инструментов, ускоряющих и упрощающих веб-разработку

22.03.2021 12:19:24 | Автор: admin
Я занимаюсь разработкой сайтов уже много лет и не верю в то, что если буду держать в секрете используемые мной инструменты, это даст мне серьёзное преимущество на рынке труда.

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



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

1. Metatags.io


Я применяю metatags.io для исследования внешнего вида моих сайтов, выводимых в областях предварительного просмотра на ресурсах Google, Facebook или Twitter. Metatags.io позволяет увидеть заголовок, описание и изображение, соответствущие сайту, в точно таком же представлении, как их видит посетитель вышеупомянутых ресурсов.

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


Metatags.io

2. ExtractCSS.com


Я применяю extractcss.com для извлечения из HTML-документов сведений о разнообразных стилях HTML-документов (речь идёт о стилях, назначаемых идентификаторам и классам элементов, о встроенных стилях) и для формирования на их основе CSS-кода. В ходе работы с этим ресурсом не нужно делать ничего особенного: достаточно скопировать в соответствующее окно HTML-код, а всё остальное делается автоматически.


ExtractCSS.com

3. Whatruns.com


Ресурс whatruns.com всегда приходит мне на помощь в ситуациях, когда надо узнать подробности об устройстве какого-нибудь сайта. Речь идёт о темах и плагинах, используемых на сайтах, о серверах, на которых они размещены. Нетребовательное к системным ресурсам расширение Whatruns можно установить в Firefox и Chrome.


Whatruns.com

4. Unminify.com


Инструмент unminify.com позволяет превращать минифицированный (упакованный, обфусцированный) код (JavaScript, CSS, HTML, XML, JSON) в удобный для восприятия вид.


Unminify.com

5. Расширение для Chrome Octotree


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


Расширение для Chrome Octotree

6. Расширение для Chrome Web Developer Checklist


Иногда я забываю о тщательной проверке различных деталей, касающихся веб-сайтов, над которыми работаю. Расширение для Chrome Web Developer Checklist помогло мне справиться с этой проблемой. Благодаря ему я уже не забываю о проверке самых разных аспектов сайтов от фронтенда и SEO, до их мобильных версий.


Расширение для Chrome Web Developer Checklist

7. Расширение для Chrome Web Developer Form Filler


Расширение Web Developer Form Filler пригодится тем разработчикам, кому, как и мне, надоедает заполнять всяческие формы, тестируя их в ходе создания сайтов. Благодаря этому расширению можно сэкономить время и сделать своё дело быстрее, чем без него.


Расширение для Chrome Web Developer Form Filler

8. Расширение для Chrome EditThisCookie


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


Расширение для Chrome EditThisCookie

9. Расширение для Chrome GTMetrix


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


Расширение для Chrome GTMetrix

10. Расширение для Chrome Google PageSpeed Insights Extension


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


Расширение для Chrome Google PageSpeed Insights Extension

11. Расширение для Chrome Browserling Cross-browser testing


Расширение Browserling позволяет просматривать сайты в различных браузерах. Например в Opera, Chrome и Firefox. Оно, кроме того, позволяет выбирать операционную систему, в которой работает интересующий нас браузер, в частности разные версии Windows и Android.


Расширение для Chrome Browserling Cross-browser testing

12. Responsively.app


Проект responsively.app даёт веб-разработчику инструмент для одновременной проверки внешнего вида его сайта на разных устройства. Раньше, когда я не знал о responsively.app, то же самое я делал, исследуя разные представления сайта по одному. Несложно представить себе то, как много времени позволяет сэкономить этот инструмент.


Responsively.app

13. Compressior.io


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


Compressor.io

Этот онлайн-инструмент можно использовать для оптимизации и сжатия изображений в форматах JPEG, PNG, SVG, GIF и WEBP.

14. DrawKit.io


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


DrawKit.io

А какими вспомогательными инструментами пользуетесь при разработке сайтов вы?

Подробнее..

Перевод Ограничения window.close()

03.04.2021 14:10:36 | Автор: admin


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

Scripts may close only the windows that were opened by them.

Почему браузеры ограничивают команду close()?


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

Иногда такое поведение браузеров объясняют, ссылаясь на некие таинственные соображения безопасности. Но основная причина ограничений, применяемых к close(), больше связана с тем, что называют пользовательский опыт. А именно, если скрипты смогут свободно закрывать любые вкладки браузеров, пользователь может потерять важные данные, состояние веб-приложения, работающего во вкладке. Это, кроме того, если вкладка или окно браузера неожиданно закрывается, может привести к нарушению механизмов перемещения по истории посещения страниц. Такие перемещения выполняются браузерными кнопками Вперёд и Назад (в Internet Explorer мы называли этот механизм TravelLog). Предположим, пользователь применяет вкладку браузера для исследования результатов поиска. Если одна из изучаемых им страниц сможет закрыть вкладку, хранящую стек навигации, историю посещённых страниц, среди которых страница с результатами поиска, это будет довольно-таки неприятно.

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

Что написано в стандартах?


Вот что об этом всём говорится в разделе dom-window-close стандарта HTML:

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

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

Как поступают браузеры?


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

Internet Explorer


В Internet Explorer вкладка или окно браузера закрывается без лишних вопросов в том случае, если для создания этой вкладки или этого окна была использована команда window.open(). Браузер не пытается удостовериться в том, что история посещений страниц вкладки содержит лишь один документ. Даже если у вкладки будет большой TravelLog, она, если открыта скриптом, просто закроется. (IE, кроме того, позволяет HTA-документам закрывать самих себя без каких-либо ограничений).

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


Окна для подтверждения закрытия вкладки или окна

Chromium (Microsoft Edge, Google Chrome и другие браузеры)


В Chromium 88 команда window.close() выполняется успешно в том случае, если у нового окна или у новой вкладки что-то записано в свойство opener, или в том случае, если стек навигации страницы содержит менее двух записей.

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

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

  • Если пользователь создаёт новую вкладку, щёлкнув по соответствующей кнопке, воспользовавшись комбинацией клавиш Ctrl + T, щёлкнув по ссылке и нажав при этом Shift, открыв URL из командной оболочки, то у открытой в результате вкладки свойство opener установлено не будет.
  • А если вкладка была открыта с помощью команды open() или через гиперссылку с заданным атрибутом target (не _blank), тогда, по умолчанию, в свойство opener записывается некое значение.
  • У любой ссылки может быть атрибут rel=opener или rel=noopener, указывающий на то, будет ли у новой вкладки установлено свойство opener.
  • При выполнении JavaScript-вызова open() можно, в строке windowFeatures, указать noopener, что приведёт к установке свойства opener новой вкладки в null.

Вышеприведённый список позволяет сделать вывод о том, что и обычный щелчок по ссылке, и использование JavaScript-команды open() может привести к созданию вкладки как с установленным, так и с неустановленным свойством opener. Это может вылиться в серьёзную путаницу: открытие ссылки с зажатой клавишей Shift может привести к открытию вкладки, которая не может сама себя закрыть. А обычный щелчок мыши по такой ссылке приводит к открытию вкладки, которая всегда может закрыть себя сама.

Во-вторых обратите внимание на то, что в начале этого раздела я, говоря о стеке навигации, употребил слово записи, а не объекты Document. В большинстве случаев понятия запись и объект Document эквивалентны, но это не одно и то же. Представьте себе ситуацию, когда в новой вкладке открывается HTML-документ, в верхней части которого содержится нечто вроде оглавления. Пользователь щёлкает по ToC-ссылке, ведущей к разделу страницы #Section3, после чего браузер послушно прокручивает страницу к нужному разделу. Стек навигации теперь содержит две записи, каждая из которых указывает на один и тот же документ. В результате Chromium-браузер блокирует вызов window.close(), а делать этого ему не следует. Этот давний недостаток с выходом Chromium 88 стал заметнее, чем раньше, так как после этого ссылкам с атрибутом target, установленным в _blank, по умолчанию назначается атрибут rel=noopener.

В ветке трекера ошибок Chromium, посвящённой проблеме 1170131, можно видеть, как эту проблему пытаются решить путём подсчёта количества объектов Document в стеке навигации. Но сделать это непросто, так как в настоящее время у процесса, отвечающего за рендеринг страницы, в котором выполняется JavaScript-код, есть доступ только к количеству записей в стеке навигации, но не к их URL.

Chromium: пользовательский опыт


Когда браузер Chrome блокирует команду close(), он выводит в консоль следующее сообщение, которое мы уже обсуждали:

Scripts may close only the windows that were opened by them.

А пользователю, который в консоль обычно не смотрит, об этом никак не сообщается. Это может показаться странным тому, кто щёлкнул по кнопке или по ссылке, предназначенной для закрытия страницы. В недавно появившемся сообщении об ошибке 1170034 предлагается показывать пользователю в такой ситуации диалоговое окно, вроде того, что показывается в Internet Explorer. (Между прочим, это сообщение об ошибке задаёт новый стандарт подготовки подобных сообщений. В нём, в виде, напоминающем комикс, показано, как несчастный пользователь превращается в счастливого в том случае, если в Chromium будет реализован предлагаемый функционал.)

Chromium: любопытные факты об очень редкой ошибке


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

Речь идёт о том, что если установить свойство Chromium On Startup (При запуске) в значение Continue where you left off (Восстановить вкладки предыдущего сеанса), перейти на страницу, которая пытается сама себя закрыть, а после этого закрыть окно браузера, то браузер потом, при каждом запуске, будет сам себя закрывать.

Попасть в такую ситуацию довольно сложно, но в Chrome/Edge 90 это вполне возможно.

Вот как воспроизвести эту ошибку. Посетите страницу https://webdbg.com/test/opener/. Щёлкните по ссылке Page that tries to close itself (Страница, которая пытается себя закрыть). Воспользуйтесь сочетанием клавиш Ctrl+Shift+Delete для очистки истории просмотра (стека навигации). Закройте браузер с помощью кнопки X. Теперь попробуйте запустить браузер. Он будет запускаться, а потом сам собой закрываться.

Safari/WebKit


Код WebKit похож на код Chromium (что неудивительно, учитывая их генеалогию). Исключением является лишь тот факт, что WebKit не уравнивает переходы по noopener-страницам с переходами, инициированными через интерфейс браузера. В результате пользователь, работая в Safari, может перемещаться по множеству страниц с одного сайта, а команда close() при этом будет работоспособна.

Если же вызов close() окажется заблокированным, то в JavaScript-консоль Safari (надёжно скрытую от посторонних глаз) будет выведено сообщение, указывающее на то, что окно закрыть нельзя из-за того, что оно создано не средствами JavaScript:

Can't close the window since it was not opened by JavaScript

Firefox


В браузере Firefox, в отличие от Chromium, та часть спецификации HTML, в которой говорится о только одном Document, реализована корректно. Firefox вызывает функцию IsOnlyTopLevelDocumentInSHistory(), а она вызывает функцию IsEmptyOrHasEntriesForSingleTopLevelPage(), которая проверяет историю сессий. Если там больше одной записи, она уточняет, относятся ли они все к одному и тому же объекту Document. Если это так вызов close() выполняется.

Firefox даёт в наше распоряжение настройку about:config, называемую dom.allow_scripts_to_close_windows, позволяющую переопределить стандартное поведение системы.

Когда Firefox блокирует close() он выводит в консоль сообщение о том, что скрипты не могут закрывать окна, которые были открыты не скриптами:

Scripts may not close windows that were not opened by script.

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

Итоги


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

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

Подробнее..

Перевод Будущее веба станет ли рендеринг в ltcanvasgt заменой DOM?

07.06.2021 20:11:26 | Автор: admin
В последнее время было немало горестных рассуждений о последствиях решения Google использовать HTML-элемент <canvas> для рендеринга всего, что видно на экране при работе с Google Docs. И то, что это многих беспокоит, вполне понятно. Когда-то веб был задуман как система для работы с тщательно структурированной информацией, полной осмысленных метаданных и рассчитанной на совместное её использование многими людьми. Но, вместо этого, тот веб, который мы видим сегодня, представляет собой довольно сложно и запутанно устроенные приложения, которые работают в браузерных песочницах.


Решение Google, которое заключается в том, чтобы перейти от вывода на страницы HTML-элементов к рисованию пикселей на <canvas>, нельзя назвать чем-то таким, чего раньше никто не видел и не пробовал. Другие передовые веб-приложения уже вышли далеко за пределы традиционных схем работы с HTML-элементами. Так, в Google Maps вывод данных на <canvas> используется уже многие годы. В VS Code для отрисовки идеального интерфейса терминала тоже используется <canvas>. А в подающем надежды наборе инструментов Google Flutter, который позволяет создавать кросс-платформенные интерфейсы, в веб-браузере, по умолчанию, используется рендеринг с использованием <canvas>.

Но в этот раз происходящее вызывает несколько иные ощущения. А именно, появляется такое чувство, что рендеринг в <canvas> и другие современные технологии, вроде WebAssembly, увели нас за точку невозврата. Все привыкли к схеме работы, когда страница загружает, в виде обычного текста, JavaScript-код, который выполняется, взаимодействуя с HTML-элементами, видимыми в инструментах разработчика. Сейчас возникает такое впечатление, что это лишь небольшой этап на пути постоянно развивающихся технологий веб-разработки.

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

Что же сейчас происходит?

Подход к разработке веб-приложений с использованием рендеринга в <canvas> будет распространяться


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

Лет 15 назад Google была первопроходцем в деле использования асинхронных JavaScript-вызовов (это потом назвали AJAX). Компания использовала какие-то новейшие для своего времени технологии в Gmail и Google Maps, а позже эти технологии превращались в фундаментальные основы веб-разработки. Теперь, благодаря переходу Google к выводу интерфейсов на <canvas>, этот подход к формированию UI начинает выглядеть совершенно приемлемым в глазах веб-разработчиков нового поколения.

Прямо сейчас перед тем, кто хочет использовать <canvas> для каких-то серьёзных дел, стоят довольно-таки высокие барьеры. В ходе работы над Google Docs специалисты компании сделали новые реализации целой кучи механизмов, которые многие программисты обычно воспринимают как нечто само собой разумеющееся. Это, например, точные инструменты для создания макетов, средства для выделения текста, системы проверки правописания, механизмы оптимизированной перерисовки экранных элементов. В наши дни в мире существует всего несколько компаний, которые способны провести работы такого масштаба лишь ради надежды на возможное улучшение производительности некоего решения.

И самой сложной и интересной задачей в этом проекте является обеспечение доступности сайтов и приложений для людей с ограниченными возможностями. Для того чтобы приложение выполняло бы указания нормативных документов по доступности, оно должно соответствовать определённым требованиям. Это нужно для компаний, которые должны соответствовать требованиям, необходимым для участия в правительственных контрактах, вроде Google, и, кроме того, важно для тех компаний, которые стремятся быть хорошими жителями веба. Версия Google Docs, основанная на <canvas>, несмотря ни на что, должна иметь отличную поддержку средств для чтения с экрана и инструментов для увеличения фрагментов экрана. Она должна поддерживать работу в режиме высокой контрастности, должна содержать механизмы, помогающие работать с ней людям с ограниченными двигательными возможностями. Один из способов реализации этих возможностей заключается в создании невидимой DOM, которая воспроизводила бы реальные данные, выводимые в <canvas>, но существовала бы только ради обеспечения правильной работы ассистивных технологий. И, конечно, две эти модели должны быть идеально синхронизированы друг с другом.

В наши дни нет единого, стандартного решения, которым разработчики могли бы воспользоваться для реализации ассистивных возможностей приложения, которое использует <canvas> для формирования интерфейса. Но по мере того, как этот подход к созданию интерфейсов будет набирать популярность, ситуация изменится в лучшую сторону. И никто не знает о том, насколько быстро произойдут эти изменения. То, что Google использует <canvas> для создания интерфейсов, привлечёт к соответствующим вопросам внимание, этим заинтересуется множество разработчиков, будут развиваться сопутствующие технологии. Потом появятся библиотеки, а ещё через некоторое время, возможно, будут созданы соответствующие стандарты и API. К закону Этвуда можно сделать дополнение, с которым он будет выглядеть так:

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

Семантический веб мёртв


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

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

В соответствии с HTML5 в понятие веб-стандарт входит всё что угодно, по поводу чего пришли к согласию производители разных браузеров. В частности, речь идёт о множестве JavaScript-API, рассчитанных на решение различных практических задач. Из этих API можно, как из кирпичей, строить функционал веб-страниц. Среди этих API можно отметить, например Geolocation, Web Storage, WebSockets, Web Workers. В HTML5, конечно, появились и некоторые новые семантические описательные элементы, но единственным по-настоящему многообещающим новшеством стандарта, связанным с встраиванием информации в веб-страницы, была поддержка микроданных (microdata). Её, правда, довольно быстро из стандарта убрали (в основном из-за того, что Google и Apple не были заинтересованы в реализации этого новшества).


Что скрывается за маской HTML5?

Рендеринг в <canvas> это, очевидно, полная противоположность веб-страницы, содержимое которой оформлено с помощью семантических элементов. Это чёрный ящик, который не даёт браузеру никакой информации о том, что в нём может происходить или происходит.

Из-за этого вся власть оказывается в руках приложений. То, что управляет отдельными пикселями, может абсолютно всё: обходить автоматизированные инструменты, бороться с блокировщиками рекламы, ограничивать браузерные возможности вроде поиска по тексту или его копирования. Это JavaScript-реинкарнация Flash и Silverlight, для работы которой не нужно устанавливать что-то, кроме самого браузера, и при использовании которой можно не думать о вопросах совместимости.

Будущее это WebAssembly и бинарные блобы


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

Если вы заглянете на страницу, работа которой основана на WebAssembly, то окажется, что на ней выполняется предварительно скомпилированный код, уровень которого лишь немногим выше уровня ассемблера. И такой код, определённо, если сравнить его даже с очень сильно минифицированным образцом JS-кода, гораздо сложнее прочесть человеку. WebAssembly используется для запуска в браузере игр, для декодирования генома, на WebAssembly основаны высокоуровневые фреймворки вроде Blazor. Непрозрачные веб-приложения, которые беспокоят пуристов, это мелочь в сравнении с тем, что обещает нам WebAssembly. Речь идёт о выводе производительности веб-приложений, работающих в браузерной песочнице, практически на уровень нативных приложений.

В наши дни WebAssembly-программам для доступа к DOM нужен корявый промежуточный JavaScript-слой. Но вспомним о проекте WebGPU, об оптимизированном последователе теперь заброшенного WebGL. У WebGPU и WebGL есть одна важная общая черта: и тот и другой проекты дают возможность оптимизированного рендеринга на <canvas>. Если объединить это с аппаратным ускорением графики, которое реализовано в браузерах, то получится, что в нашем распоряжении оказался низкоуровневый механизм вывода графики, который можно использовать для создания веб-приложений нового поколения. Или, что больше похоже на правду, нового поколения библиотек и фреймворков, которые лягут в основу новых веб-приложений. Учитывая то, как много внимания к себе привлекает WebAssembly, сложно представить себе будущее веб-разработки, на которое не повлияют WebAssembly и WebGPU.

И почему нет? В конце концов, та модель, которую до сих пор использует Google Docs, это, определённо, не что-то такое, что здравомыслящий веб-разработчик выбрал бы, если бы ему нужно было с нуля написать текстовый редактор. Речь идёт о движке для формирования макетов документов, который основан на движке для формирования HTML-макетов, причём связаны эти движки с помощью абстракции, состоящей из кусков JavaScript-кода. Но Google Docs это реальное приложение, которым, просто потому что это возможно, пользуется невообразимое количество людей. С точки зрения архитектуры программного обеспечения это куча кода, которая работает лишь каким-то чудом, а не аккуратная, образцовая программа.

Итоги


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

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

Как вы относитесь к веб-приложениям, интерфейс которых основан на <canvas>?


Подробнее..

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

23.04.2021 14:18:12 | Автор: admin


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

Если вы давно хотели разобраться в CORS и вас достали постоянные ошибки, добро пожаловать под кат.

Ошибка в консоли вашего браузера



No Access-Control-Allow-Origin header is present on the requested resource.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/

Access to fetch at https://example.com from origin http://localhost:3000 has been blocked by CORS policy.


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

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

Но давайте-ка пойдем к истокам

В начале был первый субресурс


Субресурс это HTML элемент, который требуется вставить в документ или выполнить в контексте этого документа. В 1993 году был введен первый тег <img>. С появлением вебстал более красивым, но заодно и стал сложнее.


Верни мне мой 1993 г.

Как вы поняли, если ваш браузер отображает страницу с <img>, он должен запросить этот тег из источника. Если браузер запрашивает тег из источника, который отличается от получателя посхеме, в полностью определенному имени хоста или порту, то это и есть запрос между различными источниками (cross-origin request).

Источники & cross-origin


Источник идентифицируется следующей тройкой параметров: схема, полностью определенное имя хоста и порт. Например, <http://example.com> и <https://example.com> имеют разные источники: первый использует схему http, а второй https. Вдобавок, портом для http по умолчанию является 80, тогда как для https 443. Следовательно, в данном примере 2 источника отличаются схемой и портом, тогда как хост один и тот же (example.com).

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

Если, к примеру, мы будем сравнивать источник <https://blog.example.com/posts/foo.html> с другими источниками, то мы получим следующие результаты:

URL Результат Причина
https://blog.example.com/posts/bar.html

Тот же Отличается только путь
https://blog.example.com/contact.html

Тот же Отличается только путь
http://blog.example.com/posts/bar.html

Отличен Разные протоколы
https://blog.example.com:8080/posts/bar.html

Отличен Отличается порт (http://personeltest.ru/aways/ порт является по умолчанию 443)
https://example.com/posts/bar.html

Отличен Разный хост

Пример запроса между различными источниками: когда ресурс (то есть, страница) типа <http://example.com/posts/bar.html> попробует отобразить тег из источника <https://example.com> (заметьте, что схема поменялась!).

Слишком много опасностей запроса между различными источниками


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

Когда тег <img> появился во Всемирной Паутине, мы тем самым открыли ящик Пандоры. Некоторое время спустя в Сети появились теги <script>, <frame>, <video>, <audio>, <iframe>, <link>, <form> и так далее. Эти теги могут быть загружены браузером уже после загрузки страницы, поэтому они все могут быть запросами в пределах одного источника и между о разными источниками.

Давайте погрузимся в воображаемый мир, где не существует CORS и веб-браузеры допускают все типы запросов между источниками.

Предположим, у меня есть страница на сайте evil.com с <script>. На первый взгляд это обычная страница, где можно прочесть полезную информацию. Но я специально создал код в теге <script>, который будет отправлять специально созданный запрос по удалению аккаунта (DELETE/account) на сайт банка. Как только вы загрузили страницу, JavaScript запускается и AJAX-запрос попадает в API банка.


Вжух, нет вашего аккаунта

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

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

Давайте рассмотрим другой, не такой зловещий сценарий.

Мне нужно опознать людей, которые работают на Awesome Corp, внутренний сайт этой компании находится по адресу intra.awesome-corp.com. На моем сайте, dangerous.com, у меня есть <img src="http://personeltest.ru/aways/intra.awesome-corp.com/avatars/john-doe.png">.

У пользователей, у которых нет активного сеансас intra.awesome-corp.com, аватарка не отобразится, так как это приведет к ошибке. Однако если вы совершили вход во внутреннюю сеть Awesome Corp., как только вы откроете мой dangerous.com сайт, я буду знать, что у вас там есть аккаунт.

Это означает, что я смогу прощупать определенную информацию о вас. Конечно, для меня будет сложнее устроить атаку, но знание о том, что у вас есть доступ к Awesome Corp., является потенциальным направлением для атаки.


Утечка информации к 3-им лицам

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

Но до зарождения CORS существовала политика одинакового источника.

Политика одинакового источника


Политика одинакового источника предотвращает cross-origin атаки, блокируя доступ для прочтения загружаемых ресурсов из другого источника. Такая политика все еще разрешает нескольким тегам вроде <img> загружать ресурсы из других источников.

Политика одинакового источника введена Netscape Navigator 2.02 в 1995 году, изначально для защищенного cross-origin доступа к Объектной модели документа (DOM).

Даже несмотря на то, что внедрение политики одинакового источника не требует придерживаться определенного порядка действий, все современные браузеры следуют этой политике в той или иной форме. Принципы политики описаны в запросе на спецификацию RFC6454 Инженерного совета интернета (Internet Engineering Task Force).

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

Теги Cross-origin Замечание
<iframe>

Встраивание разрешено Зависит от X-Frame-Options
<link>

Встраивание разрешено Надлежащий Content-Type может быть затребован
<form>

Ввод разрешен Распространены cross-origin записи
<img>

Встраивание разрешено Принятие через разные источники через JavaScript и его загрузка в <canvas> запрещены
<audio> / <video>
Встраивание разрешено
<script>

Встраивание разрешено Доступ к определенным API может быть запрещен

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

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

Врываемся в CORS


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

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

  1. Запись из разных источников
  2. Вставка из разных источников
  3. Считывание из разных источников

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

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

Вставки из разных источников это теги, загружаемые через <script>, <link>, <img>, <video>, <audio>, <object>, <embed>, <iframe> и т.п. Все они разрешены по умолчанию. <iframe> выделяется на их фоне, так как он используется для загрузки другой страницы внутри фрейма. Его обрамление в зависимости от источника может регулироваться посредством использования заголовка X-Frame-options.

Что касается <img> и других вставных тегов, то они устроены так, что сами инициируют запросы из разных источников cross-origin запроса. Именно поэтому в CORS существует различие между вставкой из разных источников и считыванием из разных источников.

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

Если ваш браузер обновлён, то он уже дополнен всей этой эвристикой.

Запись из разных источников


Операции записи из разных источников порой очень проблематичны. Давайте рассмотрим пример и посмотрим на CORS в деле.

Во-первых, у нас будет простой Crystal (с использованием Kemal) HTTP сервер:

require "kemal"port = ENV["PORT"].to_i || 4000get "/" do  "Hello world!"endget "/greet" do  "Hey!"endpost "/greet" do |env|  name = env.params.json["name"].as(String)  "Hello, #{name}!"endKemal.config.port = portKemal.run

Он просто берет запрос по ссылке /greet с name в теле запроса и возвращает Hello #{name}!. Чтобы запустить это маленький Crystal сервер мы можем написать

$ crystal run server.cr


Так запускается сервер, который будет слушать localhost:4000. Если мы откроем localhost:4000 в нашем браузере, то появиться страница с тривиальным Hello World.


Hello world!

Теперь, когда мы знаем, что наш сервер работает, давайте из консоли нашего браузера сделаем запрос POST /greet на сервер, слушающий localhost:4000,. Мы можем это сделать, используя fetch:

fetch(  'http://localhost:4000/greet',  {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify({ name: 'Ilija'})  }).then(resp => resp.text()).then(console.log)


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


Привет!

Это был POST запрос, но не из разных источников. Мы отправили запрос из браузера, где была отображена страница с адреса http://localhost:4000 (источник), к тому же источнику.

Теперь, давайте попробуем повторить такой же запрос но с различными источниками. Мы откроем https://google.com и попробуем тот же запрос с той же вкладки нашего браузера:


Привет, CORS!

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

В первом примере, где мы отправили запрос в http://localhost:4000/greet из вкладки, которая отображала http://localhost:4000, наш браузер смотрит на запрос и разрешает, так как ему кажется, что наш сайт запрашивает наш сервер (что есть отлично). Но во втором примере где наш сайт (http://personeltest.ru/aways/google.com) хочет написать на http://localhost:4000, тогда наш браузер отмечает этот запрос и не разрешает ему пройти.

Предварительные запросы


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


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

Интересно заметить, то у первого запроса в HTTP фигурирует метод OPTIONS, в то время как у второго методPOST.

Если внимательно посмотреть на запрос OPTIONS, то мы увидим, что этот запрос отправлен нашим браузером до отправления запроса POST.


Смотрим запрос OPTIONS

Интересно, что несмотря на то, что статус запроса OPTIONS был HTTP 200, он был все же отмечен красным в списке запросов. Почему?

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

  • Запрос использует методы отличные от GET, POST, или HEAD
  • Запрос включает заголовки отличные от Accept, Accept-Language или Content-Language
  • Запрос имеет значение заголовка Content-Type отличное от application/x-www-form-urlencoded, multipart/form-data, или text/plain.

Следовательно, в примере выше, несмотря на то, что мы отправили запрос POST, браузер считает наш запрос сложным из-за заголовка Content-Type: application/json.

Если бы мы изменили наш сервер так, чтобы он обрабатывал контент text/plain (вместо JSON), мы бы могли обойтись без предварительных запросов:

require "kemal"get "/" do  "Hello world!"endget "/greet" do  "Hey!"endpost "/greet" do |env|  body = env.request.body  name = "there"  name = body.gets.as(String) if !body.nil?  "Hello, #{name}!"endKemal.config.port = 4000Kemal.run


Теперь, когда мы можем отправить наш запрос с заголовком Content-type: text/plain:

fetch(  'http://localhost:4000/greet',  {    method: 'POST',    headers: {      'Content-Type': 'text/plain'    },    body: 'Ilija'  }).then(resp => resp.text()).then(console.log)


Теперь, пока предварительный запрос не будет отправлен, CORS политика браузера будет постоянно блокировать запрос:


CORS стоит насмерть

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


Запрос прошел

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

CORS политика вашего браузера считает, что это фактически считывание из разных источников, так как, несмотря на то, что запрос был отправлен как POST, Content-type значение заголовка по сути приравнивает его кGET. Считывания из разных источниковзаблокированы по умолчанию, следовательно мы видим заблокированный запрос в нашей панели Network.

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

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

  • Access-Control-Allow-Methods, который указывает на то, какие методы поддерживаются URL-ом ответа в контексте CORS протокола.
  • Access-Control-Allow-Headers, который указывает, на то, какие заголовки поддерживаются URL-ом ответа в контексте CORS протокола.
  • Access-Control-Max-Age, который указывает число секунд (5 по умолчанию) и это значение соответствует периоду, накоторый предоставляемая заголовками Access-Control-Allow-Methods и Access-Control-Allow-Headers информация может быть кэширована.

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

etch(  'http://localhost:4000/greet',  {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify({ name: 'Ilija'})  }).then(resp => resp.text()).then(console.log


Мы уже выяснили, что когда мы отправляем запрос, наш браузер будет сверяться с сервером, можно ли выполнить запрос с данными из разных источников. Чтобы обеспечить работоспособность в среде с разными источниками, мы должны сначала добавить конечную точку OPTIONS/greet к нашему серверу. В заголовке ответа новая конечная точка должна сообщить браузеру, что запрос на POST /greet с заголовком Content-type: application/json из источника https://www.google.com может быть принят.

Мы это сделаем, используя заголовки Access-Control-Allow-*:

options "/greet" do |env|  # Allow `POST /greet`...  env.response.headers["Access-Control-Allow-Methods"] = "POST"  # ...with `Content-type` header in the request...  env.response.headers["Access-Control-Allow-Headers"] = "Content-type"  # ...from https://www.google.com origin.  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"end


Если мы запустим сервер и отправим запрос, то


Все еще заблокирован?

Наш запрос остается заблокированным. Даже несмотря на то что наша конечная точка OPTIONS/greet в самом деле разрешила запрос, мы пока еще видим сообщение об ошибке. В нашей панели Network происходит кое-что интересное:


OPTIONS стал зеленым!

Запрос в конечную точку OPTIONS/greet прошел успешно! Однако запрос POST /greet все еще терпит неудачу. Если взглянуть на внутрь запроса POST /greet мы увидим знакомую картинку:


POST тоже стал зеленым?

На самом деле запрос удался: сервер вернул HTTP 200. Предварительный запрос заработал. Браузер совершил POST-запрос вместо того, чтобы его заблокировать. Однако ответ на запрос POST не содержит никаких CORS заголовков, так что даже несмотря на то, что браузер сделал запрос, он заблокировал любой ответ.

Чтобы разрешить браузеру обработать ответ из запроса POST /greet, нам также нужно добавить заголовок CORS к конечной точке POST:

post "/greet" do |env|  name = env.params.json["name"].as(String)  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"  "Hello, #{name}!"end


Добавляя к заголовку Access-Control-Allow-Origin заголовок ответа, мы сообщаем браузеру, что вкладка с открытой https://www.google.com имеет доступ к содержимому ответа.

Если попытаться еще разок, то


POST работает!

Мы увидим, что POST /greet получил для нас ответ без каких-либо ошибок. Если посмотреть на панели Network, то мы увидим, что оба запроса зеленые.


OPTIONS & POST в деле!

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

Считывание из разных источников


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

Скажем, в нашем Crystal сервере есть действие GET /greet.

get "/greet" do  "Hey!"end

Из нашей вкладки, что передала www.google.com если мы попробуем запросить эндпоинт GET /greet, то CORS нас заблокирует:


CORS блокирует

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



На самом деле, как и прежде, наш браузер разрешил запрос: мы получили код состоянияHTTP 200. Однако он не показал нашу открытую страницу/вкладку в ответ на этот запрос. Еще раз, в данном случае CORS не заблокировал запрос, он заблокировал ответ.

Так же, как и в случае с записью из разных источников, мы можемосвободить CORS и обеспечить считывание из разных источников, добавляя заголовок Access-Control-Allow-Origin:

get "/greet" do |env|  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"  "Hey!"end


Когда браузер получит ответ от сервера, он проверит заголовок Access-Control-Allow-Origin и исходя изего значения решит, сможет ли он позволить странице прочитать ответ. Учитывая, что в данном случае значением является https://www.google.com, итог будет успешным:


Успешный запрос GET между разными источниками

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

Тонкая настройка CORS


Как мы уже видели в предыдущих примерах, чтобы смягчить политику CORS нашего сайта мы можем присвоить опцию Access-Control-Allow-Origin для нашего действия /greet значению https://www.google.com:

post "/greet" do |env|  body = env.request.body  name = "there"  name = body.gets.as(String) if !body.nil?  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"  "Hello, #{name}!"end

Это разрешит нашему источнику https://www.google.com запросить наш сервер, и наш браузер свободно сделает это. Имея Access-Control-Allow-Origin мы можем попробовать снова выполнить вызов fetch:


Сработало!

И это работает! С новой политикой CORS мы можем вызвать действие /greet из нашей вкладки, в которой загружена страница https://www.google.com. Или, мы могли бы присвоить заголовку значение *, которое сообщило бы браузеру, что сервер может быть вызван из любого источника.

Устанавливая такую конфигурацию, нужно тщательно взвесить все риски. Тем не менее, вставка заголовков с нестрогими требованиями к CORS почти всегда безопасна. Есть эмпирическое правило: если вы открываете URL в приватной вкладке и вас устраивает информация, которая там отображается, то вы можете установить разрешающую CORS политику (*) для данного URL.

Другой способ настройки CORS на нашем сайте это использование заголовка запроса Access-Control-Allow-Credentials. Access-Control-Allow-Credentials запрашивает браузер, показывать ли ответ JavaScript коду клиентской части, когда в качестве режима учетных данных запроса используется include.

Учетный режим запросов исходит из внедрения Fetch API, который в свою очередь корнями идет к объектам XMLHttpRequest:

var client = new XMLHttpRequest()client.open("GET", "./")client.withCredentials = true


С вводом fetch, метод withCredentials превратился в опциональный аргумент fetch запроса:

fetch("./", { credentials: "include" }).then(/* ... */)

Доступными опциями для обработки учетных данныхявляются omit, same-origin и include. Доступны разные режимы, так что программист может настроить отправляемый запрос, пока ответ от сервера сообщает браузеру как вести себя, когда учетные данные отправлены с запросом (через заголовок Access-Control-Allow-Credential).

Спецификация Fetch API содержит подробно расписанный и детально разобранный функционал взаимодействия CORS и Web API fetch, а также характеризует механизмы безопасности, используемые браузерами.

Несколько правильных решений


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

Свободный доступ для всех


Как правило, это тот случай, когда у вас есть сайт с открытым контентом, не ограниченный платным доступом или сайт, требующий аутентификацию или авторизацию. Тогда вы должны установить Access-Control-Allow-Origin: * для ресурсов сайта.

Значение * хорошо подойдетв случаях, когда

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

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


Сверхупрощение VPN

Теперь, когда взломщик захостит dangerous.com, который содержит ссылку файла с VPN, то (в теории) он может создать скрипт в их сайте, который сможет иметь доступ к этому файлу:


Утечка файла

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

Всё в семью


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

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

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



В данных случаях мы хотим, чтобы наш API установил заголовок Access-Control-Allow-Origin к URL нашего сайта. Это обеспечит нас тем, что браузеры никогда не отправят запросы нашему API с других страниц.

Если пользователи или другие сайты попробуют взломать данные нашего аналитического API, то набор заголовков Access-Control-Allow-Origin, установленный на нашем API, не пропустит запрос.



Null источник


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

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

Пропускай куки, если возможно


Как мы уже видели с Access-Control-Allow-Credentials, куки не включены по умолчанию. Чтобы разрешить отправку куки с разных источников, нужно просто вернуть Access-Control-Allow-Credentials: true. Этот заголовок сообщит браузерам, что им разрешается пересылать удостоверяющие данные (то есть куки) в запросах между разными источниками.

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

Куки между разными источниками полезнее всего в ситуациях, когда вы точно знаете какие именно клиенты будут иметь доступ к вашему серверу. Именно поэтому семантика CORS не позволяет нам установить Access-Control-Allow-Origin: *, когда удостоверяющие данные между разными источниками разрешены.

В то время как комбинация из Access-Control-Allow-Origin: * и Access-Control-Allow-Credentials: true технически разрешается, она является анти-паттерном и ее следует безусловно избегать.

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

Дополнительная литература


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




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

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Что такое рендеринг на стороне сервера и нужен ли он мне?

04.01.2021 10:13:21 | Автор: admin
Привет, Хабр!

В новом году начнем общение с вами с затравочной статьи о серверном рендеринге (server-side rendering). В случае вашей заинтересованности возможна более свежая публикация о Nuxt.js и дальнейшая издательская работа в этом направлении


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

До пришествия приложений, полностью генерируемых на JS в браузере, HTML-разметка выдавалась клиенту в ответ на HTTP-вызов. Это могло происходить путем возврата статического HTML-файла с контентом, либо путем обработки отклика при помощи какого-либо серверного языка (PHP, Python или Java), причем, более динамическим образом.

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

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

<!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8">    <link rel="shortcut icon" href="http://personeltest.ru/aways/habr.com/favicon.ico">    <title>React App</title>  </head>  <body>    <div id="root"></div>    <script src="http://personeltest.ru/aways/habr.com/app.js"></script>  </body></html>


Выбрав этот отклик, наш браузер также выберет пакет app.js, содержащий наше приложение, и через одну-две секунды полностью отобразит страницу.

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

Почему это проблема?



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

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


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

Ладно, но в демографическом отношении моя целевая аудитория точно не относится ни к одной из этих групп, так стоит ли мне волноваться?

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

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

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

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

Как решить эту проблему



Есть несколько способов ее решения.

A Попробуйте оставить все ключевые страницы вашего сайта статическими



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

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

B Генерируйте части вашего приложения в виде HTML-страниц в процессе сборки



В проект можно добавить такие библиотеки как react-snapshot; они используются для генерации HTML-копий страниц вашего приложения и для сохранения их в специально предназначенном каталоге. Затем этот каталог развертывается наряду с пакетом JS. Таким образом, HTML будет подаваться с сервера вместе с откликом, и ваш сайт увидят в том числе те пользователи, у которых отключен JavaScript, а также заметят поисковики и т.д.

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

"build": "webpack && react-snapshot --build-dir static"

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

C Создать на JS приложение, использующее серверный рендеринг



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

Два наиболее популярных решения, обеспечивающих серверный рендеринг для React:



Создайте собственную реализацию SSR



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

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

Давайте создадим входную точку:

// index.jsimport React from 'react';import { render } from 'react-dom';import App from './App.js';render(<App />, document.getElementById('root'));


И компонент-приложение (App):

// App.jsimport React from 'react';const App = () => {  return (    <div>      Welcome to SSR powered React application!    </div>  );}


А также оболочку, чтобы загрузить наше приложение:

// index.html<!doctype html><html>  <head>    <meta charset="utf-8" />  </head>  <body>    <div id="root"></div>    <script src="http://personeltest.ru/aways/habr.com/bundle.js"></script>  </body></html>


Как видите, приложение получилось довольно простым. В рамках этой статьи мы не будем пошагово разбирать все шаги, необходимые для генерации правильной сборки webpack+babel.
Если запустить приложение в его текущем состоянии, то на экране появится сообщение-приветствие. Просмотрев исходный код, вы увидите содержимое файла index.html, но приветственного сообщения там не будет. Для решения этой проблемы добавим серверный рендеринг. Для начала добавим 3 пакета:

yarn add express pug babel-node --save-dev

Express это мощный веб-сервер для node, pug движок-шаблонизатор, который можно использовать с express, а babel-node это обертка для node, обеспечивает транспиляцию на лету.

Сначала скопируем наш файл index.html и сохраним его как index.pug:

// index.pug
<!doctype html>
<html>
<head>
<meta charset=utf-8 />
</head>
<body>
<div id=root>!{app}</div>
<script src=bundle.js></script>
</body>
</html>

Как видите, файл практически не изменился, не считая того, что теперь в HTML вставлено !{app}. Это переменная pug, которая впоследствии будет заменена реальным HTML.

Создадим наш сервер:

// server.jsimport React from 'react';import { renderToString } from 'react-dom/server';import express from 'express';import path from 'path';import App from './src/App';const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));app.get('*', (req, res) => {  const html = renderToString(    <App />  );  res.render(path.join(__dirname, 'src/index.pug'), {    app: html  });});app.listen(3000, () => console.log('listening on port 3000'));


Разберем этот файл по порядку.

import { renderToString } from 'react-dom/server';

Библиотека react-dom содержит отдельный именованный экспорт renderToString, работающий подобно известному нам render, но отображает не DOM, а HTML в виде строки.

const app = express();app.set('view engine', 'pug');app.use('/', express.static(path.join(__dirname, 'dist')));


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

В последней строке мы приказываем express искать файл в каталоге dist, и, если запрос (напр. /bundle.js) совпадает с файлом, присутствующем в этом каталоге, то выдать его в ответ.

app.get('*', (req, res) => {});


Теперь мы приказываем express добавить обработчик на каждый несовпавший URL в том числе, на наш несуществующий файл index.html (как вы помните, мы переименовали его в index.pug, и его нет в каталоге dist).

const html = renderToString(  <App />);


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

res.render(path.join(__dirname, 'src/index.pug'), {  app: html});


Теперь, когда у нас есть отображенный HTML, мы приказываем express отобразить в ответ файл index.pug и заменить переменную app тем HTML, что мы получили.

app.listen(3000, () => console.log('listening on port 3000'));

Наконец, мы обеспечиваем запуск сервера и настраиваем его так, чтобы он слушал порт 3000.
Теперь нам осталось всего лишь добавить нужный скрипт в package.json:

"scripts": {  "server": "babel-node server.js"}


Теперь, вызвав yarn run server, мы должны получить подтверждение, что сервер действительно работает. Переходим в браузере по адресу localhost:3000, где мы, опять же, должны увидеть наше приложение. Если мы просмотрим исходный код на данном этапе, то увидим:

<!doctype html><html>  <head>    <meta charset="utf-8" />  </head>  <body>    <div id="root">div data-reactroot="">Welcome to SSR powered React application!</div></div>    <script src="bundle.js"></script>  </body></html>


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

Зачем же нам по-прежнему нужен bundle.js?



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

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

О чем необходимо помнить



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

  • Любое состояние, сгенерированное на стороне сервера, не будет передаваться в состояние клиентского приложения. Это означает, что, если ваша серверная часть выберет некоторые данные и использует их для отображения HTML, то эти данные не попадут в this.state, которое увидит браузер
  • componentDidMount не вызывается на сервере это означает, что не будут вызываться никакие операции по выборке данных, которые вы привыкли там размещать. В принципе, это хорошо, поскольку вы должны предоставлять нужные вам данные в виде пропсов. Помните, что отображение нужно отложить (вызвав res.render) до тех пор, пока данные не будут выбраны. Из-за этого посетители могут заметить некоторые задержки в работе сайта
  • если вы собираетесь использовать роутер react (напр. @reach/router или react-router) то должны убедиться, что приложению передается правильный URL, когда оно отображается на сервере. Обязательно почитайте об этом в документации!
Подробнее..

Перевод 5 HTML-трюков, о которых никто не говорит

15.03.2021 00:06:29 | Автор: admin

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

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

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

1. Ленивая загрузка изображения

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

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

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

Этого легко добиться с помощью обычного HTML.

Всё, что вам нужно сделать, это добавить свойство loading=lazy у тега img.

После добавления свойства ваш элемент должен выглядеть примерно так:

<img src="image.png" loading="lazy" alt="" width="200" height="200">

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

2. Автокомплит

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

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

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

Однако HTML также позволяет отображать набор предопределённых вариантов, используя тег <datalist>.

Помните, что атрибут ID этого тега должен совпадать с атрибутом list тега input.

<label for="country">Choose your country from the list:</label><input list="countries" name="country" id="country"><datalist id="countries">  <option value="UK">  <option value="Germany">  <option value="USA">  <option value="Japan">  <option value="India"></datalist>

3. Picture

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

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

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

К счастью, HTML даёт разработчикам возможность довольно легко исправить это, используя тег <picture>, позволяющий добавлять несколько изображений, соответствующих разной ширине.

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

<picture>  <source media="(min-width:768px)" srcset="med_flag.jpg">  <source media="(min-width:495px)" srcset="small_flower.jpg">  <img src="high_flag.jpg" alt="Flags" style="width:auto;"></picture>

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

Этот тег очень похож на теги <audio> и <video>.

4. Базовый URL

Это один из моих любимых тегов при создании карты сайта.

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

Например, если я хочу указать URL-адрес на Twitter Илона Маска и Билла Гейтса, начало URL-адреса (домена) будет таким же, а то, что следует за ним, будет их соответствующими идентификаторами.

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

Однако в HTML есть тег <base>, который позволяет вам установить базовый URL-адрес, как показано ниже:

<head><base href="http://personeltest.ru/aways/www.twitter.com/" target="_blank"></head><body><img src="elonmusk" alt="Elon Musk"><a href="BillGates">Bill Gate</a></body>

Приведённый выше код сгенерирует изображение с ссылкой на https://www.twitter.com/elonmusk и ссылочный тег, перенаправляющий на https://www.twitter.com/billgates".

Тег <base> должен иметь либо href, либо target-атрибуты.

5. Обновление документа

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

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

Это поведение встроено в HTML, и вы можете использовать его с помощью тега <meta> и установки http-Equiv=refresh в него:

<meta http-Equiv="refresh" content="4; URL='https://google.com'/>

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

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

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

Заключение

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

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

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

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

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

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

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

Другие профессии и курсы
Подробнее..

Навыки для виртуальных ассистентов на веб-технологиях

28.12.2020 14:06:07 | Автор: admin

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

Canvas App стандартное веб-приложение в привычном понимании, которое запускается и работает внутри WebView, но есть свои особенности.

Как это работает по шагам:

  1. Пользователь произносит ключевую фразу, например Салют, какие у меня задачи на сегодня.

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

  3. В БД зарегистрированных смартапов находится тот, которому соответствует активационная фраза. Регистрация происходит через SmartApp Studio и доступна всем разработчикам без исключения.

  4. Во время регистрации смартапа в SmartApp Studio разработчик указывает два эндпоинта: один для веб-приложения, второй для сценарного бэкенда. Именно их достанет из БД NLP-платформа, когда найдет соответствующий смартап.

  5. В эндпоинт сценарного бэкенда будет отправлено сообщение с распознанной активационной фразой. Формат сообщений подробно описан в документации SmartApp API.

  6. Эндпоинт веб-приложения будет указан для загрузки в WebView.

  7. Ответ от сценарного бэкенда придёт в веб-приложение в качестве JS-события, подписавшись на которое, можно управлять веб-приложением.

Упрощенная схема для наглядностиУпрощенная схема для наглядности

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

В SmartApp Graph/IDE, той самой онлайн-среде, в качестве источника можно указать git-репозиторий, чем мы и воспользуемся, чтобы получить эндпоинт до сценарного бэкенда. Далее его надо указать при регистрации нашего смартапа в SmartApp Studio. В качестве эндпоинта веб-приложения укажем любой известный веб-ресурс, например, sberdevices.ru. Позже поменяем на URL нашего веб-приложения.

Шаблон проекта

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

Итак, что мы хотим от приложения:

  • добавлять задачи;

  • выполнять задачи;

  • удалять задачи;

  • и все это голосом, но не сразу.

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

> npx create-react-app todo-canvas-app

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

Код формы
export const App: FC = memo(() => {  const [note, setNote] = useState("");  return (    <main className="container">      <form        onSubmit={(event) => {          event.preventDefault();          setNote("");        }}      >        <input          className="add-note"          type="text"          value={note}          onChange={({ target: { value } }) => setNote(value)}        />      </form>      <ul className="notes">        {appState.notes.map((note, index) => (          <li className="note" key={note.id}>            <span>              <span style={{ fontWeight: "bold" }}>{index + 1}. </span>              <span                style={{                  textDecorationLine: note.completed ? "line-through" : "none",                }}              >                {note.title}              </span>            </span>            <input              className="done-note"              type="checkbox"              checked={note.completed}            />          </li>        ))}      </ul>    </main>  );});

Дальше нам надо сделать базовую логику нашего приложения. Пользоваться будем стандартными средствами React, используя useReducer.

Код редьюсера
const reducer = (state, action) => {  switch (action.type) {    case "add_note":      return {        ...state,        notes: [          ...state.notes,          {            id: Math.random().toString(36).substring(7),            title: action.note,            completed: false,          },        ],      };    case "done_note":      return {        ...state,        notes: state.notes.map((note) =>          note.id === action.id ? { ...note, completed: !note.completed } : note        ),      };    case "delete_note":      return {        ...state,        notes: state.notes.filter(({ id }) => id !== action.id),      };    default:      throw new Error();  }};

Далее будем диспатчить экшены их обработчиков на форме.

Код подключения
export const App: FC = memo(() => {  const [appState, dispatch] = useReducer(reducer, { notes: [] });  //...  return (    <main className="container">      <form        onSubmit={(event) => {          event.preventDefault();          dispatch({ type: "add_note", note });          setNote("");        }}      >        <input          className="add-note"          type="text"          placeholder="Add Note"          value={note}          onChange={({ target: { value } }) => setNote(value)}          required          autoFocus        />      </form>      <ul className="notes">        {appState.notes.map((note, index) => (          <li className="note" key={note.id}>            <span>              <span style={{ fontWeight: "bold" }}>{index + 1}. </span>              <span                style={{                  textDecorationLine: note.completed ? "line-through" : "none",                }}              >                {note.title}              </span>            </span>            <input              className="done-note"              type="checkbox"              checked={note.completed}              onChange={() => dispatch({ type: "done_note", id: note.id })}            />          </li>        ))}      </ul>    </main>  );});

Запускаем и проверяем.

npm start

Работа с голосом

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

npm i @sberdevices/assistant-client

В момент открытия WebView платформа инжектит JS API для взаимодействия с ассистентом. Это биндиги до нативных методов платформы. Assistant Client обёртка, которая в дев-режиме позволяет отлаживать взаимодействие с ассистентом в браузере, а в продакшене предоставляет удобный для веб-приложений API.

Идём в app.js и там же, где наш основной редюсер, создаем инстанс Assistant Client.

const initializeAssistant = () => {  if (process.env.NODE_ENV === "development") {    return createSmartappDebugger({      token: process.env.REACT_APP_TOKEN ?? "",      initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`,    });  }  return createAssistant();};

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

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

Ассистент присылает структурированные команды в формате JSON. Полное описание формата можно найти в документации Assistant Client на GitHub.

interface AssistantSmartAppCommand {  // Тип команды  type: "smart_app_data";  // Любые данные, которые нужны смартапу  smart_app_data: Record<string, any>;  sdkMeta: {    requestId: string;  };}

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

export const App: FC = memo(() => {  const [appState, dispatch] = useReducer(reducer, { notes: [] });  const [note, setNote] = useState("");  const assistantRef = useRef();  useEffect(() => {    assistantRef.current = initializeAssistant();    assistantRef.current.on("data", ({ action }) => {      if (action) {        dispatch(action);      }    });  }, []);    // ...

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

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

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

Дополним код инициализации Asisstant Client.

const initializeAssistant = (getState) => {  if (process.env.NODE_ENV === "development") {    return createSmartappDebugger({      token: process.env.REACT_APP_TOKEN ?? "",      initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`,      getState,    });  }  return createAssistant({ getState });};

И передадим стейт в обработку ассистенту. Формат стейта также описан в документации Asisstant Client.

export const App: FC = memo(() => {  // ...  const assistantStateRef = useRef<AssistantAppState>();// ...  useEffect(() => {    assistantRef.current = initializeAssistant(() => assistantStateRef.current);    // ...  }, []);  useEffect(() => {    assistantStateRef.current = {      item_selector: {        items: appState.notes.map(({ id, title }, index) => ({          number: index + 1,          id,          title,        })),      },    };  }, [appState]);  // ...

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

Тудух может скопиться достаточное количество, чтобы они не влезли в экран. Само собой, мы хотим уметь скроллить экран, чтобы иметь возможность прочитать всё, что скопилось. На устройствах, где нет тач-интерфейса, например, на SberBox, мы можем скроллить пультом ДУ или голосом. Нажатия кнопок на пульте превращаются в события нажатий на стрелки клавиатуры на window, но что делать с голосом?

Голосовые паттерны навигации встроены в NLP-платформу, и разработчику сценария ничего не надо делать самому. А для разработчика веб-приложения достаточно подписаться на специальный тип команд, приходящих от ассистента через Assistant Client. Все вариации навигационных фраз будут кастится в конечное число навигационных команд. Их всего пять: UP, DOWN, LEFT, RIGHT, BACK.

assistant.on('data', (command) => {    if (command.navigation) {        switch(command.navigation.command) {            case 'UP':                window.scrollTo(0, 0);                break;            case 'DOWN':                window.scrollTo(0, 1000);                break;        }    }});

Перезапускаем наше приложение и пробуем после нажатия на лавашар сказать: Напомни купить коту корм. И вуаля!

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

ngrok http 3000

Полученный URL с https указываем в SmartApp Studio, сохраняем черновик и говорим ассистенту: Сбер, какие у меня задачи на сегодня?. Это cработает, если вы залогинены под одним и тем же SberID на устройстве и в SmartApp Studio. Черновики по-умолчанию доступны к запуску на устройствах разработчика.

Вместо эпилога

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

Этот короткое интро скорее, обзор возможностей на искусственном примере. Как сделать смартап с компонентами, оплатой, автотестами, обязательно расскажем в следующих статьях. Спасибо за внимание, ещё увидимся!

По всем вопросам разработки смартапов можно обращаться в сообщество разработчиков SmartMarket в телеграмме.

Подробнее..

Перевод 6 лучших практик React в 2021 году

11.03.2021 16:14:30 | Автор: admin

Для будущих студентов курса "React.js Developer" подготовили перевод материала.

Также предлагаем всем желающим посмотреть открытый вебинар на тему ReactJS: быстрый старт. Сильные и слабые стороны.


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

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

  1. Используйте event.target.name для обработчиков событий.

  2. Как избежать ручной привязки обработчиков событий к this?

  3. Используйте React hooks для обновления состояния.

  4. Кэширование затратных операций с useMemo.

  5. Разделение функций на отдельные элементы для улучшения качества кода.

  6. Как создавать собственные хуки в React?

#1: Используйте имя обработчика события

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

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

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

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

export default class App extends React.Component {    constructor(props) {        super(props);        this.state = {            item1: "",            item2: "",            items: "",            errorMsg: ""        };        this.onFirstInputChange = this.onFirstInputChange.bind(this);        this.onSecondInputChange = this.onSecondInputChange.bind(this);    }    onFirstInputChange(event) {        const value = event.target.value;        this.setState({            item1: value        });    }    onSecondInputChange(event) {        const value = event.target.value;        this.setState({            item2: value        });    }    render() {        return (            <div>                <div className="input-section">                    {this.state.errorMsg && (                        <p className="error-msg">{this.state.errorMsg}</p>                    )}                    <input                        type="text"                        name="item1"                        placeholder="Enter text"                        value={this.state.item1}                        onChange={this.onFirstInputChange}                    />                    <input                        type="text"                        name="item2"                        placeholder="Enter more text"                        value={this.state.item2}                        onChange={this.onSecondInputChange}                    />                </div>            </div>      );    }}

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

Давайте рассмотрим эту ситуацию по-другому, используя поле имени. Мы можем получить доступ к этому значению через свойство event.target.name. Теперь создадим одну функцию, которая сможет обрабатывать оба события одновременно. Таким образом, мы можем удалить обе функции onFirstInputChange и onSecondInputChange.

onInputChange = (event) => {  const name = event.target.name;  const value = event.target.value;  this.setState({    [name]: value  });};

Полегче, да? Конечно, вам часто требуется дополнительная проверка данных, которые вы сохраняете в своем состоянии (state). Вы можете использовать утверждение switch для добавления собственных правил валидации для каждого введенного значения.

#2: Избегайте ручной привязки (binding) this

Скорее всего, вы знаете, что React не сохраняет привязку this при прикреплении обработчика к событию onClick или onChange. Поэтому мы должны связать это вручную. Почему мы связываем *this*? Мы хотим связать this обработчика события (event handler) с экземпляром компонента (component instance), чтобы не потерять его контекст, когда мы передадим его в качестве обратного вызова.

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

class Button extends Component {  constructor(props) {    super(props);    this.state = { clicked: false };    this.handleClick = this.handleClick.bind(this);  }    handleClick() {    this.props.setState({ clicked: true });  }    render() {    return <button onClick={this.handleClick}>Click me!</button>;  );}

Однако привязка больше не требуется, так как команда CLI createe-react-app использует @babel/babel-plugin-transform-class-properties плагин версии >=7 и babel/plugin-proposal class-properties плагин версии <7.

Примечание: Вы должны изменить синтаксис обработчика событий (event handler) на синтаксис функции стрелок (arrow function).

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

class Button extends Component {  constructor(props) {    super(props);    this.state = { clicked: false };  }    handleClick = () => this.setState({clicked: true });  render() {    return <button onClick={this.handleClick}>Click me!</button>;  );}

Это так просто! Вам не нужно беспокоиться о привязке функций в вашем конструкторе.

#3: Используйте React hooks чтобы обновить ваше состояние

Начиная с версии 16.8.0, теперь можно использовать методы состояния и жизненного цикла (state and lifecycle methods) внутри функциональных компонентов с помощью React Hooks. Другими словами, мы можем писать более читаемый код, который также намного проще в обслуживании.

Для этого мы будем использовать useState hook. Для тех, кто не знает, что такое hook и зачем его использовать вот краткое определение из React documentation..

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

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

Во-первых, давайте посмотрим, как мы обновляем состояние с помощью setState hook.

this.setState({    errorMsg: "",    items: [item1, item2]});

А теперь давайте воспользуемся useState hook. Нам нужно импортировать этот hook из react библиотеки. Теперь мы можем объявить новые переменные состояния и передать им начальное значение. Мы будем использовать деструктуризацию,чтобы извлечь переменную для получения значения и еще одну для установки значения (это будет функция).Рассмотрим, как это можно сделать в приведенном выше примере.

import React, { useState } from "react";const App = () => {  const [items, setIems] = useState([]);  const [errorMsg, setErrorMsg] = useState("");};export default App;

Теперь вы можете получить прямой доступ как к константам items, так и к errorMsg в вашем компоненте.

Далее, мы можем обновить состояние внутри такой функции:

import React, { useState } from "react";const App = () => {  const [items, setIems] = useState([]);  const [errorMsg, setErrorMsg] = useState("");  return (    <form>        <button onClick={() => setItems(["item A", "item B"])}>          Set items        </button>    </form>  );};export default App;

Вот как ты можешь использовать state hooks (хуки состояния).

#4: Кэширование затратных операций с помощью useMemo

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

Поэтому мы можем использовать hook useMemo для запоминания вывода при передаче тех же параметров в мемоизуемую функцию (memoized function). Hook useMemo принимает функцию и вводимые параметры для запоминания. React ссылается на это как на массив зависимостей. Каждое из значений, упоминаемое внутри функции, также должно появиться в массиве зависимостей.

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

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

#5: Разделение функций на чистые функции (Pure functions, PF) для улучшения качества кода

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

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

Вы можете недоумевать, зачем вам нужно заниматься функциональным программированием?

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

- freeCodeCamp

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

function ascSort (a, b) {  return a < b ? -1 : (b > a ? 1 : 0);}function descSort (a, b) {  return b < a ? -1 : (a > b ? 1 : 0);}

#6: Создайте собственные React Hooks

Мы научились использовать useState и useMemo React hooks. Тем не менее, React позволяет вам устанавливать свои собственные React hooks, чтобы сформировать необходимую логику и делать компоненты более читабельными.

Мы можем задать пользовательские React hooks, начиная с ключевого слова use, как и все другие React hooks. Это выгодно, когда вы хотите распределить логику между различными функциями. Вместо копирования функции, мы можем определить логику как React hook и повторно использовать ее в других функциях.

Вот пример React-компонента, который обновляет состояние, когда размер экрана становится меньше 600 пикселей. Если это происходит, переменная isScreenSmall устанавливается в true. В противном случае переменная устанавливается в false. Мы используем событие resize из объекта окна для обнаружения изменения размера экрана.

const LayoutComponent = () => {  const [onSmallScreen, setOnSmallScreen] = useState(false);  useEffect(() => {    checkScreenSize();    window.addEventListener("resize", checkScreenSize);  }, []);  let checkScreenSize = () => {    setOnSmallScreen(window.innerWidth < 768);  };  return (    <div className={`${onSmallScreen ? "small" : "large"}`}>      <h1>Hello World!</h1>    </div>  );};

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

import { useState, useEffect } from "react";const useSize = () => {  const [isScreenSmall, setIsScreenSmall] = useState(false);  let checkScreenSize = () => {    setIsScreenSmall(window.innerWidth < 600);  };  useEffect(() => {    checkScreenSize();    window.addEventListener("resize", checkScreenSize);    return () => window.removeEventListener("resize", checkScreenSize);  }, []);  return isScreenSmall;};export default useSize;

Мы можем создать пользовательский React hook, путем использования логики с функцией use.

В этом примере мы назвали пользовательский React hook useSize. Теперь мы можем импортировать useSize hook в любом месте, где нам это необходимо.

React custom hooks в этом нет ничего нового. Вы обертываете логику с функцией и даете ей имя, которое начинается с use. Она действует как обычная функция. Однако, следуя правилам "use", вы говорите всем, кто её импортирует, что это hook (хук). Более того, поскольку это hook, вы должны структурировать его так, чтобы следовать rules of hooks (правилам хуков).

Вот как сейчас выглядит наш компонент. Код становится намного чище!

import React from 'react'import useSize from './useSize.js'const LayoutComponent = () => {  const onSmallScreen = useSize();  return (    <div className={`${onSmallScreen ? "small" : "large"}`}>      <h1>Hello World!</h1>    </div>  );}

Бонусный совет: Мониторинг фронтенда

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

Вот и все! Эта статья научила вас шести методикам улучшения читабельности и качества кода.


Узнать подробнее о курсе "React.js Developer".

Посмотреть открытый вебинар на тему ReactJS: быстрый старт. Сильные и слабые стороны.

Подробнее..

От студента до учителя как разобраться в веб-разработке, если это не твой профиль

21.04.2021 20:21:36 | Автор: admin

Хоть кому-то и может показаться, что веб-разработчик это суровый технарь (айтишник же!), вход в эту профессию не сложнее, чем вPython. В неё часто переходят бывшие педагоги, юристы, бухгалтеры и другие гуманитарии. О том, с чего начать обучение, какие ошибки допускают новички, как освоиться в профессии и стоит ли самостоятельно учиться, рассказывает преподаватель веб-разработки в GeekBrains Алексей Кадочников.

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

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

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

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

Кто переучивается на разработчика

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

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

Мне самому пришлось сменить специальность. Восемь лет назад, когда я окончил университет, оказалось, что на рынке по специальности Вычислительные машины, комплексы, системы и сети всего 8 вакансий. Для четырех из них мне не хватало опыта, а по ещё четырём мне не перезвонили. В результате устроился инженером на завод и через несколько месяцев работы понял, что это не то, чему я хочу посвятить жизнь. Тогда яс нуляпрошелкурсы веб-разработкии нашёл работу по их окончанию. СейчасяFront-end developerвMail.ru GroupипреподаювGeekBrains.

Еще один пример мой студент Павел Литвин. Он не доучился в ВУЗе на безопасника, работал менеджером по продажам, потом в SEO, в конце концов выучился фронтенд-разработке и стал зарабатывать в4 раза больше, чем до курсов.И таких историй множество.

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

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

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

Самостоятельное обучение

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

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

Еще одна частая проблема самостоятельногообучения веб-разработке с нуляосвоение устаревших технологий. Мне приходилось переучивать студентов, выучивших неактуальную информацию. Есть вещи, которые уже не применяются, оптимизировались и их нужно удалить из памяти или перенастроить. Например, раньше в верстке для перемещения элемента использовалась командаfloat left, но это довольно громоздкое и сложное решение. Затем вместо него начали использоватьdisplay: flex. Теперь и этот метод успел устареть и теперь актуаленdisplay: grid. Внешний вид от всех этих способов будет одинаковым, но последнее решение изящнее и быстрее в реализации.

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

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

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

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

Высшее образование и курсы

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

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

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

Читать учебники перед началом обучения не стоит: они очень быстро устаревают. С момента написания книги до её попадания на полки проходит не один месяц. Её нужно отредактировать, сверстать, напечатать, выпустить на рынок. Лучше перед учебой (или одновременно с ней) читать официальную документацию выбранного языка или технологии. Например, вотсайт, на котором собран максимум информации для старта в JavaScript.

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

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

  • Junior-frontendдолжензнатьhtml + css + js + react.

  • Junior-fullstack: html + css + js + php +базыданных.

  • Middle frontendразработчик: html + css + js + react + vue + node.js +команднаяразработка.

  • Middle-fullstack: html + css + js + react + php + laravel +базыданных+команднаяразработка.

Обучение веб-разработкена наших курсах длится от 5 месяцев. За это время можно получить базу junior-фронтенда. Чтобы изучить технологии, нужные для миддл-фронтенда, понадобится год. Освоение навыков миддла и для фронтенда, и для бэкенда требует 1,5 года. А дальше нужно идти в бой и набираться опыта, чтобы подтвердить этот статус в реальной работе.

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

Как устроиться на работу и что от нее ожидать

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

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

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

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

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

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

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

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Оператор firstTruthy:

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

Оператор evenMultiplied:

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

Оператор liveSearch:

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

Заключение

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

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

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


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

Подробнее..

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

03.02.2021 10:04:43 | Автор: admin
Привет, Хабр!

У нас выходит долгожданное второе издание книги "Веб-разработка с применением Node и Express".



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


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

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

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

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

Согласно стандарту Internet Engineering Task Force (IETF), веб-ссылку можно представить как инструмент для описания отношений между страницами в вебе. Наиболее известные веб-ссылки те, что фигурируют на HTML-страницах и заключаются в элементы link или anchor, либо в заголовки HTTP. Но ссылки также могут фигурировать и в ресурсах API, а при использовании их вместо внешних ключей существенно сокращается объем информации, которую поставщику API приходится дополнительно документировать, а пользователю изучать.

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

В то время как ссылки не находят широкого применения в API, некоторые очень известные веб-API все-таки основаны на HTTP URL, используемых в качестве средства представления взаимоотношений. Таковы, например, Google Drive API и GitHub API. Почему так складывается? В этой статье я покажу, как на практике строится использование внешних ключей API, объясню их недостатки по сравнению с использованием ссылок, и расскажу, как преобразовать дизайн, использующий внешние ключи, в такой, где применяются ссылки.

Представление взаимоотношений при помощи внешних ключей


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

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



Взаимоотношение между Лесси и Джо выражается так: в представлении Лесси Джо обозначен как обладатель имени и значения, соответствующих владельцу. Обратное взаимоотношение не выражено. Значение владельца равно 98765, это внешний ключ. Вероятно, это самый настоящий внешний ключ базы данных то есть, перед нами значение первичного ключа из какой-то строки какой-то таблицы базы данных. Но, даже если реализация API немного изменит значения ключей, она все равно сближается по основным характеристикам с внешним ключом.
Значение 98765 не слишком годится для непосредственного использования клиентом. В наиболее распространенных случаях клиенту, воспользовавшись этим значением, нужно составить URL, а в документации к API необходимо описать формулу для выполнения такого преобразования. Как правило, это делается путем определения шаблона URI, вот так:

/people/{person_id}

Обратное взаимоотношение любимцы принадлежат владельцу также можно предоставить в API, реализовав и документировав один из следующих шаблонов URI (различия между ними только стилистические, а не по существу):

/pets?owner={person_id}
/people/{person_id}/pets


В API, спроектированных по такому принципу, обычно требуется определять и документировать много шаблонов URI. Наиболее популярным языком для определения таких шаблонов является не тот, что задан в спецификации IETF, а язык OpenAPI (ранее известный под названием Swagger). До версии 3.0 в OpenAPI не существовало способа указать, какие значения полей могут быть вставлены в какие шаблоны, поэтому часть документации требовалось составлять на естественном языке, а что-то приходилось угадывать клиенту. В версии 3.0 OpenAPI появился новый синтаксис под названием links, призванный решить эту проблему, но для того, чтобы пользоваться этой возможностью последовательно, надо потрудиться.

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

Представление взаимоотношений при помощи ссылок


Что, если бы ресурсы, показанные выше, были видоизменены следующим образом:



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

Обратите внимание: обратное взаимоотношение, то есть, от питомца к владельцу, теперь тоже реализовано явно, поскольку к представлению Joel добавлено поле "pets".

Изменение "id" на "self", в сущности, не является необходимым или важным, но существует соглашение, что при помощи "self" идентифицируется ресурс, чьи атрибуты и взаимоотношения указаны другими парами имя/значение в том же объекте JSON. "self" это имя, зарегистрированное в IANA для этой цели.

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

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

В предыдущем примере я использовал в ссылках относительную форму записи URI, например, /people/98765. Возможно, клиенту было бы немного удобнее (хотя, автору при форматировании этого поста было не слишком сподручно), если бы я выразил URI в абсолютной форме, напр. pets.org/people/98765. Клиентам необходимо знать лишь стандартные правила URI, определенные в спецификациях IETF, чтобы преобразовывать такие URI из одной формы в другую, поэтому выбор конкретной формы URI не так важен, как могло бы показаться на первый взгляд. Сравните эту ситуацию с описанным выше преобразованием из внешнего ключа в URL, для чего требовались конкретные знания об API зоомагазина. Относительные URL несколько удобнее для тех, кто занимается реализацией сервера, о чем рассказано ниже, но абсолютные URL, пожалуй, удобнее для большинства клиентов. Возможно, именно поэтому в API Google Drive и GitHub используются абсолютные URL.

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

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


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

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

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

  1. Не переписывайте URL в прокси. Я стараюсь избегать переписывания URL, но в вашей среде может не быть такой возможности.
  2. В прокси аккуратно найдите и переназначьте им формат везде, где они фигурируют в запросе или в отклике. Я так никогда не делал, поскольку мне это кажется сложным, чреватым ошибками и неэффективным, но кто-то, возможно, так поступает.
  3. Записывайте все ссылки в относительном виде. Можно не только встроить во все прокси некоторые возможности по перезаписи URL; более того, относительные URL могут упростить использование одного и того же кода в тестировании и в продакшене, так как код не придется конфигурировать и знать для этого его хост-имя. Если писать ссылки с использованием относительных URL, то есть, с единственным ведущим слэшем, как я показал в примере выше, то возникают некоторые минусы как для сервера, так и для клиента. Но в таком случае в прокси появляется лишь возможность сменить хост-имя (точнее, те части URL, которые называются схемой и источником), но не путь. В зависимости от того, как построены ваши URL, вы можете реализовать в прокси некоторую возможность переписывать пути, если готовы писать ссылки с использованием относительных URL без ведущих слэшей, но я так никогда не делал, поскольку полагаю, что серверам будет сложно записывать такие URL как следует.


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

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

/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765


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

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

Возможно, формат 2 для описания владельцев даже не будет предусмотрен. Также нет концептуального смысла в том, чтобы использовать в ссылках конкретную версию URL ведь Лесси принадлежит не конкретной версии Джо, а Джо как таковому. Поэтому, даже если вы предоставляете URL в формате /v1/people/98765 и идентифицируете таким образом конкретную версию Джо, то также должны предоставлять URL /people/98765 для идентификации самого Джо, и именно второй вариант использовать в ссылках. Другой вариант определить только URL /people/98765 и позволить клиентам выбирать конкретную версию, включая для этого заголовок запроса. Для этого заголовка нет никакого стандарта, но, если называть его Accept-Version, то такой вариант хорошо сочетается с именованием стандартных заголовков. Лично я предпочитаю использовать для версионирования заголовок и избегаю ставить в URL номера версий. но URL с номерами версий популярны, и я часто реализую и заголовок. и версионные URL, так как легче реализовать оба варианта, чем спорить, какой лучше. Подробнее о версионировании API можете почитать в этой статье.

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


В большинстве веб-API URL нового ресурса выделяется сервером, когда новый ресурс создается при помощи метода POST. Если вы пользуетесь этим методом для создания ресурсов и указываете взаимоотношения при помощи ссылок, то вам не требуется публиковать шаблон для URI этих ресурсов. Однако, некоторые API позволяют клиенту контролировать URL нового ресурса. Позволяя клиентам контролировать URL новых ресурсов, мы значительно упрощаем многие паттерны написания скриптов API разработчикам клиентской части, а также поддерживаем сценарии, в которых API используется для синхронизации информационной модели с внешним источником информации. В HTTP для этой цели предусмотрен специальный метод: PUT. PUT означает создай ресурс по этому URL, если он еще не существует, а если он существует обнови его. Если ваш API позволяет клиентам создавать новые сущности при помощи метода PUT, то вы должны документировать правила составления новых URL, возможно, включив для этого шаблон URI в спецификацию API. Также можно предоставить клиентам частичный контроль над URL, включив первичное ключ-подобное значение в тело или заголовки POST. В таком случае не требуется шаблон URI для POST как такового, но клиенту все равно придется выучить шаблон URI, чтобы полноценно пользоваться достигаемой в результате предсказуемостью URI.

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

В вышеприведенном примере мы включили следующую пару имя/значение в представление Джо:

"pets": "/pets?owner=/people/98765"

Клиенту, чтобы пользоваться этим URL, не требуется что-либо знать о его структуре кроме того, что он был записан в соответствии со стандартными спецификациями. Таким образом, клиент может получить по этой ссылке список питомцев Джо, не изучая для этого никакой язык запросов. Также отсутствует необходимость документировать в API форматы его URL но только в случае, если клиент сначала сделает запрос GET к /people/98765. Если же, кроме того, в API зоомагазина документирована возможность выполнения запросов, то клиент может составить такой же или эквивалентный URL запроса, чтобы извлечь питомцев интересующего его владельца, не извлекая перед этим самого владельца достаточно будет знать URI владельца. Возможно, даже важнее, что клиент может формировать и запросы, подобные следующим, что в ином случае было бы невозможно:

/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie


Спецификация URI описывает для этой цели часть HTTP URL, называемую "компонент запроса" это участок URL после первого ? и до первого #. Стиль запрашивания URI, который я предпочитаю использовать всегда ставить клиент-специфичные запросы в компонент запроса URI. Но при этом допустимо выражать клиентские запросы и в той части URL, которая называется путь. Так или иначе, необходимо описать клиентам, как составляются эти URL вы фактически проектируете и документируете язык запросов, специфичный для вашего API. Разумеется, также можно разрешить клиентам ставить запросы в теле сообщения, а не в URL, и пользоваться методом POST, а не GET. Поскольку существует практический лимит по размеру URL превышая 4k байт, вы всякий раз испытываете судьбу рекомендуется поддерживать POST для запросов, даже если вы уже поддерживаете GET.

Поскольку запросы такая полезная возможность в API, и поскольку проектировать и реализовывать языки запросов непросто, появились такие технологии, как GraphQL. Я никогда не пользовался GraphQL, поэтому не могу рекомендовать его, но вы можете рассмотреть его в качестве альтернативы для реализации возможности запросов в вашем API. Инструменты для реализации запросов в API, в том числе, GraphQL, лучше всего использовать в качестве дополнения к стандартному HTTP API для считывания и записи ресурсов, а не как альтернативу HTTP.

И кстати Как лучше всего писать ссылки в JSON?


В JSON, в отличие от HTML, нет встроенного механизма для выражения ссылок. Многие по-своему понимают, как ссылки должны выражаться в JSON, и некоторые подобные мнения публиковались в более или менее официальных документах, но в настоящее время нет стандартов, ратифицированных авторитетными организациями, которые бы это регламентировали. В вышеприведенном примере я выражал ссылки при помощи обычных пар имя/значение, написанных на JSON предпочитаю такой стиль и, кстати, этот же стиль используется в Google Drive и GitHub. Другой стиль, который вам, вероятно, встретится, таков:
  {"self": "/pets/12345", "name": "Lassie", "links": [   {"rel": "owner" ,    "href": "/people/98765"   } ]}

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

Есть и другой стиль написания ссылок на JSON, который мне нравится, и он выглядит так:
 {"self": "/pets/12345", "name": "Lassie", "owner": {"self": "/people/98765"}}


Польза этого стиля в том, что он явно дает: "/people/98765" это URL, а не просто строка. Я изучил этот паттерн по RDF/JSON. Одна из причин освоить этот паттерн вам так или иначе придется им пользоваться, всякий раз, когда вы захотите отобразить информацию об одном ресурсе, вложенную в другом ресурсе, как показано в следующем примере. Если использовать этот паттерн повсюду, код приобретает красивое единообразие:

{"self": "/pets?owner=/people/98765", "type": "Collection",  "contents": [   {"self": "/pets/12345",    "name": "Lassie",    "owner": {"self": "/people/98765"}   } ]}


Более подробно о том, как лучше всего использовать JSON для представления данных, рассказано в статье Terrifically Simple JSON.

Наконец, в чем же разница между атрибутом и взаимоотношением?


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

{"self": "/people/98765", "shoeSize": 10}

Принято считать, что shoeSize это атрибут, а не взаимоотношение, а 10 это значение, а не сущность. Правда, не менее логично утверждать, что строка '10 фактически является ссылкой, записанной специальной нотацией, предназначенной для ссылок на числа, до 11-го целого числа, которое само по себе является сущностью. Если 11-е целое число совершенно полноценная сущность, а строка '10' лишь указывает на нее, то пара имя/значение '"shoeSize": 10' концептуально является ссылкой, хотя, здесь и не используются URI.

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

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

Ссылки попросту лучше


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

Создаем веб-приложение на Haskell с использованием Reflex. Часть 1

24.02.2021 20:16:16 | Автор: admin

Введение


Всем привет! Меня зовут Никита, и мы в Typeable для разработки фронтенда для части проектов используем FRP-подход, а конкретно его реализацию на Haskell веб-фреймоворк reflex. На русскоязычных ресурсах отсутствуют какие-либо руководства по данному фреймворку (да и в англоязычном интернете их не так много), и мы решили это немного исправить.


В этой серии статей будет рассмотрено создание веб-приложения на Haskell с использованием платформы reflex-platform. reflex-platform предоставляет пакеты reflex и reflex-dom. Пакет reflex является реализацией Functional reactive programming (FRP) на языке Haskell. В библиотеке reflex-dom содержится большое число функций, классов и типов для работы с DOM. Эти пакеты разделены, т.к. FRP-подход можно использовать не только в веб-разработке. Разрабатывать мы будем приложение Todo List, которое позволяет выполнять различные манипуляции со списком задач.



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

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


  • Behavior a реактивная переменная, изменяющаяся во времени. Представляет собой некоторый контейнер, который на протяжении всего своего жизненного цикла содержит значение.
  • Event a событие в системе. Событие несет в себе информацию, которую можно получить только во время срабатывания события.

Пакет reflex предоставляет еще один новый тип:


  • Dynamic a является объединением Behavior a и Event a, т.е. это контейнер, который всегда содержит в себе некоторое значение, и, подобно событию, он умеет уведомлять о своем изменении, в отличие от Behavior a.

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


Подготовка


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


Чтобы ускорить процесс сборки, имеет смысл настроить кэш nix. В случае, если вы не используете NixOS, то вам нужно добавить следующие строки в файл /etc/nix/nix.conf:


binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.orgbinary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=binary-caches-parallel-connections = 40

Если используете NixOS, то в файл /etc/nixos/configuration.nix:


nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];

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


  • todo-client клиентская часть;
  • todo-server серверная часть;
  • todo-common содержит общие модули, которые используются сервером и клиентом (например типы API).

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


  • Создать директорию приложения: todo-app;
  • Создать проекты todo-common (library), todo-server (executable), todo-client (executable) в todo-app;
  • Настроить сборку через nix (файл default.nix в директории todo-app);
    • Также надо не забыть включить опцию useWarp = true;;
  • Настроить сборку через cabal (файлы cabal.project и cabal-ghcjs.project).

На момент публикации статьи default.nix будет выглядеть примерно следующим образом:


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:{  useWarp = true;  packages = {    todo-common = ./todo-common;    todo-server = ./todo-server;    todo-client = ./todo-client;  };  shells = {    ghc = ["todo-common" "todo-server" "todo-client"];    ghcjs = ["todo-common" "todo-client"];  };})

Примечание: в документации предлагается вручную склонировать репозиторий reflex-platform. В данном примере мы воспользовались средствами nix для получения платформы из репозитория.

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


Чтобы убедиться, что все работает, добавим в todo-client/src/Main.hs следующий код:


{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidget $ el "h1" $ text "Hello, reflex!"

Вся разработка ведется из nix-shell, поэтому в самом начале необходимо войти в этот shell:


$ nix-shell . -A shells.ghc

Для запуска через ghcid требуется ввести следующую команду:


$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'

Если все работает, то по адресу localhost:3003 вы увидите приветствие Hello, reflex!



Почему 3003?


Номер порта ищется в переменной окружения JSADDLE_WARP_PORT. Если эта переменная не установлена, то по умолчанию берется значение 3003.


Как это работает


Вы можете заметить, мы использовали при сборке не GHCJS, а обычный GHC. Это возможно благодаря пакетам jsaddle и jsaddle-warp. Пакет jsaddle предоставляет интерфейс для JS для работы из-под GHC и GHCJS. С помощью пакета jsaddle-warp мы можем запустить сервер, который посредством веб-сокетов будет обновлять DOM и играть роль JS-движка. Как раз для этого и был установлен флаг useWarp = true;, иначе по умолчанию использовался бы пакет jsaddle-webkit2gtk, и при запуске мы бы увидели десктопное приложение. Стоит отметить, что еще существуют прослойки jsaddle-wkwebview (для iOS приложений) и jsaddle-clib (для Android приложений).


Простейшее приложение TODO


Приступим к разработке!


Добавим следующий код в todo-client/src/Main.hs.


{-# LANGUAGE MonoLocalBinds #-}{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidgetWithHead headWidget rootWidgetheadWidget :: MonadWidget t m => m ()headWidget = blankrootWidget :: MonadWidget t m => m ()rootWidget = blank

Можно сказать, что функция mainWidgetWithHead представляет собой элемент <html> страницы. Она принимает два параметра head и body. Существуют еще функции mainWidget и mainWidgetWithCss. Первая функция принимает только виджет с элементом body. Вторая первым аргументом принимает стили, добавляемые в элемент style, и вторым аргументом элемент body.


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

Функция blank равносильна pure () и она ничего не делает, никак не изменяет DOM и никак не влияет на сеть событий.


Теперь опишем элемент <head> нашей страницы.


headWidget :: MonadWidget t m => m ()headWidget = do  elAttr "meta" ("charset" =: "utf-8") blank  elAttr "meta"    (  "name" =: "viewport"    <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )    blank  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"    <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"    <> "crossorigin" =: "anonymous")    blank  el "title" $ text "TODO App"

Данная функция сгенерирует следующее содержимое элемента head:


<meta charset="utf-8"><meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"><link crossorigin="anonymous" href="http://personeltest.ru/aways/stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet"><title>TODO App</title>

Класс MonadWidget позволяет строить или перестраивать DOM, а также определять сеть событий, которые происходят на странице.


Функция elAttr имеет следующий тип:


elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a

Она принимает название тэга, атрибуты и содержимое элемента. Возвращает эта функция, и вообще весь набор функций, строящих DOM, то, что возвращает их внутренний виджет. В данном случае наши элементы пустые, поэтому используется blank. Это одно из наиболее частых применений этой функции когда требуется сделать тело элемента пустым. Так же используется функция el. Ее входными параметрами являются только название тэга и содержимое, другими словами это упрощенная версия функции elAttr без атрибутов. Другая функция, используемая здесь text. Ее задача вывод текста на странице. Эта функция экранирует все возможные служебные символы, слова и тэги, и поэтому именно тот текст, который передан в нее, будет выведен. Для того чтобы встроить кусок html, существует функция elDynHtml.


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


type MonadWidgetConstraints t m =  ( DomBuilder t m  , DomBuilderSpace m ~ GhcjsDomSpace  , MonadFix m  , MonadHold t m  , MonadSample t (Performable m)  , MonadReflexCreateTrigger t m  , PostBuild t m  , PerformEvent t m  , MonadIO m  , MonadIO (Performable m)#ifndef ghcjs_HOST_OS  , DOM.MonadJSM m  , DOM.MonadJSM (Performable m)#endif  , TriggerEvent t m  , HasJSContext m  , HasJSContext (Performable m)  , HasDocument m  , MonadRef m  , Ref m ~ Ref IO  , MonadRef (Performable m)  , Ref (Performable m) ~ Ref IO  )class MonadWidgetConstraints t m => MonadWidget t m

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


newtype Todo = Todo  { todoText :: Text }newTodo :: Text -> TodonewTodo todoText = Todo {..}

Тело будет иметь следующую структуру:


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ do    elClass "h2" "text-center mt-3" $ text "Todos"    newTodoEv <- newTodoForm    todosDyn <- foldDyn (:) [] newTodoEv    delimiter    todoListWidget todosDyn

Функция elClass на вход принимает название тэга, класс (классы) и содержимое. divClass это сокращенная версия elClass "div".


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


foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)

Она похожа на foldr :: (a -> b -> b) -> b -> [a] -> b и, по сути, выполняет такую же роль, только в роли списка здесь событие. Результирующее значение обернуто в контейнер Dynamic, т.к. оно будет обновляться после каждого события. Процесс обновления задаётся функцией-параметром, которая принимает на вход значение из возникшего события и текущее значение из Dynamic. На их основе формируется новое значение, которое будет находиться в Dynamic. Это обновление будет происходить каждый раз при возникновении события.


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


Функция newTodoForm строит ту часть DOM, в которой будет форма ввода описания задания, и возвращает событие, которое несет в себе новое Todo. Именно при возникновении этого события будет обновляться список заданий.


newTodoForm :: MonadWidget t m => m (Event t Todo)newTodoForm = rowWrapper $  el "form" $    divClass "input-group" $ do      iEl <- inputElement $ def        & initialAttributes .~          (  "type" =: "text"          <> "class" =: "form-control"          <> "placeholder" =: "Todo" )      let        newTodoDyn = newTodo <$> value iEl        btnAttr = "class" =: "btn btn-outline-secondary"          <> "type" =: "button"      (btnEl, _) <- divClass "input-group-append" $        elAttr' "button" btnAttr $ text "Add new entry"      pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Первое нововведение, которое мы встречаем тут, это функция inputElement. Ее название говорит само за себя, она добавляет элемент input. В качестве параметра она принимает тип InputElementConfig. Он имеет много полей, наследует несколько различный классов, но в данном примере нам наиболее интересно добавить нужные атрибуты этому тегу, и это можно сделать при помощи линзы initialAttributes. Функция value является методом класса HasValue и возвращает значение, которое находится в данном input. В случае типа InputElement оно имеет тип Dynamic t Text. Это значение будет обновляться при каждом изменении, происходящем в поле input.


Следующее изменение, которое тут можно заметить, это использование функции elAttr'. Отличие функций со штрихом от функций без штриха для построения DOM заключается в том, что эти функции вдобавок возвращают сам элемент страницы, с которым мы можем производить различные манипуляции. В нашем случае он необходим, чтобы мы могли получить событие нажатия на этот элемент. Для этого служит функция domEvent. Эта функция принимает название события, в нашем случае Click и сам элемент, с которым связано это событие. Функция имеет следующую сигнатуру:


domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)

Ее возвращаемый тип зависит от типа события и типа элемента. В нашем случае это ().


Следующая функция, которую мы встречаем tagPromptlyDyn. Она имеет следующий тип:


tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a

Ее задача заключается в том, чтобы при срабатывании события, поместить в него значение, которое находится в данный момент внутри Dynamic. Т.е. событие, являющееся результатом функции tagPromptlyDyn valDyn btnEv возникает одновременно с btnEv, но несёт в себе значение, которое было в valDyn. Для нашего примера это событие будет происходить при нажатии кнопки и нести в себе значение из текстового поля ввода.


Тут следует сказать про то, что функции, которые содержат в своём названии слово promptly, потенциально опасные они могут вызывать циклы в сети событий. Внешне это будет выглядеть так, как будто приложение зависло. Вызов tagPromplyDyn valDyn btnEv, по возможности, надо заменять на tag (current valDyn) btnEv. Функция current получает Behavior из Dynamic. Эти вызовы не всегда взаимозаменяемые. Если обновление Dynamic и событие Event в tagPromplyDyn возникают в один момент, т.е. в одном фрейме, то выходное событие будет содержать те данные, которые получил Dynamic в этом фрейме. В случае, если мы будем использовать tag (current valDyn) btnEv, то выходное событие будет содержать те данные, которыми исходный current valDyn, т.е. Behavior, обладал в прошлом фрейме.


Здесь мы подошли к еще одному различию между Behavior и Dynamic: если Behavior и Dynamic получают обновление в одном фрейме, то Dynamic будет обновлен уже в этом фрейме, а Behavior приобретет новое значение в следующем. Другими словами, если событие произошло в момент времени t1 и в момент времени t2, то Dynamic будет обладать значением, которое принесло событие t1 в промежутке времени [t1, t2), а Behavior (t1, t2].


Задача функции todoListWidget заключается в выводе всего списка Todo.


todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()todoListWidget todosDyn = rowWrapper $  void $ simpleList todosDyn todoWidget

Здесь встречается функция simpleList. Она имеет следующую сигнатуру:


simpleList  :: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)  => Dynamic t [v]  -> (Dynamic t v -> m a)  -> m (Dynamic t [a])

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


todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()todoWidget todoDyn =  divClass "d-flex border-bottom" $    divClass "p-2 flex-grow-1 my-auto" $      dynText $ todoText <$> todoDyn

Функция dynText отличается от функции text тем, что на вход принимает текст, обернутый в Dynamic. В случае, если элемент списка будет изменен, то это значение также обновится в DOM.


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


rowWrapper :: MonadWidget t m => m a -> m arowWrapper ma =  divClass "row justify-content-md-center" $    divClass "col-6" ma

Функция delimiter просто добавляет элемент-разделитель.


delimiter :: MonadWidget t m => m ()delimiter = rowWrapper $  divClass "border-top mt-3" blank


Полученный результат можно посмотреть в нашем репозитории.


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

Подробнее..

Перевод Углубленный анализ тестирования виджетов во Flutter. Часть II. Классы Finder и WidgetTester

11.05.2021 18:04:27 | Автор: admin

Перевод материала подготовлен в рамках онлайн-курса "Flutter Mobile Developer".

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


Это продолжение первой части статьи о тестировании виджетов во Flutter.

Продолжим наше изучение процесса тестирования виджетов.

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

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

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

  2. Мы сохраняем наши тесты в папке test.

  3. Внутри функции testWidgets() пишем тесты виджетов, и мы подробно рассмотрели состав этой функции.

Продолжим наш анализ.

Как пишется тест виджета?

Тест виджета обычно дает возможность проверить:

  1. Отображаются ли визуальные элементы.

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

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

  1. Задаем начальные условия и создаем виджет для тестирования.

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

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

  4. Убеждаемся, что результаты соответствуют ожидаемым.

Создание виджета для тестирования

Чтобы протестировать виджет, очевидно, нам нужен сам виджет. Давайте рассмотрим тест по умолчанию в папке test:

void main() {  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here    },  );}

Наверняка, вы заметили объект WidgetTester в функции обратного вызова, где мы пишем наш тест. Пришло время применить его.

Чтобы создать новый виджет для тестирования, используем метод pumpWidget():

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),          ),        ),      );    },  );

(Не забудьте про await, иначе тест будет выдавать кучу ошибок.)

Этот метод создает виджет для тестирования.

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

Объекты-искатели

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

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

Итак, как же найти виджет? Для этого мы используем объект-искатель, класс Finder. (Вы можете искать и элементы, но это другая тема.)

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

Давайте рассмотрим широко распространенные и некоторые более специфические способы поиска виджетов:

find.byType()

Давайте в качестве примера рассмотрим поиск виджета Text:

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              child: Text('Hi there!'),            ),          ),        ),      );      var finder = find.byType(Text);    },  );

Здесь для создания объекта-искателя мы используем предопределенный экземпляр класса CommonFinders под именем find. Функция byType() помогает нам найти ЛЮБОЙ виджет определенного типа. Таким образом, если в дереве виджетов существует два текстовых виджета, будут идентифицированы ОБА. Поэтому, если вы хотите найти определенный виджет Text, подумайте о том, чтобы добавить в него ключ или использовать следующий тип:

find.text()

Чтобы найти конкретный виджет Text, используйте функцию find.text():

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              child: Text('Hi there!'),            ),          ),        ),      );      var finder = find.text('Hi there!');    },  );

Это также применимо и для любого виджета типа EditableText, например виджета TextField.

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      var controller = TextEditingController.fromValue(TextEditingValue(text: 'Hi there!'));      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              child: TextField(controller: controller,),            ),          ),        ),      );      var finder = find.text('Hi there!');    },  );

find.byKey()

Один из самых распространенных и простых способов найти виджет это просто добавить в него ключ:

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              child: Icon(                Icons.add,                key: Key('demoKey'),              ),            ),          ),        ),      );      var finder = find.byKey(Key('demoKey'));    },  );

find.descendant() и find.ancestor()

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

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

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              key: Key('demoKey'),              child: Icon(Icons.add),            ),          ),        ),      );            var finder = find.descendant(        of: find.byKey(Key('demoKey')),        matching: find.byType(Icon),      );    },  );

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

Вызов find.ancestor() во многом схож, но роли меняются местами, так как мы пытаемся найти виджет, расположенный выше виджета, определенного с помощью параметра of.

Если бы здесь мы пытались найти виджет Center, мы бы сделали следующее:

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              key: Key('demoKey'),              child: Icon(Icons.add),            ),          ),        ),      );      var finder = find.ancestor(        of: find.byType(Icon),        matching: find.byKey(Key('demoKey')),      );    },  );

Создание пользовательского объекта-искателя

При использовании функций вида find.xxxx() мы используем предопределенный класс Finder. А если мы хотим использовать собственный способ поиска виджета?

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

  1. Сначала дополним классMatchFinder.

class BadlyWrittenWidgetFinder extends MatchFinder {    @override  // TODO: implement description  String get description => throw UnimplementedError();  @override  bool matches(Element candidate) {    // TODO: implement matches    throw UnimplementedError();  }  }

2. С помощью функции matches() мы проверяем, соответствует ли виджет нашим условиям. В нашем случае предстоит проверить, является ли виджет значком и равен ли его ключ значению null:

class BadlyWrittenWidgetFinder extends MatchFinder {  BadlyWrittenWidgetFinder({bool skipOffstage = true})      : super(skipOffstage: skipOffstage);  @override  String get description => 'Finds icons with no key';  @override  bool matches(Element candidate) {    final Widget widget = candidate.widget;    return widget is Icon && widget.key == null;  }}

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

extension BadlyWrittenWidget on CommonFinders {  Finder byBadlyWrittenWidget({bool skipOffstage = true }) => BadlyWrittenWidgetFinder(skipOffstage: skipOffstage);}

4. Благодаря расширениям мы можем обращаться к объекту-искателю так же, как и к любым другим объектам:

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(        MaterialApp(          home: Scaffold(            appBar: AppBar(),            body: Center(              key: Key('demoKey'),              child: Icon(Icons.add),            ),          ),        ),      );      var finder = find.byBadlyWrittenWidget();    },  );

Теперь, когда мы познакомились с объектами-искателями, перейдем к изучению класса WidgetTester.

Все, что нужно знать о WidgetTester

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

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

В тесте виджета функция setState() работает не так, как она обычно работает.

Хотя функция setState()помечает виджет, подлежащий перестраиванию, в реальности она не перестраивает дерево виджетов в тесте. Так как же нам это сделать? Давайте посмотрим на методы pump.

Для чего нужны методы pump

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

Немного о функции pumpWidget()

Как мы видели ранее, функция pumpWidget() использовалась для установки корневого виджета для тестирования. Она вызывает функцию runApp(), используя указанный виджет, и осуществляет внутренний вызов функции pump(). При повторном вызове функция перестраивает все дерево.

Подробнее о функции pump()

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

class CounterWidget extends StatefulWidget {  @override  _CounterWidgetState createState() => _CounterWidgetState();}class _CounterWidgetState extends State<CounterWidget> {  var count = 0;  @override  Widget build(BuildContext context) {    return MaterialApp(      home: Scaffold(        body: Text('$count'),        floatingActionButton: FloatingActionButton(          child: Icon(Icons.add),          onPressed: () {            setState(() {              count++;            });          },        ),      ),    );  }}

Виджет просто хранит значение счетчика и обновляет его при нажатии кнопки FloatingActionButton, как в стандартном приложении-счетчике.

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

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(CounterWidget());      var finder = find.byIcon(Icons.add);      await tester.tap(finder);            // Ignore this line for now      // It just verifies that the value is what we expect it to be      expect(find.text('1'), findsOneWidget);    },  );

А вот и нет:

Причина в том, что мы перестраиваем виджет Text, отображающий счетчик, с помощью функции setState() в виджете, но в данном случае виджет не перестраивается. Нам также необходимо вызвать метод pump():

  testWidgets(    'Test description',    (WidgetTester tester) async {      // Write your test here      await tester.pumpWidget(CounterWidget());      var finder = find.byIcon(Icons.add);      await tester.tap(finder);      await tester.pump();      // Ignore this line for now      // It just verifies that the value is what we expect it to be      expect(find.text('1'), findsOneWidget);    },  );

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

Если вам нужно запланировать отображение кадра через определенное время, в метод pump() также можно передать время тогда будет запланировано перестраивание виджета ПОСЛЕ истечения указанного временного промежутка:

await tester.pump(Duration(seconds: 1));

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

У метода pump есть полезная особенность: вы можете остановить его на нужном этапе перестраивания и визуализации виджета. Для этого необходимо задать параметр EnginePhase данного метода:

enum EnginePhase {  /// The build phase in the widgets library. See [BuildOwner.buildScope].  build,  /// The layout phase in the rendering library. See [PipelineOwner.flushLayout].  layout,  /// The compositing bits update phase in the rendering library. See  /// [PipelineOwner.flushCompositingBits].  compositingBits,  /// The paint phase in the rendering library. See [PipelineOwner.flushPaint].  paint,  /// The compositing phase in the rendering library. See  /// [RenderView.compositeFrame]. This is the phase in which data is sent to  /// the GPU. If semantics are not enabled, then this is the last phase.  composite,  /// The semantics building phase in the rendering library. See  /// [PipelineOwner.flushSemantics].  flushSemantics,  /// The final phase in the rendering library, wherein semantics information is  /// sent to the embedder. See [SemanticsOwner.sendSemanticsUpdate].  sendSemanticsUpdate,}await tester.pump(Duration.zero, EnginePhase.paint);

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

Переходим к pumpAndSettle()

Метод pumpAndSettle() это, по сути, тот же метод pump, но вызываемый до того момента, когда не будет запланировано ни одного нового кадра. Он помогает завершить все анимации.

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

await tester.pumpAndSettle(        Duration(milliseconds: 10),        EnginePhase.paint,        Duration(minutes: 1),      );

Взаимодействие со средой

Класс WidgetTester позволяет нам использовать сложные взаимодействия помимо обычных взаимодействий типа поиск + касание. Вот что можно делать с его помощью:

Метод tester.drag() позволяет инициировать перетаскивание из середины виджета, который мы находим с помощью объекта-искателя по определенному смещению. Мы можем задать направление перетаскивания, указав соответствующие смещения по осям X и Y:

      var finder = find.byIcon(Icons.add);      var moveBy = Offset(100, 100);      var slopeX = 1.0;      var slopeY = 1.0;      await tester.drag(finder, moveBy, touchSlopX: slopeX, touchSlopY: slopeY);

Мы также можем инициировать перетаскивание с контролем по времени, используя методtester.timedDrag():

      var finder = find.byIcon(Icons.add);      var moveBy = Offset(100, 100);      var dragDuration = Duration(seconds: 1);      await tester.timedDrag(finder, moveBy, dragDuration);

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

      var dragFrom = Offset(250, 300);      var moveBy = Offset(100, 100);      var slopeX = 1.0;      var slopeY = 1.0;      await tester.dragFrom(dragFrom, moveBy, touchSlopX: slopeX, touchSlopY: slopeY);

Также существует вариантэтого метода с контролем по времени tester.timedDragFrom().

      var dragFrom = Offset(250, 300);      var moveBy = Offset(100, 100);      var duration = Duration(seconds: 1);      await tester.timedDragFrom(dragFrom, moveBy, duration);

Примечание. Если вы хотите имитировать смахивание, используйте методtester.fling() вместоtester.drag().

Создание пользовательских жестов

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

Сначала нам нужно инициализировать жест:

      var dragFrom = Offset(250, 300);      var gesture = await tester.startGesture(dragFrom);

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

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

      var dragFrom = Offset(250, 300);      var gesture = await tester.startGesture(dragFrom);            await gesture.moveBy(Offset(50.0, 0));      await gesture.moveBy(Offset(0.0, -50.0));      await gesture.moveBy(Offset(-50.0, 0));      await gesture.moveBy(Offset(0.0, 50.0));            await gesture.up();

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

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


Подробнее о курсе "Flutter Mobile Developer".

Участвовать в интенсиве Создаем приложение на Flutter для Web, iOS и Android.

Подробнее..

Создаем веб-приложение на Haskell с использованием Reflex. Часть 3

17.05.2021 18:06:47 | Автор: admin

Часть 1.


Часть 2.


Всем привет! В этой части мы рассмотрим использование класса EventWriter и библиотеки ghcjs-dom.



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


Сейчас, для того, чтобы прокинуть события с более глубоких уровней, мы передаем их в качестве возвращемых значений. Это не всегда удобно, особенно, когда надо возвращать что-то, помимо события (например, форма ввода может возвращать одновременно и событие нажатия кнопки, и данные из формы). Гораздо удобнее было бы использовать механизм, который может "прокинуть" события наверх автоматически, не задумываясь о том, что надо их постоянно возвращать. И такой механизм есть EventWriter. Этот класс позволяет записывать события, наподобие стандартной монады Writer. Перепишем наше приложение с использованием EventWriter.


Для начала рассмотрим сам класс EventWriter:


class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where  tellEvent :: Event t w -> m ()

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


Существует трансформер, являющийся экземпляром этого класса EventWriterT, для его запуска используется функция runEventWriterT.


Далее переходим к изменению функций. Наибольшие изменения ожидают функцию rootWidget.


rootWidget :: MonadWidget t m => m ()rootWidget =  divClass "container" $ mdo    elClass "h2" "text-center mt-3" $ text "Todos"    (_, ev) <- runEventWriterT $ do      todosDyn <- foldDyn appEndo mempty ev      newTodoForm      delimiter      todoListWidget todosDyn    blank

Мы добавили запуск трансформера и избавились от всех возвращаемых событий.


Изменения в newTodoForm не такие большие, но все же, стоит их отметить:


newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

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


Функция todoListWidget сильно упростилась.


todoListWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Dynamic t Todos -> m ()todoListWidget todosDyn = rowWrapper $  void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget

Нас теперь вообще не интересует возвращаемое событие, и, соответственно, отпала необходимость в извлечении Event из Dynamic.


В функции todoWidget также произошли заметные изменения. Больше нет необходимости работать с возвращаемым типом преобразовывать Event t (Event t TodoEvent). Отличие функции dyn_ от функции dyn, в том, что она игнорирует возвращаемое значение.


todoWidget  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Dynamic t Todo -> m ()todoWidget ix todoDyn' = do  todoDyn <- holdUniqDyn todoDyn'  dyn_ $ ffor todoDyn $ \td@Todo{..} -> case todoState of    TodoDone         -> todoDone ix todoText    TodoActive False -> todoActive ix todoText    TodoActive True  -> todoEditable ix todoText

Единственное изменение в функциях todoDone, todoActive и todoEditable это новый тип и запись события вместо его возврата.


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoActive ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Done"    (editEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Edit"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , update (Just . startEdit) ix  <$ domEvent Click editEl      , delete ix <$ domEvent Click delEl      ]todoDone  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoDone ix todoText = divClass "d-flex border-bottom" $ do  divClass "p-2 flex-grow-1 my-auto" $    el "del" $ text todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Undo"    (delEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Drop"    tellEvent $ Endo <$> leftmost      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl      , delete ix <$ domEvent Click delEl      ]todoEditable  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> m ()todoEditable ix todoText = divClass "d-flex border-bottom" $ do  updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $    editTodoForm todoText  divClass "p-2 btn-group" $ do    (doneEl, _) <- elAttr' "button"      (  "class" =: "btn btn-outline-secondary"      <> "type" =: "button" ) $ text "Finish edit"    let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix    tellEvent $      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

Применение класса EventWriter упростило код и сделало его более читаемым.


ghcjs-dom


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


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


function toClipboard(txt){  var inpEl = document.createElement("textarea");  document.body.appendChild(inpEl);  inpEl.value = txt  inpEl.focus();  inpEl.select();  document.execCommand('copy');  document.body.removeChild(inpEl);}

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


{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE MonoLocalBinds #-}module GHCJS whereimport Control.Monadimport Data.Functor (($>))import Data.Text (Text)import GHCJS.DOMimport GHCJS.DOM.Document  (createElement, execCommand, getBodyUnchecked)import GHCJS.DOM.Element as Element hiding (scroll)import GHCJS.DOM.HTMLElement as HE (focus)import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)import GHCJS.DOM.Node (appendChild, removeChild)import GHCJS.DOM.Types hiding (Event, Text)import Reflex.Dom as RtoClipboard :: MonadJSM m => Text -> m ()toClipboard txt = do  doc <- currentDocumentUnchecked  body <- getBodyUnchecked doc  inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc    ("textarea" :: Text)  void $ appendChild body inpEl  HE.focus inpEl  HIE.setValue inpEl txt  HIE.select inpEl  void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)  void $ removeChild body inpEl

Почти каждой строке из haskell функции toClipboard есть соответствие из JS функции. Стоит отметить, что здесь нет привычного класса MonadWidget, а используется MonadJSM это та монада, в которой производятся вся работы с помощью ghcjs-dom. Класс MonadWidget наследует класс MonadJSM. Рассмотрим, как осуществляется привязка обработчика к событию:


copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()copyByEvent txt ev =  void $ performEvent $ ev $> toClipboard txt

Здесь мы видим новую для нас функцию performEvent, и с помощью нее осуществляется привязка обработчика к событию. Она является методом класса PerformEvent:


class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where  type Performable m :: * -> *  performEvent :: Event t (Performable m a) -> m (Event t a)  performEvent_ :: Event t (Performable m ()) -> m ()

Теперь изменим виджет невыполненного задания, предварительно не забыв добавить импорт import GHCJS:


todoActive  :: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()todoActive ix Todo{..} =  divClass "d-flex border-bottom" $ do    divClass "p-2 flex-grow-1 my-auto" $      text todoText    divClass "p-2 btn-group" $ do      (copyEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Copy"      (doneEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Done"      (editEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Edit"      (delEl, _) <- elAttr' "button"        (  "class" =: "btn btn-outline-secondary"        <> "type" =: "button" ) $ text "Drop"      copyByEvent todoText $ domEvent Click copyEl      tellEvent $ leftmost        [ ToggleTodo ix <$ domEvent Click doneEl        , StartEditTodo ix <$ domEvent Click editEl        , DeleteTodo ix <$ domEvent Click delEl        ]

Была добавлена новая кнопка Copy и вызов определенной функции copyByEvent. Эти же самые действия можно проделать с виджетами для других состояний задания.


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


В следующей части рассмотрим использование JSFFI (JS Foreign Function Interface).

Подробнее..

Создаем веб-приложение на Haskell с использованием Reflex. Часть 4

18.06.2021 18:20:53 | Автор: admin

Часть 1.


Часть 2.


Часть 3.


Всем привет! В новой части мы рассмотрим использование JSFFI.


intro


JSFFI


Добавим в наше приложение возможность установки даты дедлайна. Допустим, требуется сделать не просто текстовый input, а чтобы это был выпадающий datepicker. Можно, конечно, написать свой datepicker на рефлексе, но ведь существует большое множество различных JS библиотек, которыми можно воспользоваться. Когда существует уже готовый код на JS, который, например, слишком большой, чтобы переписывать с использованием GHCJS, есть возможность подключить его с помощью JSFFI (JavaScript Foreign Function Interface). В нашем случае мы будем использовать flatpickr.


Создадим новый модуль JSFFI, сразу добавим его импорт в Main. Вставим в созданный файл следующий код:


{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Control.Monad.IO.Classimport Reflex.Domforeign import javascript unsafe  "(function() { \  \ flatpickr($1, { \  \   enableTime: false, \  \   dateFormat: \"Y-m-d\" \  \  }); \  \})()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw

Так же не забудем добавить в элемент head необходимые скрипт и стили:


  elAttr "link"    (  "rel" =: "stylesheet"    <> "href" =: "https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" )    blank  elAttr "script"    (  "src" =: "https://cdn.jsdelivr.net/npm/flatpickr")    blank

Пробуем скомпилировать, так же как и раньше, и получаем следующую ошибку:


src/JSFFI.hs:(9,1)-(16,60): error:     The `javascript' calling convention is unsupported on this platform     When checking declaration:        foreign import javascript unsafe "(function() {    flatpickr($1, {      enableTime: false,      dateFormat: \"Y-m-d\"    });   })()" addDatePicker_js          :: RawInputElement GhcjsDomSpace -> IO ()  |9 | foreign import javascript unsafe  |

Действительно, сейчас мы собираем наше приложение с помощью GHC, который понятия не имеет, что такое JSFFI. Напомним, что сейчас запускается сервер, который с помощью вебсокетов отправляет обновленный DOM, когда требуется, и код на JavaScript для него чужд. Здесь напрашивается вывод, что использовать наш datepicker при сборке с помощью GHC не получится. Тем не менее, в продакшене GHC для клиента не будет использоваться, мы будем компилировать в JS при помощи GHCJS, и полученный JS встраивать уже в нашу страницу. ghcid не поддерживает GHCJS поэтому смысла грузиться в nix shell нет, мы будем использовать nix сразу для сборки:


nix-build . -A ghcjs.todo-client -o todo-client-bin

В корневой директории приложения появится директория todo-client-bin со следующей структурой:


todo-client-bin bin     todo-client-bin     todo-client-bin.jsexe         all.js         all.js.externs         index.html         lib.js         manifest.webapp         out.frefs.js         out.frefs.json         out.js         out.stats         rts.js         runmain.js

Открыв index.html в браузере, увидим наше приложение. Мы собрали проект с помощью GHCJS, но ведь для разработки все равно удобнее использовать GHC вместе с ghcid, поэтому модифицируем модуль JSFFI следующем образом:


{-# LANGUAGE CPP #-}{-# LANGUAGE MonoLocalBinds #-}module JSFFI whereimport Reflex.Dom#ifdef ghcjs_HOST_OSimport Control.Monad.IO.Classforeign import javascript unsafe  "(function() {\    flatpickr($1, {\      enableTime: false,\      dateFormat: \"Y-m-d\"\    }); \  })()"  addDatePicker_js :: RawInputElement GhcjsDomSpace -> IO ()addDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker = liftIO . addDatePicker_js . _inputElement_raw#elseaddDatePicker :: MonadWidget t m => InputElement er GhcjsDomSpace t -> m ()addDatePicker _ = pure ()#endif

Мы добавили условную компиляцию: в зависимости от платформы, либо будем использовать вызов JS функций, либо заглушку.


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


newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  addDatePicker dEl  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

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


uncaught exception in Haskell main thread: ReferenceError: flatpickr is not definedrts.js:5902 ReferenceError: flatpickr is not defined    at out.js:43493    at h$$abX (out.js:43495)    at h$runThreadSlice (rts.js:6847)    at h$runThreadSliceCatch (rts.js:6814)    at h$mainLoop (rts.js:6809)    at rts.js:2190    at runIfPresent (rts.js:2204)    at onGlobalMessage (rts.js:2240)

Замечаем, что необходимая нам функция не определена. Так получается, потому что элемент script со ссылкой создается динамически, равно как и вообще все элементы страницы. Поэтому, когда мы используем вызов функции flatpickr, скрипт, содержащий библиотеку с этой функцией может быть еще не загружен. Надо явно расставить порядок загрузки.
Решим эту проблему при помощи пакета reflex-dom-contrib. Этот пакет содержит много полезных при разработке функций. Его подключение нетривиально. Дело в том, что на Hackage лежит устаревшая версия этого пакета, поэтому придется брать его напрямую c GitHub. Обновим default.nix следующим образом.


{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {    owner = "reflex-frp";    repo = "reflex-platform";    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";    })}:(import reflex-platform {}).project ({ pkgs, ... }:let  reflexDomContribSrc = builtins.fetchGit {    url = "https://github.com/reflex-frp/reflex-dom-contrib.git";    rev = "11db20865fd275362be9ea099ef88ded425789e7";  };  override = self: pkg: with pkgs.haskell.lib;  doJailbreak (pkg.overrideAttrs  (old: {    buildInputs = old.buildInputs ++ [ self.doctest self.cabal-doctest ];  }));in {  useWarp = true;  overrides = self: super: with pkgs.haskell.lib; rec {    reflex-dom-contrib = dontHaddock (override self      (self.callCabal2nix "reflex-dom-contrib" reflexDomContribSrc { }));  };  packages = {    todo-common = ./todo-common;    todo-server = ./todo-server;    todo-client = ./todo-client;  };  shells = {    ghc = ["todo-common" "todo-server" "todo-client"];    ghcjs = ["todo-common" "todo-client"];  };})

Добавим импорт модуля import Reflex.Dom.Contrib.Widgets.ScriptDependent и внесем изменения в форму:


newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo  iEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Todo" )    & inputElementConfig_setValue .~ ("" <$ btnEv)  dEl <- inputElement $ def    & initialAttributes .~      (  "type" =: "text"      <> "class" =: "form-control"      <> "placeholder" =: "Deadline"      <> "style" =: "max-width: 150px" )  pb <- getPostBuild  widgetHoldUntilDefined "flatpickr"    (pb $> "https://cdn.jsdelivr.net/npm/flatpickr")    blank    (addDatePicker dEl)  let    addNewTodo = \todo -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo) todos    newTodoDyn = addNewTodo <$> value iEl    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"  (btnEl, _) <- divClass "input-group-append" $    elAttr' "button" btnAttr $ text "Add new entry"  let btnEv = domEvent Click btnEl  pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

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


Но мы никак не задействовали это поле. Изменим тип Todo, не забыв добавить импорт Data.Time:


data Todo = Todo  { todoText     :: Text  , todoDeadline :: Day  , todoState    :: TodoState }  deriving (Generic, Eq, Show)newTodo :: Text -> Day -> TodonewTodo todoText todoDeadline = Todo {todoState = TodoActive False, ..}

Теперь изменим функцию с формой для нового задания:


...  today <- utctDay <$> liftIO getCurrentTime  let    dateStrDyn = value dEl    dateDyn = fromMaybe today . parseTimeM True      defaultTimeLocale "%Y-%m-%d" . unpack <$> dateStrDyn    addNewTodo = \todo date -> Endo $ \todos ->      insert (nextKey todos) (newTodo todo date) todos    newTodoDyn = addNewTodo <$> value iEl <*> dateDyn    btnAttr = "class" =: "btn btn-outline-secondary"      <> "type" =: "button"...

И добавим отображение даты в списке:


todoActive  :: (EventWriter t (Endo Todos) m, MonadWidget t m)  => Int -> Text -> Day -> m ()todoActive ix todoText deadline = divClass "d-flex border-bottom" $ do  elClass "p" "p-2 flex-grow-1 my-auto" $ do    text todoText    elClass "span" "badge badge-secondary px-2" $      text $ pack $ formatTime defaultTimeLocale "%F" deadline  divClass "p-2 btn-group" $ do  ...

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


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

Подробнее..

Перевод Быстрый и грязный Django Передача данных в JavaScript без AJAX

18.03.2021 16:05:16 | Автор: admin

Привет, хабровчане. Для будущих студентов курса "Web-разработчик на Python" подготовили перевод материала.


Если мы хотим передать данные из Django в JavaScript, мы обычно говорим об API, сериализаторах, вызовах JSON и AJAX. Обычно дело усложняется наличием React или Angular на фронте.

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

Обычный подход

Допустим, у нас есть приложение на Django со следующей моделью:

from django.db import modelsclass SomeDataModel(models.Model):    date = models.DateField(db_index=True)    value = models.IntegerField()

И простой TemplateView:

<img alt="Изображение выглядит как текст

from django.views.generic import TemplateViewclass SomeTemplateView(TemplateView):    template_name = 'some_template.html'

Теперь мы можем построить простую линейную диаграмму с помощью Chart.js, и мы не хотим использовать AJAX, создавать новые API и т.д.

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

<canvas id="chart"></canvas><script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script><script>window.onload = function () {  var data = [48, -63, 81, 11, 70];  var labels = ['January', 'February', 'March', 'April', 'May'];  var config = {    type: 'line',    data: {      labels: labels,      datasets: [        {          label: 'A random dataset',          backgroundColor: 'black',          borderColor: 'lightblue',          data: data,          fill: false        }      ]    },    options: {      responsive: true    }  };  var ctx = document.getElementById('chart').getContext('2d');  window.myLine = new Chart(ctx, config);};</script>

И получится следующее:

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

from django.views.generic import TemplateViewfrom some_project.some_app.models import SomeDataModelclass SomeTemplateView(TemplateView):    template_name = 'some_template.html'    def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        context['data'] = [            {                'id': obj.id,                'value': obj.value,                'date': obj.date.isoformat()            }            for obj in SomeDataModel.objects.all()        ]        return context

А затем мы визуализируем массив JavaScript с помощью шаблона Django:

<canvas id="chart"></canvas><script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script><script>window.onload = function () {  // We render via Django template  var data = [    {% for item in data %}      {{ item.value }},    {% endfor %}  ]  // We render via Django template  var labels = [    {% for item in data %}      "{{ item.date }}",    {% endfor %}  ]  console.log(data);  console.log(labels);  var config = {    type: 'line',    data: {      labels: labels,      datasets: [        {          label: 'A random dataset',          backgroundColor: 'black',          borderColor: 'lightblue',          data: data,          fill: false        }      ]    },    options: {      responsive: true    }  };  var ctx = document.getElementById('chart').getContext('2d');  window.myLine = new Chart(ctx, config);};</script>

Именно так это и работает, но, как по мне, слишком грязно. У нас больше нет JavaScript, но есть JavaScript с шаблоном Django. Мы теряем возможность выделить JavaScript в отдельный файл .js. Также мы не можем красиво работать с этим JavaScript.

Но можем сделать лучше и быстрее.

Добавим остроты

Стратегия следующая:

  1. В нашем случае мы будем сериализовать данные через json.dumps и хранить их в контексте.

  2. Отрендерим скрытый элемент <div> с уникальным id и атрибутом data-json, а именно с сериализованными данными JSON.

  3. Запросите этот <div> из JavaScript, прочитайте атрибут data-json и используйте JSON.parse, чтобы получить необходимые данные.

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

Почти как упрощенный AJAX.

Ниже пример того, как я использую эту стратегию:

import jsonfrom django.views.generic import TemplateViewclass SomeTemplateView(TemplateView):    template_name = 'some_template.html'    def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        context['data'] = json.dumps(            [                {                    'id': obj.id,                    'value': obj.value,                    'date': obj.date.isoformat()                }                for obj in SomeDataModel.objects.all()            ]        )        return context

Теперь извлечем наш код на JavaScript в статичный файл chart.js.

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

{% load static %}<div style="display: none" id="jsonData" data-json="{{ data }}"></div><canvas id="chart"></canvas><script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script><script src="{% static 'chart.js' %}"></script>

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

Атрибут data-json (который необязателен и не является чем-то предопределенным) содержит нужный нам JSON.

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

function loadJson(selector) {  return JSON.parse(document.querySelector(selector).getAttribute('data-json'));}

И вот наш chart.js готов:

function loadJson(selector) {  return JSON.parse(document.querySelector(selector).getAttribute('data-json'));}window.onload = function () {  var jsonData = loadJson('#jsonData');  var data = jsonData.map((item) => item.value);  var labels = jsonData.map((item) => item.date);  console.log(data);  console.log(labels);  var config = {    type: 'line',    data: {      labels: labels,      datasets: [        {          label: 'A random dataset',          backgroundColor: 'black',          borderColor: 'lightblue',          data: data,          fill: false        }      ]    },    options: {      responsive: true    }  };  var ctx = document.getElementById('chart').getContext('2d');  window.myLine = new Chart(ctx, config);};

Быстрый и грязный способ для тех, кто просто хочет что-то отправить. А вот и конечный результат:

Дисклеймер

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


Узнать подробнее о курсе "Web-разработчик на Python".

Посетить Demo day к курсу.

Подробнее..

Web-дизайнер кто он, сколько зарабатывает и как же на него выучиться?

15.01.2021 08:08:52 | Автор: admin

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

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

Кто такой web-дизайнер?

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

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

Направления web-дизайна

Формально, специалист работает в одном или двух одновременно направлениях, а именно:

  • UX этоUser Experience(дословно: опыт пользователя). То есть это то, какой опыт/впечатление получает пользователь от работы с вашим интерфейсом. Удается ли ему достичь цели и на сколько просто или сложно это сделать.

  • UI этоUser Interface(дословно пользовательский интерфейс) то, как выглядит интерфейс и то, какие физические характеристики приобретает. Определяет, какого цвета будет ваше изделие, удобно ли будет человеку попадать пальцем в кнопочки, читабельным ли будет текст и тому подобное

Сколько зарабатывает web-дизайнер?

По данным портала Trud средняя зарплата веб-дизайнера в России составляет $520 в месяц, что в переводе на рубли составляет 39300 руб. Самые высокие ставки в Москве в среднем около $1000 веб-дизайнер и $1500 UI-дизайнер.

По данным онлайн-университета Skillbox начинающий дизайнер в России может спокойно зарабатывать порядка 20000-40000 руб. но для этого так же следует иметь за плечами несколько проектов, а иногда и опыт в команде. Дизайнер с опытом работы за плечами и хорошим портфолио зарабатывает уже от 60000 руб. Ведущий специалист или арт-директор зарабатывают уже от 150000 руб. но не стоит забывать, что это все стоит не малых усилий. Главное помнить, что чем сложнее работа, тем больше денег выполучаете итем интереснее сама работа. Отдельно хочется сказать о UX-дизайнере, стоит лишь мельком пробежаться по вакансиям, и вы сразу понимаете, насколько выгодно работать специалистом в этой сфере, UX-дизайнер зарабатывает от 90000-250000 руб. спокойно.

hh.ruhh.ru

Сколько web-дизайнер зарабатывает на фрилансе?

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

fl.rufl.ru

Где же выучиться?

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

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

  • https://skillbox.ru/ это университет, который позволяет обучаться профессии в дистанционном формате. Skillbox предлагает освоить такие специализации за 1-2 года обучения, что довольно быстро. На первый доход от веб-дизайна абитуриенты могут рассчитывать уже спустя 4 месяца после обучения.

  • https://geekbrains.ru/ этообразовательная экосистема, в которой любой человек может получить всё для успешного профессионального.

  • https://wayup.in/ этообразовательная онлайн-платформа, основанная Андреем Гавриловым в 2014 году, на которой можно получить перспективную digital-профессию с гарантией.

Как устроиться на работу и как ее найти?

Для начинающих веб-дизайнеров существует два варианта заработать деньги с использованием полученных навыков. Один из них это трудоустройство в компанию, второй индивидуальная деятельность по оказанию услуг веб-дизайна. Необходимо регулярно мониторить объявления о вакансиях на специализированных сайтах: hh.ru, Rabota.ru, Zarplata.ru, Vakant.ru. Будьте готовы к тому, что работодатель будет предъявлять достаточно объемный перечень требований к соискателю: знание программ Photoshop, CorelDraw, знание HTML & CSS, умение разработки иллюстрация, иконок, умение создания графики, flesh-анимации.

Достаточно зарегистрироваться на биржах фриланса (FL.ru, Freelance.ru, Kwork.ru и др.) и выполнять предлагаемые задания. Но и на указанных сервисах начинающему веб дизайнеру не доверят дорогостоящий заказ. Нужно наработать определенный рейтинг и зарекомендовать себя как ответственного исполнителя, чтобы получить более или менее высокое вознаграждение за свой труд.


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

Подробнее..

Установка discourse в Ubuntu 16.04

19.01.2021 22:21:19 | Автор: admin
В статье рассматриваются установка discourse в среде разработки, затем в среде эксплуатации, запуск sidekiq и начальная настройка (кроме настройки электронной почты, необходимой для активации аккаутнов по е-мэйл и рассылки уведомлений, а также https).

Установка в среде разработки



1. Подключаемся к СУБД PostgreSQL с помощью psql -U postgres и создаем базу данных discourse_development и пользователя discourse_user, которому даем права доступа к этой базе данных.

create database discourse_development;create user discourse_user;alter user discourse_user with encrypted password 'your_preferred_password';alter database discourse_development owner to discourse_user;


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

\c discourse_development;create extension hstore;create extension pg_trgm;\q


2. Клонируем файлы discourse. Если у Вас версия PostgreSQL ниже 12 (psql --version), откатываем их к версии 2.4.0.beta11, которая вышла 13 февраля 2020 года (если я правильно прочитал git log).

Для этого. во-первых, есть команда

git clone https://github.com/discourse/discourse.git


Для отката к февральской версии вводим

git checkout 2136d4b5d535ca1fb83bd015502741d53301a61f


3. Устанавливаем гемы командой bundle install, предварительно удалив/переименовав Gemfile.lock

4. В config/database.yml добавляем значения username и password, а также строки encoding: utf8 и template: template0 и запускаем bundle exec rake db:migrate.

5. Запускаем веб-сервер для Rails командой

UNICORN_PORT=3002 bundle exec unicorn -c config/unicorn.conf.rb


6. Настраиваем обратный прокси-сервер nginx, добавляем в config/environments/development.rb строку config.hosts << "discourse.domain.name"


Скриншот 1. Содержимое файла /etc/nginx/sites-enabled/discourse.conf

Прим.1 Строки location /assets/ { и location /images/ { ... нужны для запуска в среде эксплуатации, для запуска в среде разработки их добавлять вообще-то еще рано.

Перезапускаем nginx командой /etc/init.d/nginx restart

7. Перезапускаем unicorn: для остановки вводим kill -QUIT `cat tmp/pids/unicorn.pid`, для повторного запуска вводим команду из п.5. Готово.

Установка в среде разработки



1. Создаем базу данных аналогичным образом, только имя базы данных указываем не discourse_development, а discourse.

2. Создаем файл config/discourse.conf командой

cp config/discourse_defaults.conf config/discourse.conf


Затем указываем в нем значения db_name, db_username, db_password, а также hostname (discourse, discourse_user, your_preferred_password, discourse.domain.name соответственно).

3. Устанавливаем необходимые пакеты командой

sudo apt install optipng pngquant jhead jpegoptim gifsicle 


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

RAILS_ENV=production bundle exec rake db:migrate 


4. Устанавливаем еще один необходимый для следующей команды пакет с помощью

sudo apt install brotli


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

RAILS_ENV=production bundle exec rake assets:precompile


5. Добавляем строки location /assets/ { ... и location /images/ { ... (см. скриншот 1) в конфигурационный файл nginx, если их там еще нет и перезапускаем nginx.

6. Остановка unicorn (см. команду выше) и запуск его в среде эксплуатации командой

RAILS_ENV=production UNICORN_PORT=3002 bundle exec unicorn -c config/unicorn.conf.rb


Запуск sidekiq



1. Создаем учетную запись администратора командой

RAILS_ENV=production bundle exec rake admin:create


и перезапускаем unicorn.

2. Для запуска sidekiq в файле config/sidekiq.yml копируем строки конфигурации для среды разработки для среды эксплуатации (см. скриншот 2) и добавляем в config/environments/production.rb строку (в случае когда в ОС установлена Redis 3.0.6)

Redis.exists_returns_integer = false


Скриншот 2.

После этого запускаем sidekiq командой

bundle exec sidekiq -C config/sidekiq.yml


3. Проверяем существование запущенного процесса sidekiq командой

ps aux | grep sidekiq


Начальная настройка



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


Скриншот 3. После авторизации в discourse

Используемая при написании статьи инструкция:

Install Discourse Forum Software on Ubuntu 18.04 Without Docker
Подробнее..

Категории

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

  • Имя: Макс
    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