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

Разработка веб-сайтов

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

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



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


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

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

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

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


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

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


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

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


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


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

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


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

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


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


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

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


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


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

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


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

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

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

Файл index.js:

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

Файл index2.js:

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

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


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

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

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

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

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


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

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

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

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


Подробнее..

Перевод Стриминг видео с помощью Akka Streams

12.06.2021 18:20:44 | Автор: admin
Автор статьи, перевод которой мы сегодня публикуем, говорит, что стриминг видео не должен быть такой задачей, с которой у кого-либо возникают сложности. Всё дело в правильном подборе инструментов, среди которых можно отметить пакет Akka Streams. Использование этого пакета позволяет эффективно разрабатывать приложения для потоковой передачи видео.



Правда, не следует думать, что то, о чём мы будем тут говорить, подобно простому примеру, вроде println(Hello world), в котором используется система акторов Akka. Сегодня вы узнаете о том, как создать свой первый сервис для потоковой передачи видео (прошу прощения, если моё предположение неверно, и у вас это уже не первый такой проект). В частности, тут будут использованы пакеты Akka HTTP и Akka Streams, с помощью которых мы создадим REST API, который обладает способностями стриминга видеофайлов в формате MP4. При этом устроен этот API будет так, чтобы то, что он выдаёт, соответствовало бы ожиданиям HTML5-тега <video>. Кроме того, тут я скажу несколько слов о наборе инструментов Akka в целом, и о некоторых его компонентах, вроде Akka Streams. Это даст вам определённый объём теории, которая пригодится вам в работе. Но, прежде чем мы приступим к делу, хочу задать один вопрос.

Почему я решил рассказать о стриминге видео?


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

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

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

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

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

Что такое набор инструментов Akka? Удобно ли им пользоваться?


Akka это опенсорсный набор инструментов, который нацелен на упрощение разработки многопоточных и распределённых приложений, и, кроме того, даёт программисту среду выполнения для подобных приложений. Системы, которые базируются на Akka, обычно очень и очень хорошо масштабируются. Проект Akka основан на модели акторов, в нём используется система конкурентного выполнения кода, основанная на акторах. Создатели этого проекта многое почерпнули из Erlang. Инструменты Akka написаны на Scala, но в проекте имеются DSL и для Scala, и для Java.

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

Основной объём кода, который мы будем тут рассматривать, будет использовать пакеты HTTP и Streams. Мы практически не будем пользоваться стандартным пакетом Akka Actors.

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

Что такое Akka Streams?


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

Для нас важен тот факт, что в Akka Streams есть встроенный механизм back-pressure (обратное давление). Благодаря этому решается одна из самых сложных проблем мира стриминговой передачи данных. Это настройка правильной реакции поставщика данных на работу в условиях, когда потребитель данных не может справиться с нагрузкой. И эту проблему решает инструмент, которым мы будем пользоваться, а нам остаётся лишь научиться работать с этим инструментом, не вдаваясь в какие-то чрезмерно сложные и запутанные темы.

Пакет Akka Streams, кроме того, даёт в наше распоряжение API, который совместим с интерфейсами, необходимыми для работы с Reactive Streams SPI. И, между прочим, стоит отметить, что сам проект Akka входит в число основателей инициативы Reactive Streams.

Про Akka Streams мы поговорили. Поэтому можем переходить к нашей следующей теоретической теме.

Что такое Akka HTTP?


Akka HTTP это, как и Akka Streams, пакет, входящий в набор инструментов Akka. Этот пакет основан на пакетах Akka Streams и Akka Actors. Он направлен на то, чтобы упростить работу приложений, в которых используются инструменты Akka, с внешним миром по протоколу HTTP. Этот пакет поддерживает и серверные, и клиентские возможности. Поэтому с его помощью можно создавать и REST API, и HTTP-клиентов, которые отправляют запросы к неким внешним сервисам.

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

HTML5-тег <video>


Тег <video> это новый элемент, который появился в HTML5. Он создавался как замена Adobe Video Player. Несложно понять, что главная задача этого HTML-элемента заключается в предоставлении разработчикам возможностей встраивания в HTML-документы медиаплееров, способных воспроизводить видеофайлы. Собственно, этот тег очень похож на <img>.

В теге <video> размещают тег <source>, имеющий два важных атрибута. Первый это src, который используется для хранения ссылки на видео, которое нужно воспроизвести. Второй это type, который содержит сведения о формате видео.

Между открывающим и закрывающим тегом <video> </video> можно ввести какой-то текст, который будет использован в роли текста, выводимого вместо элемента <video> в тех случаях, когда этот элемент не поддерживается браузером. Но в наши дни тег <video> поддерживает даже Internet Explorer, поэтому вероятность возникновения ситуации, в которой может понадобиться подобный текст, стремится к нулю.

Как будет работать стриминг с использованием тега и FileIO?</font></h2>Хотя на первый взгляд кажется, что стриминг с использованием тега <video> и FileIO это нечто архисложное, если в этот вопрос немного вникнуть, окажется, что ничего особенного в нём нет. Дело в том, что то, о чём идёт речь, представлено готовыми блоками, из которых нужно лишь собрать то, что нам нужно.На стороне сервера основная нагрузка ложится на объект <code>FileIO</code>. Он будет генерировать события, содержащие фрагменты файла, потоковую передачу которого мы осуществляем. Конечно, размеры этих фрагментов поддаются настройке. И, более того, настроить можно и позицию, начиная с которой будет осуществляться стриминг файла. То есть видео необязательно смотреть сначала его можно начинать смотреть с любого места, интересующего пользователя. Всё это отлично сочетается с возможностями тега <code><video></code>, выполняющего запрос HTTP GET с заголовком <code>Range</code> для того чтобы включить воспроизведение видео без его предварительной загрузки.Вот пара примеров запросов, выполняемых элементом <code><video></code>:<img src="http://personeltest.ru/aways/lh3.googleusercontent.com/Dfgd_RjgpV9W3jzBF9TVNLT_n8cAf5_VfvhxbfeK5wYH5slwUtXhjQ0FlZ0I79gCWsNgIF1Q3q7nPty5HqpCpgXIYrEtjQINuyMaMHe1nWu228-p8o9Hf3n-cHNZN_5iS0QGktk" align="center"><img src="http://personeltest.ru/aways/lh3.googleusercontent.com/SQPBs8xy8HVsT8cw2_WRoOnQW1dqKRp2WVSAzfFsbCBkSmETbm7NKS8zMLBdwcWdIo1AQDO_tSJJw1_2Tcd_oMoaBzdTwDz62Tq2jtzKX0ujmuCEqo99NKH_YLnlL6FQbZ5UOd0" align="center">Если кому интересно заголовок <code>Range:bytes=x-</code> отвечает за выбор позиции, с которой начинается воспроизведение видео. Первый запрос уходит на сервер в начале воспроизведения видео, а второй может быть отправлен тогда, когда пользователь решит куда-нибудь перемотать видео.Сейчас, после довольно-таки длинного вступления, пришло время заняться кодом.<h2><font color="#3AC1EF">Пишем стриминговый сервис</font></h2>В нескольких следующих разделах я расскажу о реализации серверной части нашего стримингового сервиса. А потом мы создадим простую HTML-страницу, с помощью которой проверим правильность работы этого сервиса.Я люблю делать предположения, поэтому сделаю ещё одно, которое заключается в том, что я ожидают, что тот, кто это читает, владеет основами Scala и SBT.<h3><font color="#3AC1EF">1</font></h3>Добавим в файл <code>build.sbt</code> необходимые зависимости. В этом проекте нам понадобится 3 пакета: <code>akka-http</code>, <code>akka-actor-typed</code> (пакета <code>akka-actor</code>, в теории, достаточно, но никогда нельзя забывать о типобезопасности) и <code>akka-stream</code>.<source lang="scala">libraryDependencies ++= Seq("com.typesafe.akka" %% "akka-actor-typed" % "2.6.14","com.typesafe.akka" %% "akka-stream" % "2.6.14","com.typesafe.akka" %% "akka-http" % "10.2.4")</source><h3><font color="#3AC1EF">2</font></h3>Теперь можно создать главный класс, ответственный за запуск приложения. Я решил расширить класс <code>App</code>. Мне кажется, что это удобнее, чем создавать метод <code>main</code>. На следующем шаге мы поместим сюда код, имеющий отношение к созданию системы акторов и HTTP-сервера.<source lang="scala">object Runner extends App {}</source><h3><font color="#3AC1EF">3</font></h3>После создания главного класса мы можем добавить в него код, о котором говорили выше.<source lang="scala">object Runner extends App {val (host, port) = ("localhost", 8090)implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "akka-video-stream")Http().newServerAt(host, port)}</source>Сейчас нас вполне устроит такая конфигурация. На последнем шаге мы добавим в код вызов метода <code>bind</code>, что позволит открыть доступ к нашему REST API. Тут мы создаём объект <code>ActorSystem</code> с именем <code>akka-video-stream</code> и HTTP-сервер, прослушивающий порт <code>8090</code> на локальном компьютере. Не забудьте о ключевом слове <code>implicit</code> в определении системы акторов, так как подобный неявный параметр необходим в сигнатуре метода <code>Http</code>.<h3><font color="#3AC1EF">4</font></h3>А тут мы, наконец, реализуем конечную точку REST API, используемую для обработки запросов от тега <code><video></code>.<source lang="scala">object Streamer {val route: Route =path("api" / "files" / "default") {get {optionalHeaderValueByName("Range") {case None =>complete(StatusCodes.RangeNotSatisfiable)case Some(range) => complete(HttpResponse(StatusCodes.OK))}}}}</source>Как видите, я создал конечную точку с URL <code>api/files/default/</code>. В её коде проверяется, есть ли в запросе заголовок <code>Range</code>. Если он содержит корректные данные сервер возвращает ответ с кодом <code>200</code> (<code>OK</code>). В противном случае возвращается ответ с кодом <code>416</code> (<code>Range Not Satisfiable</code>).<h3><font color="#3AC1EF">5</font></h3>Пятый шаг нашей работы отлично подходит для реализации метода, ради которого, собственно, и была написана эта статья.<source lang="scala">private def stream(rangeHeader: String): HttpResponse = {val path = "path/to/file"val file = new File(path)val fileSize = file.length()val range = rangeHeader.split("=")(1).split("-")val start = range(0).toIntval end = fileSize - 1val headers = List(RawHeader("Content-Range", s"bytes ${start}-${end}/${fileSize}"),RawHeader("Accept-Ranges", s"bytes"))val fileSource: Source[ByteString, Future[IOResult]] = FileIO.fromPath(file.toPath, 1024, start)val responseEntity = HttpEntity(MediaTypes.`video/mp4`, fileSource)HttpResponse(StatusCodes.PartialContent, headers, responseEntity)}</source>Тут я сделал следующее:<ul><li>Загрузил файл, потоковую передачу которого я хочу организовать, а затем, учитывая заголовок из запроса, и данные о файле, нашёл позицию в файле, с которой начнётся стриминг, а так же сформировал заголовок <code>Content Range</code>.</li><li>С помощью <code>FileIO</code> создал поток из ранее загруженного файла. Затем я использовал этот поток в роли данных в <code>HttpEntity</code>.</li><li>Я создал ответ, <code>HttpResponse</code>, с кодом <code>206</code> (<code>Partial Content</code>), с соответствующими заголовками и с телом в виде <code>responseEntity</code>.</li></ul>Ещё мне хочется подробнее поговорить о <code>FileIO</code>, так как это самый удивительный механизм во всей статье. Что, на самом деле, происходит при выполнении строки <code>FileIO.fromPath(file.toPath, 1024, start)</code>?Тут, из содержимого файла, находящегося по заданному пути, создаётся объект <code>Source</code> (Akka-аналог Producer из Reactive Streams). Каждый элемент, выдаваемый этим объектом, имеет размер, в точности равный 1 Мб. Первый элемент будет взят из позиции, указанной в параметре <code>start</code>. Поэтому, если в <code>start </code>будет 0 первый элемент окажется первым мегабайтом файла.<h3><font color="#3AC1EF">6</font></h3>Мы уже реализовали основную логику серверной части приложения. А сейчас нам надо отрефакторить её код для того чтобы нашим сервером можно было бы пользоваться.Начнём с внесения изменений в определение REST API:<source>complete(HttpResponse(StatusCodes.Ok)) => complete(stream(range))</source>Получается, что вместо того, чтобы просто возвращать <code>OK</code>, мы вызываем метод <code>stream</code> с передачей ему параметра <code>range</code> и начинаем стриминг.Нельзя забывать о том, что наш API всё ещё недоступен для внешнего мира. Поэтому нам нужно модифицировать соответствующий фрагмент кода, ответственный за запуск HTTP-сервера:<source>Http().newServerAt(host, port) =>Http().newServerAt(host, port).bind(Streamer.route)</source>Готово! Теперь у нас есть рабочий бэкенд, а наш REST API ждёт подключений от любых программ, которым он нужен. Осталось лишь всё протестировать.<h2><font color="#3AC1EF">Тестирование стримингового сервиса</font></h2>Мы, чтобы протестировать приложение, создадим простую HTTP-страницу, единственным достойным внимания элементом которой будет тег <code><video></code>. Причём, обо всём, что надо знать для понимания работы этой страницы, мы уже говорили. Поэтому я просто приведу ниже полный код соответствующего HTML-документа.То, что вы в итоге увидите в окне браузера, открыв эту страницу, должно, более или менее, напоминать то, что я покажу ниже. Конечно, ваш стриминговый сервис вполне может передавать не тот видеофайл, который использовал я.<source lang="html"><!DOCTYPE html><html lang="en"><head><title>Akka streaming example</title></head><body style="background: black"><div style="display: flex; justify-content: center"><video width="1280" height="960" muted autoplay controls><source src="http://personeltest.ru/away/localhost:8090/api/files/default" type="video/mp4">Update your browser to one from XXI century




Тут мне хотелось бы обратить ваше внимание на 5 важных вещей:

  1. Я использовал возможности тега <source> вместо использования соответствующих атрибутов тега <video>.
  2. В атрибуте src тега <source> я указал путь, при обращении к которому бэкенд начнёт потоковую передачу видеофайла.
  3. В атрибуте type тега <source> я указал тип файла.
  4. Я добавил к тегу <video> атрибуты autoplay и muted для того чтобы видео начинало бы воспроизводиться автоматически.
  5. К тегу <video> я добавил и атрибут controls, благодаря чему будут выводиться элементы управления видеоплеера, встроенного в страницу.

На элемент <div> можете особого внимания не обращать. Он тут нужен лишь для стилизации плеера.

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


Правильная работа стримингового сервиса

Обратите внимание на то, что автоматическое воспроизведение видео не начнётся до тех пор, пока к тегу <video> не будут добавлены атрибуты muted и autoplay. Если не оснастить тег <video> этими атрибутами воспроизведение придётся включать вручную, нажимая на соответствующую кнопку.

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

Что можно улучшить?


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

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

Итоги


Сегодня я постарался доказать то, что реализация простого стримингового сервиса это, при условии использования правильных инструментов, проще, чем кажется. Использование инструментов Akka и подходящего HTML-тега способно значительно сократить объём работы. Правда, не забывайте о том, что тут показан очень простой пример. Для реализации реального стримингового сервиса этого может быть недостаточно.

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

Вот GitHub-репозиторий с кодом проекта.

Какие инструменты вы выбрали бы для создания собственного стримингового сервиса?


Подробнее..

Перевод Разработка REST-серверов на Go. Часть 3 использование веб-фреймворка Gin

17.06.2021 16:13:58 | Автор: admin
Сегодня, в третьей части серии материалов, посвящённых разработке серверов на Go, мы займёмся реализацией нашего REST-сервера с использованием Gin одного из самых популярных веб-фреймворков для Go. Вот код, который мы будем тут обсуждать.



Выбор веб-фреймворка


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

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

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

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

Маршрутизация и Gin


Наша функция main настраивает новый маршрутизатор Gin и регистрирует маршруты:

router := gin.Default()server := NewTaskServer()router.POST("/task/", server.createTaskHandler)router.GET("/task/", server.getAllTasksHandler)router.DELETE("/task/", server.deleteAllTasksHandler)router.GET("/task/:id", server.getTaskHandler)router.DELETE("/task/:id", server.deleteTaskHandler)router.GET("/tag/:tag", server.tagHandler)router.GET("/due/:year/:month/:day", server.dueHandler)

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

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

  1. Вместо указания HTTP-метода в виде дополнительного (Go) вызова метода в маршруте, метод закодирован в имени функции, используемой для регистрации маршрута. Например тут используется конструкция вида router.POST, а не что-то вроде router.HandleFunc(...).Methods(POST).
  2. Gorilla поддерживает обработку запросов с использованием регулярных выражений. А Gin нет. К этому ограничению мы ещё вернёмся.

Обработчики запросов


Посмотрим на код обработчиков запросов, используемых при применении Gin. Начнём с самых простых, в частности с getAllTasksHandler:

func (ts *taskServer) getAllTasksHandler(c *gin.Context) {allTasks := ts.store.GetAllTasks()c.JSON(http.StatusOK, allTasks)}

Тут стоит обратить внимание на несколько интересных моментов:

  1. У обработчиков, используемых в Gin, нет стандартных сигнатур HTTP-обработчиков Go. Они просто принимают объект gin.Context, который может быть использован для анализа запроса и для формирования ответа. Но в Gin есть механизмы для взаимодействия со стандартными обработчиками вспомогательные функции gin.WrapF и gin.WrapH.
  2. В отличие от ранней версии нашего сервера, тут нет нужды вручную писать в журнал сведения о запросах, так как стандартный механизм логирования Gin, представленный ПО промежуточного уровня, сам решает эту задачу (и делается это с использованием всяческих полезных мелочей, вроде оформления вывода разными цветами и включения в журнал сведений о времени обработки запросов).
  3. Нам, кроме того, больше не нужно самостоятельно реализовывать вспомогательную функцию renderJSON, так как в Gin есть собственный механизм Context.JSON, который позволяет формировать JSON-ответы.

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

func (ts *taskServer) getTaskHandler(c *gin.Context) {id, err := strconv.Atoi(c.Params.ByName("id"))if err != nil {c.String(http.StatusBadRequest, err.Error())return}task, err := ts.store.GetTask(id)if err != nil {c.String(http.StatusNotFound, err.Error())return}c.JSON(http.StatusOK, task)}

Тут особенно интересно выглядит обработка параметров. Gin позволяет обращаться к параметрам маршрута (к тому, что начинается с двоеточия, вроде :id) через Context.Params.

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

Привязка данных запросов


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

func (ts *taskServer) createTaskHandler(c *gin.Context) {type RequestTask struct {Text string  `json:"text"`Tags []string `json:"tags"`Due time.Time `json:"due"`}var rt RequestTaskif err := c.ShouldBindJSON(&rt); err != nil {c.String(http.StatusBadRequest, err.Error())}id := ts.store.CreateTask(rt.Text, rt.Tags, rt.Due)c.JSON(http.StatusOK, gin.H{"Id": id})}

В Gin имеется серьёзная инфраструктура для организации привязки запросов к структурам данных Go, содержащих данные из запросов. Тут под привязкой понимается обработка содержимого запросов (которое может быть представлено данными в различных форматах, например JSON и YAML), проверка полученных данных и запись соответствующих значений в структуры Go. Здесь мы пользуемся весьма примитивной формой привязки данных для RequestTask, где проверка данных не используется. Но, полагаю, нам стоит знать не только о базовых, но и о более продвинутых возможностях Gin.

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

Ещё внимание обратить стоит на то, что теперь нам не нужно пользоваться одноразовой структурой для ID ответа. Вместо этого мы используем gin.H псевдоним для map[string]interface{}; это очень просто, но, всё же, позволяет весьма эффективно конструировать ответы, используя совсем небольшие объёмы кода.

Дополнительные возможности Gin


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

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

Ограничения фреймворков


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

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

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

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

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

Каким фреймворком вы воспользовались бы при разработке сервера на Go?


Подробнее..

Перевод Оптимизация веб-графики в 2021 году

20.06.2021 18:15:44 | Автор: admin
Изображения, используемые на веб-страницах, привлекают пользователей, пользователи довольно-таки охотно щёлкают по ним мышью. Изображения делают веб-страницы лучше во всём кроме скорости работы страниц. Изображения это огромные куски байтов, которые обычно являются теми частями сайтов, которые загружаются медленнее всего. В этом материале я собрал всё, что нужно знать в 2021 году об улучшении скорости работы веб-страниц через оптимизацию работы с изображениями.



Изображения обычно имеют большие размеры. Даже очень большие. В большинстве случаев CSS- и JavaScript-ресурсы, необходимые для обеспечения работоспособности страниц это мелочь в сравнении с тем объёмом данных, который нужно передать по сети для загрузки изображений, используемых на страницах. Медленные изображения могут повредить показателям Core Web Vitals сайта, могут оказать воздействие на SEO и потребовать дополнительных затрат на трафик. Изображения это обычно тот самый ресурс сайта, который оказывает решающее воздействие на показатель Largest Contentful Paint (LCP) и на задержки загрузки сайта. Они способны увеличить показатель Cumulative Layout Shift (CLS). Если вы не знакомы с этими показателями производительности сайтов почитайте о них в Definitive Guide to Measuring Web Performance.

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

1. Формат изображений


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

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


Изображения ленивца

Слева мы можем видеть фото нашего товарища-ленивца Сэма. Эта картинка в формате JPG занимает всего лишь 32,7 Кб. А если то же самое изображение преобразовать в формат PNG размер графического файла увеличится более чем вдвое до 90,6 Кб!

Справа находится рисунок со всё тем же Сэмом. Этот рисунок лучше всего хранить в формате PNG. Так он занимает всего 5,5 Кб. А если преобразовать его в JPG, то его размер подскочит до 11,3 Кб.

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

Существует, конечно, ещё много графических форматов! Если у вас имеется некое векторное изображение (состоящее из всяческих линий и геометрических фигур), то вам лучше всего подойдёт формат SVG. Более новые браузеры поддерживают и более современные графические форматы вроде AVIF и WebP. Их использование для хранения подходящих изображений позволяет добиться ещё более серьёзного уменьшения размеров графических файлов.

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


Не все посетители сайта будут просматривать его в одних и тех же условиях. У кого-то имеется огромный монитор шириной в 1600 пикселей. А кто-то смотрит сайт на планшете с шириной экрана в 900 пикселей, или на телефоне с экраном шириной в 600 пикселей. Если на сайте применяется изображение шириной в 1200 пикселей это будет означать, что при просмотре такого сайта на устройствах с небольшими экранами сетевые и другие ресурсы будут тратиться впустую, так как размер таких изображений при выводе на экран, всё равно, будет уменьшен.


Просмотр сайта на устройствах с разными экранами

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

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 900px) 100vw, 1200px"alt="my awesome picture" height="900" width="1200" />

В данном случае ширина базового изображения составляет 1200 пикселей. Оно, кроме того, является изображением, записанным в атрибут src тега и используемым по умолчанию. В srcset описаны 3 варианта изображения шириной в 600, 900 и 1200 пикселей. В sizes используются медиа-запросы CSS, позволяющие дать браузеру подсказку, касающуюся видимой области, доступной для вывода изображения. Если ширина окна меньше 900 пикселей место, где будет выведено изображение, займёт всю его ширину 100vw. В противном случае место для вывода изображения никогда не окажется шире 1200 пикселей.

Большинство инструментов для работы с изображениями, вроде Photoshop, Gimp и Paint.NET, умеют экспортировать изображения в различных размерах. Стандартные системные графические программы тоже, в определённых пределах, способны решать подобные задачи. А если надо автоматизировать обработку очень большого количества изображений возможно, есть смысл взглянуть на соответствующие инструменты командной строки вроде ImageMagick.

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


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

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 600px) 0, 600px"alt="my awesome picture" height="900" width="1200" />

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

3. Качество изображений


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


Исходное PNG-изображение с прозрачными участками имеет размер 57 Кб. Такое же изображение, но сжатое, имеет размер 15 Кб.

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

4. Встраивание изображений в веб-страницы


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

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


Изображение, встроенное в страницу

Может, выглядит это и странновато, но тут перед нами так называемый Data URL. Такие URL пользуются полной поддержкой всех браузеров. В атрибуте src сказано, что соответствующие данные надо воспринимать как PNG-изображение в кодировке base64. После описания изображения идёт набор символов, представляющих содержимое этого изображения. В данном случае это маленькая красная точка.

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

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

А вот удобный веб-инструмент для преобразования изображений в формат base64.

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


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

Вместо того, чтобы заставлять браузер сразу загружать все изображения, можно посоветовать ему немного полениться. Ленивая загрузка изображений это такой подход к работе с изображениями, когда браузеру предлагают загружать изображения только тогда, когда они могут понадобиться пользователю. Применение ленивой загрузки изображений способно оказать огромное положительное влияние на показатель Largest Contentful Paint (LCP), так как благодаря этому браузер, при загрузке страницы, может уделить основное внимание только самым важным изображениям.

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

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

var lazyEls = [].slice.call(document.querySelectorAll("[data-src]"));var lazyObserver = new IntersectionObserver(function(entries) {entries.forEach(function(entry) {if (entry.isIntersecting) {var el = entry.target;var src = el.getAttribute("data-src");if (src) { el.setAttribute("src", src); }lazyObserver.unobserve(el);}});});lazyEls.forEach(function(el) {lazyObserver.observe(el);});

Тут, для определения того момента, когда надо загружать изображение, используется объект IntersectionObserver. Когда наступает нужный момент содержимое атрибута data-src копируется в атрибут src и изображение загружается. Тот же подход можно применить к атрибуту srcset и воспользоваться им при работе с любым количеством изображений.

Пользуются этим, переименовывая атрибут src в data-src.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy" />

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

Настройка размеров области, которую займёт изображение


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

Избежать сдвига макета страницы можно, указав атрибуты height и width тега <img>.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy"width="1200" height="900" />

Обратите на то, что значения атрибутов height и width это не 1200px и 900px. Это просто 1200 и 900. И работают они немного не так, как можно было бы ожидать. Размер соответствующего изображения не обязательно будет составлять 1200x900 пикселей. Этот размер будет зависеть от CSS и от размеров макета страницы. Но браузер, благодаря этим атрибутам, получит сведения о соотношении сторон изображения. В результате, узнав ширину изображения, браузер сможет правильно настроить его высоту.

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

Итоги


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

Как вы оптимизируете изображения, используемые в ваших веб-проектах?


Подробнее..

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

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

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

Предыстория

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

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

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

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

Поехали

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробнее..

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

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

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

Предыстория

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

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

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

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

Поехали

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробнее..

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

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


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


Медиа


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


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


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





CSS


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

JavaScript


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





Браузеры


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Валидация

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

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

Подробнее..

Перевод Пробелы бывают разные ampnbsp C2A0

18.06.2021 16:08:17 | Автор: admin
image

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

Эта простая на первый взгляд проблема бросала вызов всем моим попыткам ее объяснить. Я придумал множество замечательных теорий: проблемы с моими классами CSS или с полями и отступами. Несоответствующие теги HTML. Ошибки браузера. Я попробовал три разных браузера и во всех получил одинаковые результаты.

Чувствуя себя сбитым с толку, я снова посмотрел на два раздела HTML в редакторе WordPress (текстовое представление) и подтвердил, что они полностью идентичны. Затем я попробовал встроенные в Firefox инструменты веб-разработчика для просмотра отображаемых элементов страницы и сравнил все их свойства CSS. Идентичны, но каким-то образом визуализированы по-разному. Я использовал инструменты разработчика, чтобы проверить точный HTML, полученный с моего веб-сервера, снова проверил два раздела и убедился, что они символьно идентичны. Инструмент Firefox источник страницы также подтвердил, что эти два раздела полностью идентичны.

К этому моменту я был готов обвинить космические лучи или магию вуду. Я обнаружил, что каждый раз, когда я копирую любой похожий раздел HTML, только что вставленный раздел будет отображаться в браузере с неправильным интервалом между элементами. Как такое могло быть? Затем я попробовал W3C Validator, который обнаружил некоторые другие проблемы с моей страницей, но ничего не могло объяснить такое поведение. И снова он подтвердил, что, несмотря на разную визуализацию в браузере, два раздела HTML идентичны.

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

Что за черт.

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

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

Я удивлен, сколько разных инструментов не смогли выявить это тонкое, но важное различие между типами пробелов в исходном HTML-коде. Редактор HTML WordPress не смог показать или правильно обработать разницу. Сбой инструментов веб-разработчика Firefox и инструментов источника страниц. Ошибка исходного представления валидатора W3C. Curl плюс шестнадцатеричный редактор был единственным способом окончательно установить достоверную информацию о точном содержании исходного кода HTML.
Подробнее..

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

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


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


Медиа


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


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


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



CSS


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

JavaScript


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







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

Как я попал на стажировку в Яндекс

18.06.2021 00:22:38 | Автор: admin

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

Сначала моя история о том, как я заинтересовался it сферой и в частности web разработкой.

Знакомство с кодом

Впервые с программным кодом я познакомился на третьем курсе института по специальности прикладная лингвистика. Это был язык программирования python и мне очень понравилось то, как я могу заставить компьютер делать разные классные штуки с помощью циклов, условных конструкций, функций и переменных. Преподаватель был современный и за основу учебной программы взял курс по основам программирования с учебной платформы udacity. В процессе нужно было написать простенький web crawler и небольшой поисковый движок для ранжирования веб страниц по количеству ссылок на них с других веб страниц. По определённым обстоятельствам я вынужден был уйти из вуза после третьего курса и стал зарабатывать на жизнь переводами.

Глубже в технологии

Тем временем я понял, что мне также интересна it сфера и я начал думать, как бы мне продолжить её осваивать и что выучить в первую очередь. Уже не помню почему, но выбор пал на курсеровский курс HTML, CSS and JavaScript Гонконгского университета. Так я впервые познакомился с языком гипертекстовой разметки, каскадными таблицами стилей и языком программирования javaScript. Мне показалось интересным манипулировать различными объектами на web странице, менять стили, расположение элементов и добавлять разную интерактивность. Затем было много разных других курсов с курсеры, edx, степика, главным курсом первого времени был наверное знамений курс Гарвардского университета cs50 на котором я впервые познакомился с алгоритмами и структурами данных и языком си. Это был очень непростой, но интересный курс по основам computer science.

Первые попытки заработать на разработке

Долгое время программирование было для меня эдаким хобби и я несильно видел себя в коммерческой разработке, я проходил курсы, делал разные учебные и личные проекты, изучал разные языки программирования (Haskell, java, golang, scheme etc) и парадигмы, в общем всё это было больше в интерес. В какой-то момент я решил попробовать сделать пару заказов на фрилансе, связанных с веб разработкой и мне удалось заработать первые деньги. После этого я начал помимо переводческой деятельности подрабатывать и веб разработкой, делал простые вещи: правки в вёрстке, подвязка сайта к crm через ajax, калькуляторы стоимостей услуг и товаров и всё в таком духе.

Смена профессии

После этого я начал задумываться о том, что нужно сменить таки профессию и стать полноценным разработчиком, специализироваться я решил на javaScript по причине его гибкости и возможности программировать во всех возможных парадигмах, а также его присутствию, как на клиенте, так и на сервере. Нужно было освоить какой-нибудь фреймворк, научиться тестировать код, верстать адаптивно, кроссбраузерно, валидно и семантично, освоить препроцессор для css, node js, typescript, webpack. Если до этого моё изучение программирования было в большей степени академичным, то теперь я решил взяться за дело всерьёз. Выбор пал на react за счёт того, что это javaScript first библиотека с минимумом магии и функциональным подход в построении интерфейсов ui, как чистая функция от состояния и свойств, переданных компоненту. На данный момент я также знаю на базовом уровне vue js, который меня восхитил своей магией и скоростью разработки, но опечалил отладкой и поиском ошибок, когда что-то ломается в шаблоне.

Хочу в крутую компанию!

Теперь перейду уже к стажировке. Я понимал, что в 25+ устроиться без опыта работы в крутую it компанию будет довольно непросто и уже пытался до этого попасть в школу программистов Хэдхантера и курсы от Тинькофф банка, после которых можно было попасть в штат, но там были очень сложные алгоритмические задачи, с которыми я не мог справиться полностью, обычно решая половину задач или меньше. В мэйл дорога была заказана сразу, потому что на свои стажировки они берут только выпускников своих образовательных программ. Про Яндекс я почему-то всё это время даже не думал, потому что считал, что там всё будет ещё гораздо страшнее. Но месяц назад в телеграм канале одного хорошего ютубера (S0ER) я наткнулся на пост о стажировке в Яндекс и подумал ну а почему бы и не попробовать, я вообще ни на что не расcчитывал и заполнил анкету указав честно все свои скромные достижения в виде одной курсовой, пачки сертификатов и резюме с указанием технологий, которые я освоил на тот момент.

Письмо счастья

В ответ мне пришла ссылка на контест. Я не буду говорить о том какие там были задания, но я был приятно удивлён тому, что на алгоритмы там была ровно одна задача из четырёх и именно её я завалил, моё решение прошло только половину тестов, из остальных там было задание на вёрстку, оно было довольно жёстким, нужно было pixel perfect сверстать определённый рисунок, состоящий из геометрических фигур без использования svg и готовых картинок, только html и css. Две оставшиеся задачи проверяли базовые вещи для javaScript разработчика: асинхронность, контекст вызова, прототипы, замыкания. Надо отметить, что каким-то образом я умудрился все три этих задания сдать с первой попытки в контесте. На всё это было дано 6 часов.

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

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

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

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

Это были команды картинок, лавки и маркета.

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

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

Подробнее..

Идеальный инструмент для создания прогрессивных веб-приложений или Все, что вы хотели знать о Workbox. Часть 2

21.06.2021 12:17:59 | Автор: admin

image


Что такое Workbox?


Workbox (далее WB) это библиотека (точнее, набор библиотек), основной целью которой является "предоставление лучших практик и избавление от шаблонного кода при работе с сервис-воркерами" (далее СВ).


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



WB предоставляет следующие возможности:


  • предварительное кэширование
  • кэширование во время выполнения
  • стратегии (кэширования)
  • обработка (перехват сетевых) запросов
  • фоновая синхронизация
  • помощь в отладке

Это вторая часть руководства. Вот ссылка на первую часть.


Модули, предоставляемые WB


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


  • workbox-background-sync: фоновая синхронизация, позволяющая выполнять сетевые запросы в режиме офлайн
  • workbox-broadcast-update: отправка уведомлений об обновлении кэша (через Broadcast Channel API)
  • workbox-cacheable-response: фильтрация кэшируемых запросов на основе статус-кодов или заголовков ответов
  • workbox-core: изменение уровня логгирования и названий кэша. Содержит общий код, используемый другими модулями
  • workbox-expiration: установка лимита записей в кэше и времени жизни сохраненных ресурсов
  • workbox-google-analytics: фиксация действий пользователей на странице в режиме офлайн
  • workbox-navigation-preload: предварительная загрузка запросов, связанных с навигацией
  • workbox-precaching: предварительное кэширование ресурсов и управление их обновлением
  • workbox-range-request: поддержка частичных ответов
  • workbox-recipes: общие паттерны использования WB
  • workbox-routing: обработка запросов с помощью встроенных стратегий кэширования или колбэков
  • workbox-strategies: стратегии кэширования во время выполнения, как правило, используемые совместно с workbox-routing
  • workbox-streams: формирование ответа на основе нескольких источников потоковой передачи данных
  • workbox-window: регистрация, управление обновлением и обработка событий жизненного цикла СВ

workbox-background-sync


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


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


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


Базовое использование


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


import { BackgroundSyncPlugin } from 'workbox-background-sync'import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {  maxRetentionTime: 24 * 60, // Попытка выполнения повторного запроса будет выполнена в течение 24 часов (в минутах)})registerRoute(  /\/api\/.*\/*.json/,  new NetworkOnly({    plugins: [bgSyncPlugin],  }),  'POST')

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


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


Создание очереди


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName') // название очереди должно быть уникальным

Название очереди используется как часть названия "тега", который получает register() глобального SyncManager. Оно также используется как название "объектного хранилища" IndexedDB.


Добавление запроса в очередь


import { Queue } from 'workbox-background-sync'const queue = new Queue('myQueueName')self.addEventListener('fetch', (event) => {  // Клонируем запрос для безопасного чтения  // при добавлении в очередь  const promiseChain = fetch(event.request.clone()).catch((err) => {    return queue.pushRequest({ request: event.request })  })  event.waitUntil(promiseChain)})

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


workbox-cacheable-response


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


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


Кэширование на основе статус-кода


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) =>    url.origin === 'https://example.com' && url.pathname.startsWith('/images/'),  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Данная настройка указывает WB кэшировать любые ответы со статусом 0 или 200 при обработке запросов к https://example.com.


Кэширование на основе заголовка


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При обработке ответов на запросы к URL, начинающемуся с /path/to/api/, проверяется, присутствует ли в ответе заголовок X-Is-Cacheable (который добавляется сервером). Если заголовок присутствует и имеет значение true, такой ответ кэшируется.


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


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


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'registerRoute(  ({ url }) => url.pathname.startsWith('/path/to/api/'),  new StaleWhileRevalidate({    cacheName: 'api-cache',    plugins: [      new CacheableResponsePlugin({        statuses: [200, 404],        headers: {          'X-Is-Cacheable': 'true'        }      })    ]  }))

При использовании встроенной стратегии без явного определения cacheableResponse.CacheableResponsePlugin, для проверки валидности ответа используются следющие критерии:


  • staleWhileRevalidate и networkFirst: ответы со статусом 0 (непрозрачные ответы) и 200 считаются валидными
  • cacheFirst: только ответы со статусом 200 считаются валидными

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


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


Для определения логики кэширования за пределами стратегии можно использовать класс CacheableResponse:


import { CacheableResponse } from 'workbox-cacheable-response'const cacheable = new CacheableResponse({  statuses: [0, 200],  headers: {    'X-Is-Cacheable': 'true'  }})const response = await fetch('/path/to/api')if (cacheable.isResponseCacheable(response)) {  const cache = await caches.open('api-cache')  cache.put(response.url, response)} else {  // Ответ не может быть кэширован}

workbox-expiration


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


Ограничение количества записей в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем количество записей в кэше        maxEntries: 20      })    ]  }))

При достижении лимита удаляются самые старые записи.


Ограничение времени хранения ресурсов в кэше


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // ограничиваем время хранения ресурсов в кэше        maxAgeSeconds: 24 * 60 * 60      })    ]  }))

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


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


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


import { CacheExpiration } from 'workbox-expiration'const cacheName = 'my-cache'const expirationManager = new CacheExpiration(cacheName, {  maxAgeSeconds: 24 * 60 * 60,  maxEntries: 20})

Затем, при обновлении записи в кэше, вызывается метод updateTimestamp() для обновления "возраста" записи.


await openCache.put(request, response)await expirationManager.updateTimestamp(request.url)

Для проверки всех записей в кэше на предмет их соответствия установленным критериям вызывается метод expireEntries():


await expirationManager.expireEntries()

workbox-precaching


СВ позволяет записывать файлы в кэш во время установки. Это называется предварительным кэшированием, поскольку контент кэшируется перед использованием СВ.


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


WB предоставляет простой и понятный API для реализации этого паттерна и эффективной загрузки ресурсов.


При первом запуске приложения workbox-precaching "смотрит" на загружаемые ресурсы, удаляет дубликаты и регистрирует соответствующие события СВ для загрузки и хранения ресурсов. URL, которые содержат информацию о версии (версионную информацию) (например, хэш контента) используются в качестве ключей кэша без дополнительной модификации. К ключам кэша URL, которые не содержат такой информации, добавляется параметр строки запроса, представляющий хэш контента, генерируемый WB во время выполнения.


workbox-precaching делает все это при обработке события install СВ.


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


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


Обработка предварительно кэшированных ответов


Вызов precacheAndRoute() или addRoute() создает маршрутизатор, который определяет совпадения запросов с предварительно кэшированными URL.


В этом маршрутизаторе используется стратегия "сначала кэш".


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


Список предварительно кэшируемых ресурсов


workbox-precaching ожидает получения массива объектов со свойствами url и revision. Данный массив иногда называют "манифестом предварительного кэширования":


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([  { url: '/index.html', revision: '383676' },  { url: '/styles/app.0c9a31.css', revision: null },  { url: '/scripts/app.0d5770.js', revision: null },  // другие записи])

Свойства revision второго и третьего объектов имеют значения null. Это объясняется тем, что версионная информация этих объектов является частью значений их свойств url.


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


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


Обратите внимание: для генерации списка предварительно кэшируемых ресурсов следует использовать один из встроенных инструментов WB: workbox-build, workbox-webpack-plugin или workbox-cli. Создавать такой список вручную очень плохая идея.


Автоматическая обработка входящих запросов


При поиске совпадения входящего запроса с кэшированным ресурсом workbox-precaching автоматически выполняет некоторые манипуляции с URL.


Например, запрос к / оценивается как запрос к index.html.


Игнорирование параметров строки запроса


По умолчанию игнорируются параметры поиска, которые начинаются с utm_ или точно совпадают с fbclid. Это означает, что запрос к /about.html?utm_campaign=abcd оценивается как запрос к /about.html.


Игнорируемые параметры указываются в настройке ignoreURLParametersMatching:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    // Игнорируем все параметры    ignoreURLParametersMatching: [/.*/]  })

Основной файл директории


По умолчанию основным файлом директории считается index.html. Именно поэтому запросы к / оцениваются как запросы к /index.html. Это поведение можно изменить с помощью настройки directoryIndex:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null },  ],  {    directoryIndex: null  })

"Чистые" URL


По умолчанию к запросу добавляется расширение .html. Например, запрос к /about оценивается как /about.html. Это можно изменить с помощью настройки cleanUrls:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute([{ url: '/about.html', revision: 'b79cd4' }], {  cleanUrls: false})

Кастомные манипуляции


Настройка urlManipulation позволяет кастомизировать логику определения совпадений. Эта функция должна возвращать массив возможных совпадений:


import { precacheAndRoute } from 'workbox-precaching'precacheAndRoute(  [    { url: '/index.html', revision: '383676' },    { url: '/styles/app.0c9a31.css', revision: null },    { url: '/scripts/app.0d5770.js', revision: null }  ],  {    urlManipulation: ({ url }) => {      // Логика определения совпадений      return [alteredUrlOption1, alteredUrlOption2]    }  })

workbox-routing


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


workbox-routing это модуль, позволяющий "связывать" поступающие запросы с функциями, формирующими на них ответы.


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


Обратите внимание на следующее:


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

Определение совпадений и обработка запросов


В WB "роут" это две функции: функция "определения совпадения" и функция "обработки запроса".


WB предоставляет некоторые утилиты для помощи в реализации названных функций.


Функция определения совпадения принимает ExtendableEvent, Request и объект URL. Возврат истинного значения из этой функции означает совпадение. Например, вот пример определения совпадения с конкретным URL:


const matchCb = ({ url, request, event }) => {  return (url.pathname === '/special/url')}

Функция обработки запроса принимает такие же параметры + аргумент value, который имеет значение, возвращаемое из первой функции:


const handlerCb = async ({ url, request, event, params }) => {  const response = await fetch(request)  const responseBody = await response.text()  return new Response(`${responseBody} <!-- Глядите-ка! Новый контент. -->`, {    headers: response.headers  })}

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


Регистрация колбэков выглядит следующим образом:


import { registerRoute } from 'workbox-routing'registerRoute(matchCb, handlerCb)

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


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


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  matchCb,  new StaleWhileRevalidate())

Определение совпадений с помощью регулярного выражения


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


import { registerRoute } from 'workbox-routing'registerRoute(  new RegExp('/styles/.*\\.css'),  handlerCb)

Для запросов из одного источника данная "регулярка" будет регистрировать совпадения для следующих URL:



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



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


new RegExp('https://cdn\\.third-party-site\\.com.*/styles/.*\\.css')

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


Роут для навигации


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


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'// Предположим, что страница `/app-shell.html` была предварительно кэшированаconst handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler)registerRoute(navigationRoute)

При посещении пользователем вашего сайта, запрос на получение страницы будет считаться навигационным, следовательно, ответом на него будет кэшированная страница /app-shell.html.


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


import { createHandlerBoundToURL } from 'workbox-precaching'import { NavigationRoute, registerRoute } from 'workbox-routing'const handler = createHandlerBoundToURL('/app-shell.html')const navigationRoute = new NavigationRoute(handler, {  allowlist: [    new RegExp('/blog/')  ],  denylist: [    new RegExp('/blog/restricted/')  ]})registerRoute(navigationRoute)

Обратите внимание, что denyList имеет приоритет перед allowList.


Обработчик по умолчанию


import { setDefaultHandler } from 'workbox-routing'setDefaultHandler(({ url, event, params }) => {  // ...})

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


import { setCatchHandler } from 'workbox-routing'setCatchHandler(({ url, event, params }) => {  // ...})

Обработка не-GET-запросов


import { registerRoute } from 'workbox-routing'registerRoute(  matchCb,  handlerCb,  // определяем метод  'POST')registerRoute(  new RegExp('/api/.*\\.json'),  handlerCb,  // определяем метод  'POST')

workbox-strategies


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


Вот какие стратегии предоставляет рассматриваемый модуль.


Stale-While-Revalidate


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


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/images/avatars/'),  new StaleWhileRevalidate())

Cache-Fisrt


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


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


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'style',  new CacheFirst())

Network-First


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


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/social-timeline/'),  new NetworkFirst())

Network-Only


import { registerRoute } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'registerRoute(  ({url}) => url.pathname.startsWith('/admin/'),  new NetworkOnly())

Cache-Only


import { registerRoute } from 'workbox-routing'import { CacheOnly } from 'workbox-strategies'registerRoute(  ({ url }) => url.pathname.startsWith('/app/v2/'),  new CacheOnly())

Настройка стратегии


Каждая стратегия позволяет кастомизировать:


  • название кэша
  • лимит записей в кэше и время их "жизни"
  • плагины

Название кэша


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',  }))

Плагины


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


  • workbox-background-sync
  • workbox-broadcast-update
  • workbox-cacheable-response
  • workbox-expiration
  • workbox-range-requests

import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { ExpirationPlugin } from 'workbox-expiration'registerRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'image-cache',    plugins: [      new ExpirationPlugin({        // Хранить ресурсы в течение недели        maxAgeSeconds: 7 * 24 * 60 * 60,        // Хранить до 10 ресурсов        maxEntries: 10      })    ]  }))

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


workbox-recipies


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


Рецепты


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


Резервный контент


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


По умолчанию резервная страница должна иметь название offline.html.


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


Рецепт


import { offlineFallback } from 'workbox-recipes'import { setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'setDefaultHandler(  new NetworkOnly())offlineFallback()

Паттерн


import { setCatchHandler, setDefaultHandler } from 'workbox-routing'import { NetworkOnly } from 'workbox-strategies'const pageFallback = 'offline.html'const imageFallback = falseconst fontFallback = falsesetDefaultHandler(  new NetworkOnly())self.addEventListener('install', event => {  const files = [pageFallback]  if (imageFallback) {    files.push(imageFallback)  }  if (fontFallback) {    files.push(fontFallback)  }  event.waitUntil(self.caches.open('workbox-offline-fallbacks').then(cache => cache.addAll(files)))})const handler = async (options) => {  const dest = options.request.destination  const cache = await self.caches.open('workbox-offline-fallbacks')  if (dest === 'document') {    return (await cache.match(pageFallback)) || Response.error()  }  if (dest === 'image' && imageFallback !== false) {    return (await cache.match(imageFallback)) || Response.error()  }  if (dest === 'font' && fontFallback !== false) {    return (await cache.match(fontFallback)) || Response.error()  }  return Response.error()}setCatchHandler(handler)

Подготовка кэша


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


Рецепт


import { warmStrategyCache } from 'workbox-recipes'import { CacheFirst } from 'workbox-strategies'// Здесь может испоьзоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html']warmStrategyCache({urls, strategy})

Паттерн


import { CacheFirst } from 'workbox-strategies'// Здесь может использоваться любая стратегияconst strategy = new CacheFirst()const urls = [  '/offline.html',]self.addEventListener('install', event => {  // `handleAll` возвращает два промиса, второй промис разрешается после добавления всех элементов в кэш  const done = urls.map(path => strategy.handleAll({    event,    request: new Request(path),  })[1])  event.waitUntil(Promise.all(done))})

Кэширование страницы


Данный рецепт позволяет СВ отвечать на запрос на получение HTML-страницы с помощью стратегии "сначала сеть". При этом, СВ оптимизируется таким образом, что в случае отсутствия подключения к сети, возвращает ответ из кэша менее чем за 4 секунды. По умолчанию запрос к сети выполняется в течение 3 секунд. Настройка warmCache позволяет подготовить ("разогреть") кэш к использованию.


Рецепт


import { pageCache } from 'workbox-recipes'pageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { NetworkFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'pages'const matchCallback = ({ request }) => request.mode === 'navigate'const networkTimeoutSeconds = 3registerRoute(  matchCallback,  new NetworkFirst({    networkTimeoutSeconds,    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Кэширование статических ресурсов


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


Рецепт


import { staticResourceCache } from 'workbox-recipes'staticResourceCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'const cacheName = 'static-resources'const matchCallback = ({ request }) =>  // CSS  request.destination === 'style' ||  // JavaScript  request.destination === 'script' ||  // веб-воркеры  request.destination === 'worker'registerRoute(  matchCallback,  new StaleWhileRevalidate({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      })    ]  }))

Кэширование изображений


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


Рецепт


import { imageCache } from 'workbox-recipes'imageCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const cacheName = 'images'const matchCallback = ({ request }) => request.destination === 'image'const maxAgeSeconds = 30 * 24 * 60 * 60const maxEntries = 60registerRoute(  matchCallback,  new CacheFirst({    cacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      }),      new ExpirationPlugin({        maxEntries,        maxAgeSeconds      })    ]  }))

Кэширование гугл-шрифтов


Данный рецепт кэширует таблицу стилей для шрифтов с помощью стратегии "считается устаревшим после запроса" и сами шрифты с помощью стратегии "сначала кэш". По умолчанию кэшируется до 30 шрифтов в течение 1 года.


Рецепт


import { googleFontsCache } from 'workbox-recipes'googleFontsCache()

Паттерн


import { registerRoute } from 'workbox-routing'import { StaleWhileRevalidate } from 'workbox-strategies'import { CacheFirst } from 'workbox-strategies'import { CacheableResponsePlugin } from 'workbox-cacheable-response'import { ExpirationPlugin } from 'workbox-expiration'const sheetCacheName = 'google-fonts-stylesheets'const fontCacheName = 'google-fonts-webfonts'const maxAgeSeconds = 60 * 60 * 24 * 365const maxEntries = 30registerRoute(  ({ url }) => url.origin === 'https://fonts.googleapis.com',  new StaleWhileRevalidate({    cacheName: sheetCacheName  }))// Кэшируем до 30 шрифтов с помощью стратегии "сначала кэш" и храним кэш в течение 1 годаregisterRoute(  ({ url }) => url.origin === 'https://fonts.gstatic.com',  new CacheFirst({    cacheName: fontCacheName,    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200],      }),      new ExpirationPlugin({        maxAgeSeconds,        maxEntries      })    ]  }))

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


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


import {  pageCache,  imageCache,  staticResourceCache,  googleFontsCache,  offlineFallback} from 'workbox-recipes'pageCache()googleFontsCache()staticResourceCache()imageCache()offlineFallback()

workbox-window


Данный модуль выполняется в контексте window. Его основными задачами является следующее:


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

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


<script type="module">import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-window.prod.mjs'if ('serviceWorker' in navigator) {  const wb = new Workbox('/sw.js')  wb.register()}</script>

Использование сборщика модулей


Установка


yarn add workbox-window# илиnpm i workbox-window

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


import { Workbox } from 'workbox-window'if ('serviceWorker' in navigator) {  const wb = new Workbox('/sw.js')  wb.register()}

Примеры


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


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // `event.isUpdate` будет иметь значение `true`, если другая версия СВ  // управляет страницей при регистрации данной версии  if (!event.isUpdate) {    console.log('СВ был активирован в первый раз!')    // Если СВ настроен для предварительного кэширования ресурсов,    // эти ресурсы могут быть получены здесь  }})// Региструем СВ после добавления обработчиков событийwb.register()

Уведомление пользователя о том, что СВ был установлен, но ожидает активации


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


const wb = new Workbox('/sw.js')wb.addEventListener('waiting', (event) => {  console.log(    `Новый СВ был установлен, но он не может быть активирован, пока все вкладки браузера не будут закрыты или перезагружены`  )})wb.register()

Уведомление пользователя об обновлении кэша


Модуль workbox-broadcast-update позволяет информировать пользователей об обновлении контента. Для получения этой информации в браузере используется событие message с типом CACHE_UPDATED:


const wb = new Workbox('/sw.js')wb.addEventListener('message', (event) => {  if (event.data.type === 'CACHE_UPDATED') {    const { updatedURL } = event.data.payload    console.log(`Доступна новая версия ${updatedURL}!`)  }})wb.register()

Отправка СВ списка URL для кэширования


В некоторых приложениях имеет смысл кэшировать только те ресурсы, которые используются посещенной пользователем страницей. Модуль workbox-routing принимает список URL и кэширует их на основе правил, определенных в маршрутизаторе.


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


const wb = new Workbox('/sw.js')wb.addEventListener('activated', (event) => {  // Получаем `URL` текущей страницы + все загружаемые страницей ресурсы  const urlsToCache = [    location.href,    ...performance      .getEntriesByType('resource')      .map((r) => r.name)  ]  // Передаем этот список СВ  wb.messageSW({    type: 'CACHE_URLS',    payload: { urlsToCache }  })})wb.register()

Практика


В этом разделе представлено несколько сниппетов, которые можно использовать в приложениях "как есть", а также краткий обзор готовых решений для разработки PWA, предоставляемых такими фреймворками для фронтенда, как React и Vue.


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


О том, что такое манифест можно почитать здесь, здесь и здесь.


Как правило, манифест (и СВ) размещаются на верхнем уровне (в корневой директории) проекта. Манифест может иметь расширение .json или .webmanifest (лучше использовать первый вариант).


Манифест


{  "name": "Название приложения",  "short_name": "Краткое название (будет указано под иконкой приложения при его установке)",  "scope": "/", // зона контроля СВ, разные страницы могут обслуживаться разными СВ  "start_url": ".", // начальный URL, как правило, директория, в которой находится index.html, в котором регистрируется СВ  "display": "standalone",  "orientation": "portrait",  "background_color": "#f0f0f0",  "theme_color": "#3c3c3c",  "description": "Описание приложения",  // этих иконок должно быть достаточно для большинства девайсов  "icons": [    {      "src": "./icons/64x64.png",      "sizes": "64x64",      "type": "image/png"    },    {      "src": "./icons/128x128.png",      "sizes": "128x128",      "type": "image/png"    },    {      "src": "./icons/256x256.png",      "sizes": "256x256",      "type": "image/png",      "purpose": "any maskable"    },    {      "src": "./icons/512x512.png",      "sizes": "512x512",      "type": "image/png"    }  ],  "serviceworker": {    "src": "./service-worker.js" // ссылка на файл с кодом СВ  }}

Ручная реализация СВ, использующего стратегию "сначала кэш"


// Название кэша// используется для обновления кэша// в данном случае, для этого достаточно изменить версию кэша - my-cache-v2const CACHE_NAME = 'my-cache-v1'// Критические для работы приложения ресурсыconst ASSETS_TO_CACHE = [  './index.html',  './offline.html',  './style.css',  './script.js']// Предварительное кэширование ресурсов, выполняемое во время установки СВself.addEventListener('install', (e) => {  e.waitUntil(    caches      .open(CACHE_NAME)      .then((cache) => cache.addAll(ASSETS_TO_CACHE))  )  self.skipWaiting()})// Удаление старого кэша во время активации нового СВself.addEventListener('activate', (e) => {  e.waitUntil(    caches      .keys()      .then((keys) =>        Promise.all(          keys.map((key) => {            if (key !== CACHE_NAME) {              return caches.delete(key)            }          })        )      )  )  self.clients.claim()})// Обработка сетевых запросов/*  1. Выполняется поиск совпадения  2. Если в кэше имеется ответ, он возвращается  3. Если ответа в кэше нет, выполняется сетевой запрос  4. Ответ на сетевой запрос кэшируется и возвращается  5. В кэш записываются только ответы на `GET-запросы`  6. При возникновении ошибки возвращается резервная страница*/self.addEventListener('fetch', (e) => {  e.respondWith(    caches      .match(e.request)      .then((response) =>          response || fetch(e.request)            .then((response) =>              caches.open(CACHE_NAME)                .then((cache) => {                  if (e.request.method === 'GET') {                    cache.put(e.request, response.clone())                  }                  return response                })          )      )      .catch(() => caches.match('./offline.html'))  )})

Конфигурация Webpack


Пример настройки вебпака для производственной сборки прогрессивного веб-приложения.


Предположим, что в нашем проекте имеется 4 директории:


  • public директория со статическими ресурсами, включая index.html, manifest.json и sw-reg.js
  • src директория с кодом приложения
  • build директория для сборки
  • config директория с настройками, включая .env, paths.js и webpack.config.js

В файле public/sw-reg.js содержится код регистрации СВ:


if ('serviceWorker' in navigator) {  window.addEventListener('load', () => {    navigator.serviceWorker      .register('./service-worker.js')      .then((reg) => {        console.log('СВ зарегистрирован: ', reg)      })      .catch((err) => {        console.error('Регистрация СВ провалилась: ', err)      })  })}

В файле config/paths.js осуществляется экспорт путей к директориям с файлами приложения:


const path = require('path')module.exports = {  public: path.resolve(__dirname, '../public'),  src: path.resolve(__dirname, '../src'),  build: path.resolve(__dirname, '../build')}

Допустим, что в качестве фронтенд-фреймворка мы используем React, а также, что в проекте используется TypeScript. Тогда файл webpack.config.js будет выглядеть следующим образом:


const webpack = require('webpack')// импортируем пути к директориям с файлами приложенияconst paths = require('../paths')// плагин для копирования статических ресурсов в директорию сборкиconst CopyWebpackPlugin = require('copy-webpack-plugin')// плагин для обработки `index.html` - вставки ссылок на стили и скрипты, добавления метаданных и т.д.const HtmlWebpackPlugin = require('html-webpack-plugin')// плагин для обеспечения прямого доступа к переменным среды окруженияconst Dotenv = require('dotenv-webpack')// плагин для минификации и удаления неиспользуемого CSSconst MiniCssExtractPlugin = require('mini-css-extract-plugin')// плагин для сжатия изображенийconst ImageminPlugin = require('imagemin-webpack-plugin').default// плагин для добавления блоков кодаconst AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')// Плагин для генерации СВconst { GenerateSW } = require('workbox-webpack-plugin')// настройки Babelconst babelLoader = {  loader: 'babel-loader',  options: {    presets: ['@babel/preset-env', '@babel/preset-react'],    plugins: [      '@babel/plugin-proposal-class-properties',      '@babel/plugin-syntax-dynamic-import',      '@babel/plugin-transform-runtime'    ]  }}module.exports = {  // режим сборки  mode: 'production',  // входная точка  entry: {    index: {      import: `${paths.src}/index.js`,      dependOn: ['react', 'helpers']    },    react: ['react', 'react-dom'],    helpers: ['immer', 'nanoid']  },  // отключаем логгирование  devtool: false,  // результат сборки  output: {    // директория сборки    path: paths.build,    // название файла    filename: 'js/[name].[contenthash].bundle.js',    publicPath: './',    // очистка директории при каждой сборке    clean: true,    crossOriginLoading: 'anonymous',    module: true  },  resolve: {    alias: {      '@': `${paths.src}/components`    },    extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json']  },  experiments: {    topLevelAwait: true,    outputModule: true  },  module: {    rules: [      // JavaScript, React      {        test: /\.m?jsx?$/i,        exclude: /node_modules/,        use: babelLoader      },      // TypeScript      {        test: /.tsx?$/i,        exclude: /node_modules/,        use: [babelLoader, 'ts-loader']      },      // CSS, SASS      {        test: /\.(c|sa|sc)ss$/i,        use: [          'style-loader',          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      },      // статические ресурсы - изображения и шрифты      {        test: /\.(jpe?g|png|gif|svg|eot|ttf|woff2?)$/i,        type: 'asset'      },      {        test: /\.(c|sa|sc)ss$/i,        use: [          MiniCssExtractPlugin.loader,          {            loader: 'css-loader',            options: { importLoaders: 1 }          },          'sass-loader'        ]      }    ]  },  plugins: [    new CopyWebpackPlugin({      patterns: [        {          from: `${paths.public}/assets`        }      ]    }),    new HtmlWebpackPlugin({      template: `${paths.public}/index.html`    }),    // это позволяет импортировать реакт только один раз    new webpack.ProvidePlugin({      React: 'react'    }),    new Dotenv({      path: './config/.env'    }),    new MiniCssExtractPlugin({      filename: 'css/[name].[contenthash].css',      chunkFilename: '[id].css'    }),    new ImageminPlugin({      test: /\.(jpe?g|png|gif|svg)$/i    }),    // Добавляем код регистрации СВ в `index.html`    new AddAssetHtmlPlugin({ filepath: `${paths.public}/sw-reg.js` }),    // Генерируем СВ    new GenerateSW({      clientsClaim: true,      skipWaiting: true    })  ],  optimization: {    runtimeChunk: 'single'  },  performance: {    hints: 'warning',    maxEntrypointSize: 512000,    maxAssetSize: 512000  }}

Здесь вы найдете шпаргалку по настройке вебпака. Пример полной конфигурации вебпака для JS/React/TS-проекта можно посмотреть здесь.


React PWA


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


yarn create react-app my-app --template pwa# илиnpx create-react-app ...

Или, если речь идет о TypeScript-проекте:


yarn create react-app my-app --template pwa-typescript# илиnpx create-react-app ...

Кроме прочего, в директории src создаются файлы service-worker.ts и serviceWorkerRegister.ts (последний импортируется в index.tsx), а в директории public файл manifest.json.


Затем, перед сборкой проекта с помощью команды yarn build или npm run build, в файл src/index.tsx необходимо внести одно изменение:


// доserviceWorkerRegistration.unregister();// послеserviceWorkerRegistration.register();

Подробнее об этом можно прочитать здесь.


Vue PWA


С Vue дела обстоят еще проще.


Глобально устанавливаем vue-cli:


yarn global add @vue/cli# илиnpm i -g @vue/cli

Затем, при создании шаблона проекта с помощью команды vue create my-app, выбираем Manually select features и Progressive Web App (PWA) Support.


Кроме прочего, в директории src создается файл registerServiceWorker.ts, который импортируется в main.ts. Данный файл содержит ссылку на файл service-worker.js, который, как и manifest.json, автоматически создается при сборке проекта с помощью команды yarn build или npm run build. Разумеется, содержимое обоих файлов можно кастомизировать.

Подробнее..

WebRTC CDN на Google Cloud Platform с балансировкой и автоматическим масштабированием

18.06.2021 10:22:12 | Автор: admin

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

Кратко напомним основные тезисы:

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

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

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

Инфраструктура Google Cloud Platform, как и в случае с AWS, поддерживает автоматическое масштабирование и балансировку распределения нагрузки, поэтому не приходится беспокоиться о лишних расходах на оплату виртуальных серверов вы платите только за то, что используете.

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

Теперь рассмотрим, как развернуть WCS CDN с балансировщиком и автоматическим масштабированием и процесс тестирования развернутой системы.

Разворачиваем WebRTC CDN с балансировщиком и автоматическим масштабированием на Google Cloud Platform

Конфигурация CDN будет следующей:

  • один Origin сервер;

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

Для развертывания потребуется настроить следующие компоненты в консоли Google Cloud Platform:

  • глобальный файрволл на уровне проекта Google Cloud;

  • виртуальные машины WCS CDN Origin и WCS CDN Edge;

  • шаблон развертывания на основе образа диска WCS CDN Edge;

  • группу масштабирования;

  • балансировщик нагрузки.

Итак, приступим.

Настраиваем глобальный файрволл на уровне проекта Google Cloud для прохождения WebRTC трафика

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

В основном меню консоли Google Cloud откройте раздел "VPC networks" и выберите пункт "Firewall":

На открывшейся странице нажмите кнопку "Create Firewall Rule" :

В открывшемся мастере задайте имя правила:

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

Еще ниже в секции "Protocols and ports" укажите порты для работы WCS и нажмите кнопку "Create":

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

Разворачиваем WCS сервер с ролью Origin для WebRTC CDN

В консоли Google Cloud откройте раздел "Compute Engine" и выберите из меню в левой части пункт "VM instances". Нажмите кнопку "Create" в диалоге создания нового экземпляра сервера:

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

Ниже на странице в секции "Boot disk" нажмите кнопку "Change" и выберите образ "CentOS 7":

Разверните секцию "Management, security, disks, networking, sole tenancy":

На вкладке "Security" добавьте публичный ключ для доступа к серверу по SSH:

На вкладке "Networking" в секции "Network interfaces" настройте внешний и внутренний IP адреса для сервера. Для работы в составе CDN серверу нужно назначить статический внутренний IP адрес:

После всех настроек нажмите кнопку "Create" для создания нового экземпляра WCS сервера с ролью CDN Origin:

Спустя пару минут сервер будет создан и запущен. Подключаемся к нему по ssh и устанавливаем WCS. Все действия - установка, изменение настроек, запуск или перезапуск WCS - должны выполняться с root правами, либо через sudo.

1.Установите Wget, Midnight Commander и дополнительные инструменты и библиотеки

sudo yum -y install wget mc tcpdump iperf3 fontconfig

2.Установите JDK. Для работы в условиях больших нагрузок рекомендуется JDK 12 или 14. Удобнее провести установку при помощи скрипта на bash. Текст скрипта:

#!/bin/bashsudo rm -rf jdk*curl -s https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz | tar -zx[ ! -d jdk-12.0.2/bin ] && exit 1sudo mkdir -p /usr/java[ -d /usr/java/jdk-12.0.2 ] && sudo rm -rf /usr/java/jdk-12.0.2sudo mv -f jdk-12.0.2 /usr/java[ ! -d /usr/java/jdk-12.0.2/bin ] && exit 1sudo rm -f /usr/java/defaultsudo ln -sf /usr/java/jdk-12.0.2 /usr/java/defaultsudo update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1sudo update-alternatives --install "/usr/bin/jstack" "jstack" "/usr/java/jdk-12.0.2/bin/jstack" 1sudo update-alternatives --install "/usr/bin/jcmd" "jcmd" "/usr/java/jdk-12.0.2/bin/jcmd" 1sudo update-alternatives --install "/usr/bin/jmap" "jmap" "/usr/java/jdk-12.0.2/bin/jmap" 1sudo update-alternatives --set "java" "/usr/java/jdk-12.0.2/bin/java"sudo update-alternatives --set "jstack" "/usr/java/jdk-12.0.2/bin/jstack"sudo update-alternatives --set "jcmd" "/usr/java/jdk-12.0.2/bin/jcmd"sudo update-alternatives --set "jmap" "/usr/java/jdk-12.0.2/bin/jmap"

3.Загрузите архив для установки самой свежей стабильной версии WebCallServer:

sudo wget https://flashphoner.com/download-wcs5.2-server.tar.gz

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

sudo tar -xvzf FlashphonerWebCallServer-5.2.714.tar.gz && cd FlashphonerWebCallServer-5.2.714 && ./install.sh

5.Для активации лицензии запустите скрипт "./activation.sh" из каталога установки WCS. Этот шаг, при желании, можно пропустить и активировать лицензию позже через веб-интерфейс:

sudo cd /usr/local/FlashphonerWebCallServer/bin && sudo ./activation.sh

6.Отключите firewalld и SELinux. Сетевой экран мы ранее настроили на уровне Google Cloud Platform, поэтому нет необходимости закрывать порты в операционной системе:

sudo systemctl stop firewalld && systemctl disable firewalld && setenforce 0

7.Откройте любым удобным редактором файл flashphoner.properties, который можно найти по пути:

/usr/local/FlashphonerWebCallServer/conf/flashphoner.properties

и внесите в него настройки для запуска CDN. В параметре "cdn_ip" укажите внутренний IP адрес вашей виртуальной машины с ролью CDN Origin:

cdn_enabled=truecdn_ip=10.128.0.3 # Local IP address CDN Origincdn_nodes_resolve_ip=falsecdn_role=origin

На скриншоте ниже примерный вид файла flashphoner.properties для WCS с ролью CDN Origin:

После изменения настроек запустите (или перезапустите) Web Call Server:

systemctl start webcallserver

На этом запуск и настройка Origin закончены. Перейдем к настройке балансировщика нагрузки и автоматического масштабирования.

Запускаем балансировщик нагрузки и автоматическое масштабирование в Google Cloud для WebRTC CDN

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

  • образ диска, который будет использоваться в шаблоне при создании нового экземпляра WCS;

  • шаблон, на основе которого будут создаваться новые экземпляры сервера при масштабировании;

  • группа масштабирования;

  • балансировщик нагрузки;

  • настройки контроля активности сервера.

Создаем образ диска WCS сервера с ролью Edge для WebRTC CDN

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

Повторите инструкцию по подготовке сервера Origin до пункта о внесении настроек в файл flashphoner.properties. Для роли Edge внесите в этот файл следующие настройки:

cdn_enabled=truecdn_ip=10.128.0.4cdn_nodes_resolve_ip=falsecdn_point_of_entry=10.128.0.3cdn_role=edgehttp_enable_root_redirect=false

После внесения и сохранения настроек, остановите в консоли Google Cloud виртуальную машину WCS CDN Edge, выберите из меню в левой части пункт "Images" и нажмите кнопку "Create Image":

В открывшемся мастере укажите имя нового образа, выберите в качестве источника диск виртуальной машины WCS CDN Edge и нажмите кнопку "Create":

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

Создаем шаблон развертывания Edge сервера

Выберите из меню в левой части окна консоли Google Cloud пункт "Instance templates" и нажмите кнопку "Create Instance template":

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

Ниже на странице в секции "Boot disk" нажмите кнопку "Change". в Открывшемся окне перейдите на вкладку "Custom Images" и выберите образ диска WCS CDN Edge, который мы создали ранее. Нажмите кнопку "Select":

Разверните секцию "Management, security, disks, networking, sole tenancy". На вкладке "Security" добавьте публичный ключ для доступа к серверу по SSH и нажмите кнопку "Create":

Шаблон развертывания для WCS с ролью CDN Edge создан. Теперь перейдем к созданию группы масштабирования.

Создаем группы масштабирования для Edge серверов

Из меню в левой части окна консоли Google Cloud выберите пункт "Instance groups" и нажмите кнопку "Create Instance group":

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

В секции "Autoscaling" на этой же странице настройте триггер запуска дополнительных серверов Edge. В качестве триггера будем использовать загрузку процессора более 80% . В поле "Maximum number of instances" укажите максимальное количество виртуальных машин, которые будут запущены при срабатывании триггера:

Затем включите проверку состояния виртуальной машины в секции "Autohealing". Для того, что бы создать настройку проверки сервера выберите из списка в поле "Health check" пункт "Сreate a health check":

В открывшемся мастере создания проверки состояния сервера укажите имя проверки, протокол TCP, порт 8081 и запрос /health-check. Настройте критерии проверки и нажмите кнопку "Save and continue":

Разверните секцию "Advanced creation options" и активируйте чекбокс "Do not retry machine creation". После чего нажмите "Create":

Будет создана группа масштабирования и запущен один WCS с ролью CDN Edge. Последним этапом настройки нашей CDN с балансировщиком нагрузки и автоматическим масштабированием будет настройка балансировщика.

Создаем балансировщик нагрузки

Сначала зарезервируем для балансировщика внешний IP адрес. В главном меню Google Cloud Platform в секции "VPC network" выберите пункт "External IP addresses" и нажмите кнопку "Reserve static address":

На открывшейся странице в поле "Name" задаем имя для зарезервированного IP адреса. Выбираем уровень качества сетевых услуг для адреса и тип распространения. После завершения всех настроек нажимаем кнопку "Reserve":

Затем переходим к настройке балансировщика.

Выбираем пункт "Load balancing" в разделе "Network services" секции "Networking" основного меню Google Cloud Platform:

Нажимаем кнопку "Create load balancer":

Затем выберите тип балансировщика "TCP Load Balancing" и нажмите кнопку "Start configuration":

На открывшейся странице укажите внешний балансировщик "From Internet to my VMs" и регион размещения серверов балансировщика. После выбора настроек нажмите кнопку "Continue":

На следующей странице задайте имя балансировщика, Затем перейдите в раздел настроек "Backend configuration" и укажите в каком регионе будут созданы сервера входящие в состав балансировщика. На вкладке "Select existing instance groups" выберите группу масштабирования Edge серверов, которую мы создали ранее. Затем в поле "Health check"выберите из выпадающего списка пункт "Сreate a health check":

На открывшейся странице укажите параметры для проверки состояния работы балансировщика порт 8081 и запрос /, после чего нажмите кнопку "Save and continue":

Затем перейдите к настройкам раздела "Frontend configuration". В этом разделе нужно создать привязку портов к внешнему IP адресу. Укажите внешний IP адрес для балансировщика, который мы зарезервировали выше и создайте конфигурации для TCP портов 8081, 8080, 8443, 8444 для HTTP(S) и WS(S). После создания необходимых портов нажмите кнопку "Create":

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

Тестирование WebRTC CDN с балансировщиком и масштабированием на базе Google Cloud Platform

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

Для проведения нагрузочного тестирования, при создании группы масштабирования мы выставили порог загрузки процессора для срабатывания триггера на 20%. Тестирование будем проводить с использованием браузера Google Chrome и виртуальной вебкамеры для организации трансляции видеопотока. Что бы сымитировать повышение нагрузки на процессор запустим воспроизведение потока с транскодированием с помощью примера "Media Devices".

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

Тестирование

В браузере Google Chrome открываем web интерфейс WCS с ролью CDN Origin Авторизуемся, открываем пример "Two-way Streaming", устанавливаем соединение с сервером по WebSocket и публикуем видеопоток.

Затем, запускаем web интерфейс WCS CDN Edge сервера по IP адресу, который был зарезервирован при создании балансировщика.

Авторизуемся, открываем пример "Media Devices" и устанавливаем соединение с балансировщиком по WebSocket. В правом столбце настроек снимаем чек бокс "default" для параметра "Size" и задаем значения для транскодирования видеопотока. Например, если поток на Origin сервере опубликован с размерами 320х240 задаем значение 640х480. Повторите действия в нескольких вкладках браузера, для имитации большого количества зрителей.

В консоли Google Cloud видим, что были запущены две дополнительные виртуальные машины:

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

http://<WCS instance IP address>:8081/?action=stat

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

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

Хорошего стриминга!

Ссылки

Наш демо сервер

CDN для стриминга WebRTC с низкой задержкой - CDN на базе WCS

Документация по быстрому развертыванию и тестированию WCS сервера

Документация по развертыванию WCS в Google Cloud Platform

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

Подробнее..

Как удвоить эффективность сотрудника при помощи цифровизации

11.06.2021 12:20:14 | Автор: admin

Всем привет!

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

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

Однажды крупная компания обратилась к нам с бизнес-задачей ежегодный рост EBITDA (прибыль до вычета процентов) на 10-15% в ближайшие 5 лет без найма новых сотрудников. Как решить эту задачу не увеличивая штат? Ответ есть цифровизовать предприятие.

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

При чем тут цифровизация?

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

  • уберёт ошибки;

  • автоматизирует рутинные процессы, которые занимают много времени;

  • передаст массу дополнительных знаний и инструментов.

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

Если коллеги находятся на разных этажах огромного здания, то сходить несколько раз за день туда-обратно с 1 на 9 этаж, казалось бы, несложно и занимает всего минут 30 в сумме. Но за год работы эти 30 минут превращаются в 126 часов рабочего времени одного человека. Это больше трёх рабочих недель чистого времени!

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

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

Это привело к тому, что окупаемость вложений снизилась до 3-5 лет. Возврат инвестиций стал быстрее.

Экономическая эффективность

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

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

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

Портал как якорь спасения

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

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

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

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

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

А вы в цифре?

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

  1. В вашей компании есть оцифрованные процессы, и вы работаете исключительно по ним;

  2. У вас есть примеры, когда за последние 2-3 года цифровые элементы позволили оптимизировать штат сотрудников;

  3. В постоянном использовании компании есть корпоративные инструменты взаимодействия людей на расстоянии (например, на уровне процессов внедрён Telegram или Zoom);

  4. У вас существуют цифровые системы обучения и развития сотрудников;

  5. Есть корпоративный новостной портал или корпоративная социальная сеть, в которую вовлечены сотрудники.

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

Совпало? Поздравляю, вы восхитительны!

Редактор: Наумкина Карина.

Подробнее..

Перевод Карманная книга по TypeScript. Часть 8. Модули

21.06.2021 10:15:25 | Автор: admin

image


Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



Определение модуля


В TS, как и в ECMAScript2015, любой файл, содержащий import или export верхнего уровня (глобальный), считается модулем.


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


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


Не модули


Для начала, давайте разберемся, что TS считает модулем. Спецификация JS определяет, что любой файл без export или await верхнего уровня является скриптом, а не модулем.


Переменные и типы, объявленные в скрипте, являются глобальными (имеют глобальную область видимости), для объединения нескольких файлов на входе в один на выходе следует использовать либо настроку компилятора outFile, либо несколько элементов script в разметке (указанных в правильном порядке).


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


export {}

Модули в TS


Существует 3 вещи, на которые следует обращать внимание при работе с модулями в TS:


  • Синтаксис: какой синтаксис я хочу использовать для импорта и экспорта сущностей?
  • Разрешение модулей: каковы отношения между названиями модулей (или их путями) и файлами на диске?
  • Результат: на что должен быть похож код модуля?

Синтаксис


Основной экспорт в файле определяется с помощью export default:


// @filename: hello.tsexport default function helloWorld() {  console.log('Привет, народ!')}

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


import hello from './hello.js'hello()

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


// @filename: maths.tsexport var pi = 3.14export let squareTwo = 1.41export const phi = 1.61export class RandomNumberGenerator {}export function absolute(num: number) {  if (num < 0) return num * -1  return num}

Указанные сущности импортируются так:


import { pi, phi, absolute } from './maths.js'console.log(pi)const absPhi = absolute(phi)  // const absPhi: number

Дополнительный синтаксис импорта


Название импортируемой сущности можно менять с помощью import { old as new }:


import { pi as  } from './maths.js'console.log()        /*          (alias) var : number          import         */

Разные способы импорта можно смешивать:


// @filename: maths.tsexport const pi = 3.14export default class RandomNumberGenerator {}// @filename: app.tsimport RNGen, { pi as  } from './maths.js'RNGen/*  (alias) class RNGen  import RNGen*/console.log()/*  (alias) const : 3.14  import */

Все экспортированные объекты при импорте можно поместить в одно пространство имен с помощью * as name:


// @filename: app.tsimport * as math from './maths.js'console.log(math.pi)const positivePhi = math.absolute(math.phi)  // const positivePhi: number

Файлы можно импортировать без указания переменных:


// @filename: app.tsimport './maths.js'console.log('3.14')

В данном случае import ничего не делает. Тем не менее, весь код из maths.ts вычисляется (оценивается), что может привести к запуску побочных эффектов, влияющих на другие объекты.


Специфичный для TS синтаксис модулей


Типы могут экспортироваться и импортироваться с помощью такого же синтаксиса, что и значения в JS:


// @filename: animal.tsexport type Cat = { breed: string, yearOfBirth: number }export interface Dog {  breeds: string[]  yearOfBirth: number}// @filename: app.tsimport { Cat, Dog } from './animal.js'type Animals = Cat | Dog

TS расширяет синтаксис import с помощью import type, что позволяет импортировать только типы.


// @filename: animal.tsexport type Cat = { breed: string, yearOfBirth: number }// 'createCatName' cannot be used as a value because it was imported using 'import type'.// 'createCatName' не может использоваться в качестве значения, поскольку импортируется с помощью 'import type'export type Dog = { breeds: string[], yearOfBirth: number }export const createCatName = () => 'fluffy'// @filename: valid.tsimport type { Cat, Dog } from './animal.js'export type Animals = Cat | Dog// @filename: app.tsimport type { createCatName } from './animal.js'const name = createCatName()

Такой импорт сообщает транспиляторам, вроде Babel, swc или esbuild, какой импорт может быть безопасно удален.


Синтаксис ES-модулей с поведением CommonJS


Синтаксис ES-модулей в TS напрямую согласуется с CommonJS и require из AMD. Импорт с помощью ES-модулей в большинстве случаев представляет собой тоже самое, что require в указанных окружениях, он позволяет обеспечить полное совпадение TS-файла с результатом CommonJS:


import fs = require('fs')const code = fs.readFileSync('hello.ts', 'utf8')

Синтаксис CommonJS


CommonJS это формат, используемый большинством npm-пакетов. Даже если вы используете только синтаксис ES-модулей, понимание того, как работает CommonJS, поможет вам в отладке приложений.


Экспорт


Идентификаторы экпортируются посредством установки свойства exports глобальной переменной module:


function absolute(num: number) {  if (num < 0) return num * -1  return num}module.exports = {  pi: 3.14,  squareTwo: 1.41,  phi: 1.61,  absolute}

Затем эти файлы импортируются с помощью инструкции require:


const maths = require('maths')maths.pi  // any

В данном случае импорт можно упростить с помощью деструктуризации:


const { squareTwo } = require('maths')squareTwo  // const squareTwo: any

Взаимодействие CommonJS с ES-модулями


Между CommonJS и ES-модулями имеется несовпадение, поскольку ES-модули поддерживают "дефолтный" экспорт только объектов, но не функций. Для преодоления данного несовпадения в TS используется флаг компиляции esModuleInterop.


Настройки, связанные с разрешением модулей


Разрешение модулей это процесс определения файла, указанного в качестве ссылки в строке из инструкции import или require.


TS предоставляет две стратегии разрешения модулей: классическую и Node. Классическая стратегия является стратегией по умолчанию (когда флаг module имеет значение, отличное от commonjs) и включается для обеспечения обратной совместимости. Стратегия Node имитирует работу Node.js в режиме CommonJS с дополнительными проверками для .ts и .d.ts.


Существует большое количество флагов, связанных с разрешением модулей: moduleResolution, baseUrl, paths, rootDirs и др.


Настройки для результатов разрешения модулей


Имеется две настройки, которые влияют на результирующий JS-код:


  • target определяет версию JS, в которую компилируется TS-код
  • module определяет, какой код используется для взаимодействия модулей между собой

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


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


Ниже приведено несколько примеров использования синтаксиса ES-модулей с разными настройками module:


import { valueOfPi } from './constants.js'export const twoPi = valueOfPi * 2

ES2020


import { valueOfPi } from './constants.js'export const twoPi = valueOfPi * 2

CommonJS


"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;

UMD


(function (factory) {  if (typeof module === "object" && typeof module.exports === "object") {    var v = factory(require, exports);    if (v !== undefined) module.exports = v;  }  else if (typeof define === "function" && define.amd) {    define(["require", "exports", "./constants.js"], factory);  }})(function (require, exports) {  "use strict";  Object.defineProperty(exports, "__esModule", { value: true });  exports.twoPi = void 0;  const constants_js_1 = require("./constants.js");  exports.twoPi = constants_js_1.valueOfPi * 2;});

Пространства имен (namespaces)


TS имеет собственный модульный формат, который называется namespaces. Данный синтаксис имеет множество полезных возможностей по созданию сложных файлов определений и по-прежнему активно используется в DefinitelyTyped. Несмотря на то, что namespaces не признаны устаревшими (deprecated), большая часть его возможностей нашла воплощение в ES-модулях, поэтому настоятельно рекомендуется использовать официальный синтаксис.




VPS серверы от Маклауд быстрые и безопасные.


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


Подробнее..

От одного приложения к сотне. Путь микрофронтенда в Тинькофф Бизнес

16.06.2021 12:16:05 | Автор: admin

Привет, меня зовут Ваня, недавно я выступил на CodeFest 11, где рассказал про путь Тинькофф Бизнеса на фронтенде от одного приложения к сотне. Но так как в ИT очень быстро все меняется, а ждать запись еще долго, сейчас я тезисно расскажу о нашем шестилетнем путешествии в дивный мир микрофронтенда!

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

Этапы развития

  1. Одно приложение на AngularJS в 20142015 годах.

  2. Миграция на Angular2.

  3. Утяжеление десяти приложений новой функциональностью.

  4. Переход к микросервисам и разбиение на 100 приложений.

На дворе начало 2015 года. К нам приходит бизнес и говорит: Мы хотим сделать зарплатный проект! Посмотрите, что есть сейчас на рынке по технологиям, и сделайте. Выбираем AngularJS, быстро создаем приложение. Спустя некоторое время аппетиты вырастают, мы создаем еще два сервиса. На этот момент фронтенд-приложения никак не взаимодействуют друг с другом.

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

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

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

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

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

Сайдбар

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

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

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

Подсвеченная область отдельное приложение СайдбарПодсвеченная область отдельное приложение Сайдбар

Frame Manager

Именно рваные переходы мы убрали с появлением Frame Manager'а (далее буду называть его ФМ).

Подсвеченная область отдельное приложение Frame ManagerПодсвеченная область отдельное приложение Frame Manager

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

Слева концепция сайдбара (было), справа Frame Manager'а (стало)Слева концепция сайдбара (было), справа Frame Manager'а (стало)

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

В плане интеграции приложения тоже все поменялось:

  • Раньше приложению-клиенту достаточно было подключить необходимый скрипт к себе в index.html.

  • Теперь все приложения ФМа хранятся в отдельной конфигурации и используются как единый источник правды.

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

Однажды через поддержку к нам обратились пользователи с ситуацией: Раньше у меня работал плагин для Google Chrome, а с недавнего времени именно на вашем сайте перестал. Почините, пожалуйста! Обычно на такие просьбы не реагируют: пользователь что-то себе установил пусть сам и разбирается. Но только не в нашей компании. Команда долго изучала вопрос, смотрела, какое окружение у клиента, версия браузера и все-все, но ответа так и не было. В итоге мы полностью повторили окружение, загрузили себе плагины и путем дебагинга установили, что данный плагин не работает, если у iframe динамически менять атрибут src или пересоздавать фрейм. К сожалению, мы так и не смогли исправить такое поведение, поскольку на этой концепции построено все взаимодействие ФМ и дочерних приложений.

Бесфрейм-менеджер

Однажды мы собрались и подумали: Несколько лет страдаем от iframe. Как перестать страдать? Давайте просто уберем его! Сказано сделано. Так и появился бесфрейм-менеджер с фантазией у нас, конечно, не фонтан ;-)

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

В решении три составляющие:

  1. Webpack-плагин основа нашего решения, подробнее о которой можно прочитать в статье Игоря.

  2. Angular builder обвязка для настройки и запуска плагина.

  3. Angular schematics скрипт для упрощения работы с файловой структурой с помощью AST.

В 2021 году плагин становится менее актуальным, потому что вышел Webpack 5 с Module Federation, но напомню, что мы вели разработку в 2018 году, а Angular стал поддерживать последнюю версию вебпака лишь с двенадцатой версии, которая вышла 12 мая 2021 года. Мы пока не уверены, сможет ли MF заменить наше решение, и изучаем комбинацию подходов.

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

http://personeltest.ru/aways/single-spa.js.org/docs/ecosystem-angular/https://single-spa.js.org/docs/ecosystem-angular/

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

Что же касается Angular builder и schematics, то они нужны, чтобы разработчики, которые будут интегрировать наше решение к себе, не выполняли километровую инструкцию, а просто написали в консоли:

ng update @scripts/deframing

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

Тестирование

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

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

  2. Сам ФМ определяет несколько жизненно важных процессов, работоспособность которых гарантирует при любых условиях: это авторизация, роутинг, работа с данными приложений. Для этого создаются приложения-стабы (stub), суть которых подключиться к ФМу и выполнить одну из вышеперечисленных функций. То есть на каждое изменение кодовой базы ФМа будет гарантированно работать эта функция.

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

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

.my-pretty-header {    display: none;}

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

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

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

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

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

Microzord

Вот мы и прошли шесть лет технического развития нашего решения. И что может быть лучше, чем поделиться этим опытом с сообществом? Все наработки будут публиковаться под npm scope @microzord с открытым кодом на Гитхабе.

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

Подробнее..

Создаем веб-приложение на 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.

Подробнее..

Дайджест Joomla за весну 2021

15.06.2021 00:15:33 | Автор: admin

Все главные новости из мира Joomla за осень 2020 и весну 2021, в одной статье.

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

Главные новости о Joomla

Начнем с того, что в 2020 году Joomla исполнилось 15 лет. 17 августа 2005 года состоялся первый публичный релиз Joomla 1.0. За это время было фактически 15 мажорных выпусков CMS, а всего было выпущено официально более 80 релизов Joomla.

С июня 2020 года вышло 7 релизов Joomla: с 3.9.20 по 3.9.27.

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

Релизы Joomla 4 и Joomla 3.10

Joomla 4

Joomla 4 в 2020 году прошла путь от Beta 2 до RC1. Проделана работа по включению в Joomla 4 css-фреймворка Bootstrap 5 и отказу от js-библиотеки jQuery. Bootstrap 5 используется как в панели администратора, так и в шаблоне по умолчанию Cassiopea.

Cообществом Joomla начата работа над локализацией Joomla 4 на русский язык. Переводчики всегда рады помощи в переводе Joomla 4 и исправлении ошибок.

Github локализаторов:https://github.com/JPathRu/localization

Joomla 3.10

Переход с Joomla 3.9 на Joomla 4.0 является миграцией. В Joomla 3.10 добавили новую функцию в компонент обновления Joomla, чтобы помочь в процессе мини-миграции: инструмент проверки перед обновлением (Pre-update Checker). Эта ветка развивается исключительно для облегчения миграции с Joomla 3 на Joomla 4 и будет поддерживаться 2 года с момента выпуска.

Статьи о Joomla

Аутентификация на основе токенов - как использовать ее в Joomla 4

Англоязычная статья и видео урок о новых возможностях Joomla 4. В статье рассказано об аутентификации без пароля (аутентификация на основе токенов) и API веб-сервисов Joomla.

Видео урок

Статья

90 баллов в Pagespeed для сайта на Joomla

Небольшая статья с рекомендациями по увеличению скорости загрузки сайта на Joomla и достижению заветной зеленой зоны по Google Page Speed. В качестве подопытного выбран сайт на на базе фреймворка Astroid от JoomDev.

Статья

Работа с шаблонами e-mail в Joomla 4

В Joomla 4 появится новая функция - шаблонизация HTML писем. В данной статье проведен небольшой мастер класс по настройке шаблонов писем для различных компонентов. Статья освещает важный момент мультиязычности рассылаемых писем.

Статья на английском

Пример обертки над API Joomla 4

На Github стали появляться примеры для оберток над API Joomla 4, что позволяет рассматривать Joomla 4 в качестве бэкенда для реактивных сайтов и сервисов.

Ссылка на Github

Официальный сайт

Информация об API Joomla

Google заявила о спонсировании разработки Joomla

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

Статья

Большой мануал по созданию каталога на базе полей Joomla

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

Статья

Интеграция форм обратной связи и Битрикс 24 на сайте Joomla

Небольшой мастер класс по интеграции CRM Битрикс 24 и сайта на CMS Joomla.

Статья

Обзор новой функции в Joomla 4: процессы публикации

В Joomla 4 появилась новая функция Workflows. В данной статье рассказано о новой функции. В русской локализации Joomla 4 термин Workflows будет переведен, как Процессы.

Статья

Статьи о подготовке шаблонов и расширений к совместимости с Joomla 4.

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

Тесты

Результаты тестирования Joomla 3.9 под PHP 8

По последним результатам тестирования Joomla показала относительно небольшой прирост под PHP 8.

Так что если вы сейчас используете хотя бы PHP 7.3 можете продолжать его использовать и не беспокоиться о том, что PHP 8 откроет вам новые горизонты. Однако если вы до сих пор используете PHP 5.6, то переход на PHP 7.4 или на 8, даст почти 35% к производительности.

Результаты тестов

Хотелось бы отметить, что многие подчеркивают производительность WordPress по сравнению с Joomla в этом тесте. В реальности надо смотреть на сравнение WordPress 5.6 - WooCommerce 4.8.0 vs Joomla. Во всех подобных тестах всегда возникает вопрос, почему для теста WP и Drupal выбирается одна из самых легких страниц, а для теста Joomla одна из самых тяжелых, так как формирования страницы избранного с кучей модулей, гораздо тяжелее чем формирование страницы статьи.

Расширения для Joomla

YOOtheme Pro

За прошедший год вышло несколько релизов популярного шаблона и сайт-билдера YOOtheme PRO. Шаблон построен на css-фреймворке UiKit, разработанным командой YOOtheme.

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

Плагин jYProExtra

Это плагин, расширяющий возможности сайтбилдера YOOTheme Pro. Он позволяет использовать улучшение изображений YOOtheme Pro на всем сайте: отложенная загрузка и возможность использовать изображения в формате WebP. Дочерние темы, удаление JavaScript, постраничная навигация YOOTheme PRO во всех компонентах и многое другое.

Страница расширения

Pro2Store для Yootheme PRO

Это аддон интернет магазина для популярного билдера сайтов Yootheme PRO.

Магазин предназначен исключительно для Yootheme PRO и распространяется бесплатно. Платно лишь дополнительные плагины оплаты.

Видео

Страница расширения

Phoca Email - компонент e-mail рассылок

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

Возможности компонента:

  • Рассылка по подписчикам.

  • Замена шаблонов рассылки компонентов, таких как Vituemart или PhocaCart.

  • Отправка сообщений от лица администрации.

  • Поддержка HTML сообщений.

  • Функция отправки статьи с сайта.

Страница расширения

Helix Ultimate 2.0 от JoomShaper

Helix - это один из самых популярных шаблонов для Joomla, на базе, которого строятся многие клубные шаблоны. За прошедший год было выпущено несколько alpha и beta версий этого фреймворка. На момент написания обзора была доступна Beta 3 на базе Bootstrap 5.

Так же JoomShaper имеет свой конструктор страниц SP Page Builder, которым пользуются более 200000 разработчиков по всему миру. Основной упор JoomShaper делает на редактирование дизайна сайта с фронта.

Статья

Обновление CCK для Joomla ZOO 4.0

YOOtheme выпустила некогда мега популярный CCK для Joomla. Спустя 6 лет после последнего значимого релиза ZOO 3.3 CCK обновился до версии 4. По сути ZOO 4 выпущен для расширения возможностей Page Builder YOOtheme Pro.

Разработчики заявляют о тесной интеграции с YOOtheme Pro и не случайно все страницы ZOO выпущены в качестве шаблонов для YOOtheme Pro.

Видео

Официальная новость

Обновления JBZoo v4.11 и 4.12

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

За эти 2 релиза проделана большая работа, добавлено много новых элементов и хуков (оплата через Сбербанк, Юкасса; доставка СДЭК, Boxberry), новый роутинг.

Подробнее

JL Like v4.0.5

Бесплатный плагин социальных кнопок для Joomla.

WT JoomShopping Bitrix24 PRO

Бесплатный плагин для двухсторонней интеграции CRM Битрикс24 и интернет-магазина на базе JoomShopping. Плагин предоставляет гибкую настройку сопоставления полей CRM и данных интернет-магазина, позволяет создавать лиды и сделки на определенной стадии воронки продаж, ищет дубли контактов, передает UTM-метки. Позволяет изменять статусы заказа в интернет-магазине в зависимости от стадии лида или сделки.

Плагин интегрирован с профессиональным плагином формы обратной связи RadicalForm и позволяет создавать лиды из форм обратной связи, созданных с помощью RadicalForm.

Страница расширения

Видео-гайд по настройке

Обновление WT Virtuemart Bitrix24

Плагин позволяет отправлять данные о заказах Virtuemart в CRM Битрикс24 в виде лидов. Своеобразная лайт-версия аналогичного плагина для JoomShopping.

Видео

Страница расширения

Обновление модуля вывода материалов JUNewsUltra Pro 6.9

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

Основной функционал.

  • Шаблонизация внешнего вида.

  • Поддержка Youtube, RSS, Vimeo, JUMultiThumb, Content Multicategories, JComments, Komento.

  • Очень гибкие настройки работы с изображениями.

  • Большой список настроек вывода материалов.

Что нового в JUNewsUltra?

  • Поддержка отдельного формата изображений WebP

  • Адаптивная поддержка изображений WebP вместе с обычными через тег <picture>

  • Добавлена поддержка нативной ленивой загрузки loading="lazy"

  • Добавлена поддержка нативного свойства decoding="async"

  • Добавлена поддержка Joomla 4

  • далены зависимости jQuery в админке Joomla 4

Страница расширения

TCI Telegram Content Import для Joomla

Системный плагин для CMS Joomla!, обеспечивающий импорт постов из Telegram в материалы Joomla.

Парсер умеет автоматически наполнять сайт Joomla из телеграмм канала создавая:

  • Заголовок поста

  • Краткое и полное описание

  • Дату создания поста

  • Автора

  • Картинки для полного и краткого описания, а так же их описание.

  • Умеет вытягивать галерею картинок из телеграмм и ссылки на ютуб.

  • Создавать теги на основе хештегов и выставлять избранное.

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

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

  • При наличии авторизации из телеграма позволяет полноценно использовать на сайте систему комментариев телеграма. Подключение производится посредством загрузки соответствующего скрипта API Telegram

Русскоязычное сообщество Joomla использует этот плагин для связи Joomlaportal.ru и телеграмм-канала сообщества.

Описание расширения

Компонент форума Kunena 5.2.1

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

Официальная новость

Akeeba Engage v1.0

Компонент комментариев от Akeeba. Комментарии совместимы с Joomla 3 и Joomla 4.

Возможности компонента:

  • Поддержка HTML комментариев.

  • Разметка Schema.org

  • Подготовлен под AMP сайты

  • Email уведомления

  • Поддержка Gravatar

  • Полная поддержка Joomla, в том числе и журнал действий и GDPR.

  • Защита от спама: CAPTCHA и Akismet.

  • Есть позиции для вывода модулей.

  • Полная поддержка кеша Joomla.

Официальная новость

Convert Forms конструктор форм для Joomla

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

Возможности компонента:

  • Конструктор форм.

  • Более 20 полей для формы.

  • Шаблоны форм.

  • Интеграции с различными онлайн сервисами рассылок.

  • Различные сценарии действий после отправки форм.

  • Зависимые поля.

  • Ajax.

  • Уведомления на email.

  • Импорт - экспорт.

  • Настройки шрифтов, цветов, границ и форм.

  • Встраивание с помощью шорткодов и модулей.

  • Адаптивность.

  • Поддержка популярных западных CRM и сервисов.

https://www.tassos.gr/joomla-extensions/convert-forms

Профессиональный плагин форм обратной связи RadicalForm

За год вышло несколько релизов. Плагин позволяет отправлять сообщения не только на e-mail, но и в Телеграм, Verbox, Jivosite и даже SMS. При необходимости разработчики могут создавать свои дополнительные плагины и отправлять данные форм куда угодно.

Страница расширения

JD Builder 1.8.0 и совместимость билдера с Joomla 4

JoomDev представил обновление своего Page Builder для Joomla. Основным нововведением считает совместимость с Joomla 4 и Joomla 3.10.

Что нового?

  • Интеграция с ACYMailing.

  • Элемент прайс-листа.

  • Бесплатные шаблоны для пользователей PRO.

  • Изменения структуры настроек.

Небольшой обзор нового функционала

Quantum Manager 1.6.0 современный файловый менеджер для Joomla.

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

  • Интеграция с фотостоками.

  • Автоматическая обработка картинок.

  • Встроенные средства редактирования изображений.

  • Поддержка WebP

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

  • Разграничение по группам пользователям

  • Настройка цвета иконок файлов.

  • Интеграция с SP Page Builder.

  • Интеграция с YOOTheme Pro

Страница расширения

Radical Multi Fields 3.0.1

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

Основная фишка этого обновления тесная интеграция с Quantum Manager v1.7.0.

Страница расширения

Обновление CFI v.1.0.6

CFI - плагин для импорта и экспорта данных стандартных материалов и кастомных (настраиваемых) полей.

Страница расширения

WT SEO Meta templates плагин сео-шаблонов для

Позволяет использовать сео-формулы (шаблоны, маски) для тега <title> и meta-тега description, например Купить {PRODUCT_NAME} в {CITY_NAME} за {PRODUCT_PRICE}. Принимает данные (в том числе и сео-шаблоны) из дополнительных плагинов-провайдеров.

На данный момент доступны плагины-провайдеры для категорий, материалов Joomla и их пользовательских полей, а так же компонентов Virtuemart и My City Selector.

https://web-tolk.ru/dev/joomla-plugins/wt-seo-meta-templates.html

Подробнее..

PHP Дайджест 205 (1 15 июня 2021)

15.06.2021 00:15:33 | Автор: admin


Подборка свежих новостей и материалов из мира PHP. В выпуске: первая альфа PHP 8.1.0, Composer 2.1, Symfony 5.3 и другие релизы. Обзор новых предложений для PHP 8.1: Partial Function Application, pipe оператор, readonly свойства. А также порция полезных инструментов, статьи, видео и подкасты.

Приятного чтения!

Новости


  • PHP 8.1.0 alpha 1


    Вышел первая альфа и тем самым стартовал рели-процесс PHP 8.1. Обновления будут выходить каждые две недели по расписанию. Финальный релиз запланирован на 25 ноября.

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

    • Enum они же перечисления RFC;
    • Новый тип never для возвращаемых значений RFC;
    • Файберы RFC;
    • Финальные константы в классах RFC;
    • Оператор распаковки поддерживает массивы со строковыми ключами RFC;
    • Объявлено устаревшим преобразование float в int, где теряется дробная часть RFC;
    • Интерфейс Serializable объявлен устаревшим RFC;
    • Запись восьмеричных чисел с префиксом 0o RFC;
    • Ограничено использование $GLOBALS RFC;

    Полный список изменений можно посмотреть на php.watch/versions/8.1.

  • PHP 8.0.7, PHP 7.4.20


    Багфикс релизы актуальных веток.

  • Стартовала программа раннего доступа PhpStorm 2021.2


    Каждую неделю публикуем новые билды, которые можно использовать бесплатно. А также анонсируем то, над чем идет работа в релизе.
    Уже доступны: поддержка енамов PHP 8.1, переработанный и улучшенный рефакторинг Extract Method, исправлены ошибки форматирования.

  • Composer 2.1.0


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

  • У каждого пакета на packagist.org теперь есть статистика по PHP-версиям


    Один из авторов Composer, Jordi Boggiano, каждые полгода публиковал в блоге пост со статистикой используемых версий PHP.

    Теперь вместо блога, эта общая статистика всегда доступна на packagist.org/php-statistics.

    Кроме того, у каждого пакета есть своя подобная страница, например, symfony/console/php-stats.

  • PHP Russia 2021


    Конференция состоится уже 28 июня. Программа сформирована habrничего лишнего, только хардкор, только технологии.

    Для читателей дайджеста есть промокод со скидкой: php_digest.


PHP Internals


  • [RFC] Partial Function Application


    Предложение было существенно переработано и объединено с более узким RFC от Никиты First-class callable syntax.

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

    Итого предлагается три способа получить ссылку на функцию:
    1. $func = some_func(...) так можно получить ссылку на любую функцию. Собственно, предложение Никиты.
    2. $func = some_func(1, 2, ?, 5) так можно получить ссылку с одним аргументом, что может быть полезно для различных колбэков.
    3. $func = any_func($all, $params, ...) так можно передать все аргументы в функцию, но при этом не вызывать ее. Ссылку позже можно вызвать, не передавая никаких параметров.

  • [RFC] Pipe Operator v2


    Если предложение выше пройдет голосование, то пайп-оператор станет его логичным продолжением.

    Вместо вложенных вызовов типа:

    array_filter(array_map('strtoupper', str_split(htmlentities("Hello World"))), fn($v) => $v != 'O');
    

    можно будет писать более понятные цепочки вида:

    $result = "Hello World"    |> htmlentities(?)    |> str_split(?)    |> array_map(strtoupper(?), ?)    |> array_filter(?, fn($v) => $v != 'O');
    

  • [RFC] Pure intersection types


    Предложение добавить пересечения типов находится на голосовании и похоже, что преодолеет необходимый порог. Тем временем можно послушать подкаст audioPHP Internals News #88 с George Peter Banyard, автором RFC.

  • [RFC] Readonly properties 2.0


    В качестве альтернативы довольно сложному и громоздкому предложению по акссессорам свойств сам же Никита выдвинул на рассмотрение RFC по readonly свойствам.

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

    Скрытый текст
    class Test {    public readonly string $prop;    public function __construct(string $prop) {        // Legal initialization.        $this->prop = $prop;    }}$test = new Test("foobar");// Legal read.var_dump($test->prop); // string(6) "foobar"// Illegal reassignment. It does not matter that the assigned value is the same.$test->prop = "foobar";// Error: Cannot modify readonly property Test::$prop
    


    А в комбинации с constructor property promotion из PHP 8.0, можно будет сократить вообще до вот такого:

    class User {    public function __construct(        public readonly string $name    ) {}}$user = new User('Roman');echo $user->name; // Ok$user->name = 'Nikita'; // Error
    

  • [RFC] Make reflection setAccessible() no-op


    Сейчас чтобы получить доступ к свойству или методу через рефлексию, надо обязательно предварительно вызвать ->setAccessible(true).

    Marco Ocramius Pivetta предлагает убрать этот вызов, то есть ReflectionProperty и ReflectionMethod будут вести себя так, как если бы уже был вызван setAccessible(true).

    class Foo { private $bar = 'a'; }(new ReflectionProperty(Foo::class, 'bar'))->getValue();
    



Инструменты


  • nunomaduro/php-interminal Инструмент для чтения PHP Internals обсуждений в терминале. Пока умеет выводить только последние сообщения, но выглядит красиво.
  • joonlabs/php-graphql PHP-реализация спецификаций GraphQL. Автор утверждает, что быстрее чем другие реализации.
  • spiral/attributes Позволяет читать атрибуты из PHP 8 на PHP 7.2+ и дополнительно может работать с аннотациями доктрины. Фреймворк-агностик и для работы требует лишь nikic/php-parser. Прислал SerafimArts.
  • spiral/storage Компонент для работы с распределёнными файловыми хранилищами. Работает поверх thephpleague/flysystem и предоставляет более удобный API. Прислал SerafimArts.
  • kalessil/production-dependencies-guar Предотвращает добавление дев-пакетов в секцию require в composer.json.

    В тему у Валентина Удальцова на канале Пых была заметка с идеями проверок на CI.


Symfony




Laravel




Yii




Статьи




Аудио/Видео





Подписывайтесь на Telegram-канал PHP Digest.

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

Заметили ошибку или опечатку? Сообщите в личку хабра или телеграм.

Прислать ссылку можно через форму или просто написав мне в телеграм.
Поиск ссылок по всем дайджестам
Предыдущий выпуск: PHP-Дайджест 204

Подробнее..

Как мы весь интернет сканировали

20.06.2021 16:11:04 | Автор: admin

Всем привет! Меня зовут Александр и я пишу код для 2ip.ru. За добрую половину сервисов можно пинать меня, готов отбиваться. Cегодня я хочу немного рассказать про переделку одного нашего старого сервиса. Это конечно не "big data", но всё равно довольно большие объемы информации, поэтому думаю будет интересно.

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

Как это всегда работало? Мы ходили в Bing с большого пула адресов и парсили выдачу по специальному запросу. Да, решение так себе, но что было то было. Было, потому что бинг прикрутил гайки и мы решили всё это сделать по человечески.

Своя база

Что если взять и спарсить весь интернет? В общем то не проблема, но мы не Google и больших ресурсов для кролинга не имеем. Или имеем?

Есть сервер с 12 ядрами и 64 гигами памяти, а в арсенале MySQL, PHP, golang и куча всяких фреймворков. Очевидно, что благодаря горутинам можно достичь неплохих результатов. Golang быстр и требует минимум ресурсов. По базе вопросы, потянет ли это все обычный MySQL?

Пробуем.

Делаем прототип

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

Итак на моем диске CSV файл размером 5 ГБ, дело за малым, написать масс ресолвер, который будет читать строку за строкой, а на выход в STDOUT, отдавать пару "домен - IP адрес"

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

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

func main() {    file, err := os.Open("domains.txt")    if err != nil {        log.Fatal(err)    }    defer file.Close()    maxGoroutines := 500    guard := make(chan struct{}, maxGoroutines)    scanner := bufio.NewScanner(file)    for scanner.Scan() {        guard <- struct{}{}        host := scanner.Text()        go func(host string) {            resolve(host)            <-guard        }(host)    }    if err := scanner.Err(); err != nil {        log.Fatal(err)    }}

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

Функция resolve опущена, но кратко это обычный ресолвер IP с выдачей результата в STDOUT. Обращаемся к DNS, получаем A записи, выдаем результат.

DNS

Прогуглив немного я понял, что большие DNS особо не лимитируют количество запросов с одного IP, но кто знает. Поэтому было принято решение поднять undbound из Docker.

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

Второй вариант Google DNS, тот который четыре восьмерки, оказался гораздо быстрее. У меня были опасения по лимитам в 500 запросов в секунду но по факту их нет.

Тестируем в localhost и на проде

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

1000 горутинов упали на 12 ядрах, а вот 500 практически не грузили проц и работали стабильно. Мощность получилась на уровне ~2000 доменов в секунду.

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

В конечном счёте я оставил процесс в tmux и через трое суток получил CSV размером 10 Гб. Идём дальше.

Ура! Переходим к следующему шагу.

База данных

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

IP - это обычный BIGINT domain - VARCHAR 255

Индексы

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

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

Партиципирование

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

Я разделил весь пул IP адресов на 20 таблиц с шагом 200 млн. Получилось примерно так:

ALTER TABLE domain_ip PARTITION BY RANGE COLUMNS (ip)  (    PARTITION p0 VALUES LESS THAN (200000000),    PARTITION p1 VALUES LESS THAN (400000000),    PARTITION p2 VALUES LESS THAN (600000000),    PARTITION p3 VALUES LESS THAN (800000000),    PARTITION p4 VALUES LESS THAN (1000000000),    PARTITION p5 VALUES LESS THAN (1200000000),    PARTITION p6 VALUES LESS THAN (1400000000),    PARTITION p7 VALUES LESS THAN (1600000000),    PARTITION p8 VALUES LESS THAN (1800000000),    PARTITION p9 VALUES LESS THAN (2000000000),    PARTITION p10 VALUES LESS THAN (2200000000),    PARTITION p11 VALUES LESS THAN (2400000000),    PARTITION p12 VALUES LESS THAN (2600000000),    PARTITION p13 VALUES LESS THAN (2800000000),    PARTITION p14 VALUES LESS THAN (3000000000),    PARTITION p15 VALUES LESS THAN (3200000000),    PARTITION p16 VALUES LESS THAN (3400000000),    PARTITION p17 VALUES LESS THAN (3600000000),    PARTITION p18 VALUES LESS THAN (3800000000),    PARTITION p19 VALUES LESS THAN (4000000000),    PARTITION p20 VALUES LESS THAN (MAXVALUE) );

И как вы поняли это сработало, иначе зачем эта статья? :)

Импорт

Кто работал с MySQL знает, что вливать большие дампы данных это довольно долгая операция. За долгие годы работы я не нашел ничего лучше, чем импорт данных из CSV. Выглядит это примерно так:

LOAD DATA INFILE '/tmp/domains.csv' IGNORE INTO TABLE domain_ipFIELDS TERMINATED BY ',' LINES TERMINATED BY '\n'

Машина переваривает CSV размером ~10 Гб за 30 минут.

Финал

Как результат получился вот такой милый сервис. Выборка из ~300 миллионов записей происходит мгновенно на довольно скромном по нынешним меркам сервере. Оперативной памяти нужно под это всё порядка 8 Гб.

Теперь можно узнать например, что к IP 8.8.8.8 человечество прицепило 8194 домена, ну или придумайте сами ... ;-)

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

Подробнее..

Категории

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

© 2006-2021, personeltest.ru