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

Разработка приложений

Жулики против разработчиков приложений Apple. Как потерять два миллиона долларов и не подать виду

08.02.2021 12:21:23 | Автор: admin


Алло, Хьюстон, у нас проблемы! Вернее, не у нас, а у вас. И не в Хьюстоне, а в Купертино, или где там теперь базируется офис Apple, занимающийся поддержкой магазина приложений App Store. Эти самые проблемы возникают регулярно не только у пользователей, но и у разработчиков софта, страдающих от действий всевозможных жуликов, c которыми в Apple не могут (или не хотят) ничего поделать. О самых интересных, а также о наиболее распространённых способах мошенничества в App Store в сегодняшней статье.


Внимание: фейкоделы!


Об одном из таких случаев рассказал мировой общественности независимый программист Коста Элефзеиру (Kosta Eleftheriou), подсчитавший, что за минувший год жулики украли у него и других авторов ПО примерно 2.000.000 $. Его грустная история, которой он поделился в Твиттере, вызывает сначала удивление, а затем оторопь: неужели так бывает на самом деле? Оказывается, бывает.



Коста разработчик экранной клавиатуры для Apple Watch, причём, как он утверждает, лучшей экранной и клавиатуры в своем роде. Он потратил долгие месяцы, создавая и совершенствуя своё детище, прежде чем оно было опубликовано в App Store. Коста преследовал две цели: во-первых, его приложение должно позволить набирать текст на часах с высокой скоростью, а во-вторых, опережать по своим функциям и удобству ближайших конкурентов. Пользователи по достоинству оценили эту разработку: в 2020 году виртуальная клавиатура Косты поднялась в топ платных приложений App Store для Apple Watch. По словам программиста, наиболее сложной частью его разработок были алгоритмы автозамены, использовавшиеся совместно с методом выбора символов свайпами. Благодаря этой технологии владельцам Apple Watch удалось приблизиться в скорости набора текста к пользователям iPhone. Все конкурирующие приложения по этому параметру плелись далеко позади.

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

Вскоре после попадания программы в топ App Store неизвестные жулики создали и разместили в каталоге Apple ее клон, в точности копировавший интерфейс приложения, но не имевший никаких полезных функций. Фактически программа просто показывала красивые картинки на экране часов, и больше не делала ничего полезного. Затем злоумышленники начали агрессивно рекламировать свою поделку в Facebook и Instagram, используя имя Косты Элефзеиру и снятое им демонстрационное видео, на котором показана работа настоящего приложения.

Но если программа толком не работает, как мошенники заставляли пользователей платить за нее? Очень просто: после запуска мошенническое приложение демонстрировало на экране пустое окно с кнопкой Разблокировать сейчас, по нажатию на которую потенциальная жертва жуликов оформляла подписку стоимостью $416 в год.



Однако если программа толком не работает и при этом стоит неприличных денег, пользователи должны очень быстро раскусить обман, и напихать разработчикам полную панамку негативных фидбеков. Так бы, безусловно, и произошло, если бы мошенники не публиковали в каталоге Apple купленные и откровенно фейковые пятизвездочные отзывы. Среди них, например, можно найти хвалебную рецензию юзера, безмерно довольного тем, что поделка мошенников прекрасно обрабатывает нажатие Control+Alt+Del, позволяющее вызвать на экран Apple Watch диспетчер задач Windows. И ещё одну, в которой указано, что экранная клавиатура для часов умеет переключаться в альбомную ориентацию, из-за чего на ней очень удобно набирать команды в терминале (wat?). Фальшивые пользователи делятся своим опытом запуска приложения для Apple Watch на iPhone, хвалят функции, которых в оригинальной программе попросту нет, и сравнивают ее с несуществующими клавиатурами для часов Apple от Google и Microsoft. Некоторые обзоры повторяли друг друга слово в слово, отличались только имена их авторов.



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

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

Одновременно с бурным развитием магазинов приложений, таких как App Store и Google Play, появилась на свет и стала набирать обороты услуга под названием ASO Application Search Optimisation (или App Store Optimisation применительно к каталогу Apple). По большому счету, это разновидность SEO, но ориентированная на продвижение приложений. То есть, комплекс мер, направленный на повышение позиции программы в поисковой выдаче каталога с целью опережения конкурентов по числу установок. Как и в случае с SEO, здесь используются белые и черные приемы. Например, белые методы включают правильную настройку заголовков, ключевых слов, оптимизацию описания на странице программы, правильное размещение скриншотов. С примером черного ASO нас познакомил Коста Элефзеиру.

Случай с программой Коста далеко не единственный в App Store. Жулики клонируют многие популярные программы, при этом отзывы на их страницах буквально сделаны под копирку. Удивительно, но в настоящее время Apple не предпринимает активных мер для борьбы с подобными явлениями, вернее, эти меры явно недостаточны для того, чтобы защитить честных разработчиков от мошенников, использующих их имя с целью наживы. Фактически, у пользователя нет возможности сообщить в Apple о мошенническом приложении магазин перенаправит его в службу поддержки, населенную, как планета Железяка из повести Кира Булычева, роботами, и заточенную на решение технических проблем.

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

Возмещения покупок и restore-from-backup scam


Один из самых распространенных методов монетизации приложений, представленных в App Store InApp Purchase, то есть, встроенные покупки. К таковым относится премиум-контент, подписки и прочие цифровые товары. В App Store существует механизм refunds возмещения пользователю стоимости внутрипрограммной транзакции, если он сообщит в Apple, что совершил покупку по ошибке, или на самом деле не собирался ничего платить после установки приложения.

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

На самом деле, по словам самих разработчиков, Apple редко возвращает пользователям платежи за сonsumables электронные покупки, совершаемые внутри приложения на постоянной основе в течение всего его жизненного цикла, например, за приобретение игровой валюты. Более того, в отличие от приложений Android, где встроенные покупки consumable и non-consumable отличаются по большому счету только вызовами API, в Apple требуют заранее выбрать нужный формат при настройке IAP.



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

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

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

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

Последнее время критика App Store со стороны разработчиков звучит всё громче. Своё мнение не стесняются высказывать известные медийные персоны и признанные эксперты в области IT. Принесет ли это какой-либо результат, и поможет ли изменить политику Apple в отношении разработчиков программного обеспечения? Время покажет.

Подробнее..

Проектирование ПО с учетом требований стандартов безопасности

27.01.2021 20:23:33 | Автор: admin

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

Основной материал подготовлен и составлен на основе требований стандарта PCI DSS. Данные требования также могут быть применены к обработке и хранению персональных данных в части выполнения требований GDPR.

Мой 12 летний опыт подготовки и успешного прохождения аудитов в разных странах мира показывает, что многие компании, которые занимаются разработкой ПО имеют системы и решения собственной разработки, которые обрабатывают карточные (и персональные) данные. А со стороны PCI Council есть даже отдельный стандарт PA DSS, который регламентирует требования к тиражируемому программному обеспечению. Вот только, большинство компаний в моей практике, будь то США, Британия или Китай, которые проходили аудит PCI DSS, не имели планов по тиражированию и продаже ПО. Более того, компании специально вносят ряд изменений в ПО используемое в рамках определенного проекта, чтобы не проходить аудит PA DSS, если это ПО внедряется на заказ. Потому не всегда выполнение требований стандарта и прохождение сертификации желанно и оправдано, а в данной статье приведены именно упомянутые стандарты, а не требования PA DSS.

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

Итак, давайте перейдем к детализации и описанию требований.

Общие разделы стандарта PCI DSS.

Требования PCI DSS (на основе требований PCI DSS 3.2.1)

1. Минимизировать места и сроки хранения критичных данных. В контексте данной публикации критичные данные это данные, безопасность которых регламентируется требованиями PCI DSS + GDPR (карточные и персональные данные).

Нужныли таковые данные впринципе или ихможно анонимизировать? Действительноли необходимо хранить критичные данные втаком объеме? Можноли избежать дублирования данных икак обеспечить ихминимизацию?

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

2. Критичные данные в базах данных, рекомендовано хранить в зашифрованном виде.

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

3. Клиентские сетевые потоки данных должны передаваться в зашифрованном виде.

Критичные данные должны передаваться по шифрованному каналу. Сетевые критичные данные должны передаваться по шифрованному каналу. Формально достаточно использование протокола безопасности TLS, но лучше обеспечить шифрование передаваемой информации на уровне ПО.

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

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

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

5. Базы данных должны размещаться в отдельной подсети от подсети приложений.

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

6. Все тестовые параметры при переводе в эксплуатацию должны быть удалены.

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

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

Не все алгоритмы шифрования считаются стойкими.

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

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

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

9. Проверка на OWASP TOP 10.

Необходимо проверять решение на основные уязвимости безопасности перед передачей его в эксплуатацию. Сам данный пункт достоин не то что одной, а целой серии статей от процесса проверок внутри Компании до внедрения систем Bug Bounty. Для начала рекомендуется проверять хотя бы на OWASP TOP 10.

10. Использование персонифицированных учетных записей.

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

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

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

12. Система разграничения полномочий с минимально необходимыми правами.

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

13. Шифрование безопасными ключами.

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

14. Требования к стойкости пароля.

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

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

Подробнее..

Как следить (наблюдать) за компьютером. Часть 1 делаем скриншоты пользователей

18.02.2021 00:06:58 | Автор: admin

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

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

Необходимый функционал:

  1. Программа должна делать скриншоты

  2. Программы не должно быть видно на панели задач

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

  4. Возможность задавать путь к директории хранилища скриншотов

  5. Возможность задавать максимальный размер хранилища скриншотов

  6. Очистка самых старых файлов из хранилища скриншотов при достижении максимума в хранилище

  7. Логирование работы программы

Алгоритм работы программы:

Код работы метода Main программы:

 static void Main(string[] args) {     Log.Instance.Info($"started");     // Скрыть окно программы     var handle = GetConsoleWindow();     ShowWindow(handle, SW_HIDE);     // Чтение конфигурации     ReadSettings();     // Запуск бесконечной работы скриншотов      while (true)     {          // Проверить хранилище          CheckStorage();          // Выполнить скриншот          DoScreen();          // Подождать интервал времени          Thread.Sleep(_interval * 1000);     } }

Далее необходимо разработать следующие методы:

ReadSettings - Чтение конфигурации
CheckStorage - Проверить хранилище
DoScreen - Выполнить скриншот

ReadSettings (чтение конфигурации)

Для создания файла конфигурации необходимо в Visual Studio открыть свойства проекта и перейти в меню Параметры. Здесь необходимо прописать какие переменные будут вынесены в файл конфигурации и задать их типы и значения.
interval - Как часто делать скриншот, в секундах
limit - Размер хранилища скриншотов, в MB
path - Путь к хранилищу скриншотов, пример C:\temp

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

/// <summary>/// Как часто делать скриншот, в секундах/// </summary>static int _interval { get; set; }/// <summary>/// Размер хранилища скриншотов, в MB/// </summary>static int _limit { get; set; }/// <summary>/// Путь к хранилищу скриншотов, пример C:\temp/// </summary>static string _path { get; set; }

Затем прочитаем настройки программы из файла конфигурации Screenshoter.exe.config который лежит около исполняемого файла приложения Screenshoter.exe. Запишем прочитанные настройки в выше созданные поля.

/// <summary>/// Чтение настроек из файла конфигурации/// </summary>static void ReadSettings(){    _interval = 10;    if (Properties.Settings.Default.interval > 0) _interval = Properties.Settings.Default.interval;    Log.Instance.Info($"set interval = {_interval} sec");    _limit = 20;    if (Properties.Settings.Default.limit > 0) _limit = Properties.Settings.Default.limit;    Log.Instance.Info($"set storage = {_limit} Mb");    _path = @"C:\temp";    if (!string.IsNullOrEmpty(Properties.Settings.Default.path)) _path = Properties.Settings.Default.path;    Log.Instance.Info($"set path = {_path}");}

CheckStorage (Проверка хранилища)

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

/// <summary>/// Проверка доступного места в хранилище/// </summary>static void CheckStorage(){    var currentSize = StorageSize();    if (currentSize > _limit)    {        // Сколько нужно очистить MB        var totalToTrash = currentSize - _limit;        // Очистить необходимое кол-во KB        StorageClear(totalToTrash * 1024);    }}

Дополнительно приведу методы StorageSize и StorageClear.

StorageSize принимает аргумент насколько нужно очистить в KB. Почему в KB (килобайтах), а не в MB (мегабайтах) ? 1 скриншот занимает в хранилище размер меньший чем 1 Мегабайт, а значит корректнее удалять в Килобайтах чтобы не удалять за 1 раз например 5 скриншотов чтобы после этого в хранилище был записан всего 1 скриншот.

Метод StorageSize подсчитывает размер всех файлов в директории хранилища, без учета вложенных директорий (не сложно добавить если нужно).

/// <summary>/// Заполненность хранилища, в MB/// </summary>/// <returns></returns>static long StorageSize(){    long i = 0;    try    {        DirectoryInfo directory = new DirectoryInfo(_path);        FileInfo[] files = directory.GetFiles();        foreach (FileInfo file in files)        {            i += file.Length;        }    }    catch (Exception ex)    {        Log.Instance.Error(3, ex.Message);        return _limit;    }    return i /= (1024 * 1024);}

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

/// <summary>/// Очистка хранилища/// </summary>/// <param name="sizeKb"></param>static void StorageClear(long sizeKb){    try    {        Log.Instance.Info($"clear = {sizeKb} Kb");        DirectoryInfo directory = new DirectoryInfo(_path);        FileInfo[] files = directory.GetFiles().OrderBy(f => f.CreationTime).ToArray();        foreach (FileInfo file in files)        {            var size = file.Length / 1024;            File.Delete(file.FullName);            sizeKb -= size;            if (sizeKb <= 0) break;        }    }    catch (Exception ex)    {        Log.Instance.Error(2, ex.Message);    }}

DoScreen (Создание скриншота)

Данный метод пытается создать скриншот в хранилище скриншотов. Формат создаемых файлов - PNG. В файл попадает весь экран основного монитора пользователя. Если создать файл не удалось, то отправка в класс Log сообщения об ошибке.

/// <summary>/// Создание скриншота/// </summary>static void DoScreen(){    try    {        Bitmap printscreen = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);        Graphics graphics = Graphics.FromImage(printscreen as Image);        graphics.CopyFromScreen(0, 0, 0, 0, printscreen.Size);        printscreen.Save(Path.Combine(_path, GetFileName()), System.Drawing.Imaging.ImageFormat.Png);    }    catch (Exception ex)    {        Log.Instance.Error(1, ex.Message);    }}/// <summary>/// Имя файла создаваемого скриншота/// </summary>/// <returns></returns>static string GetFileName(){    var time = DateTime.Now;    return $"{time.ToString("yyyy_MM_dd__HH_mm_ss")}.png";}

Чтобы программы не было видно на панели задач необходимо окно программы спрятать. Вызов данного функционала происходит из метода Main.

#region DllImport[DllImport("kernel32.dll")]static extern IntPtr GetConsoleWindow();[DllImport("user32.dll")]static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);const int SW_HIDE = 0;const int SW_SHOW = 5;#endregion

Логирование действий программы состоит в виде статического класса Log сделанного согласно паттерну Singleton (одиночка).

public sealed class Log{    private static volatile Log _instance;    private static readonly object SyncRoot = new object();    private readonly object _logLocker = new object();    private Log()    {        CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;        LogDirectory = Path.Combine(CurrentDirectory, "log");    }    public string CurrentDirectory { get; set; }    public string LogDirectory { get; set; }    public static Log Instance    {        get        {            if (_instance == null)            {                lock (SyncRoot)                {                    if (_instance == null) _instance = new Log();                }            }            return _instance;        }    }    public void Error(int errorNumber, string errorText)    {        Add($"Ошибка {(errorNumber.ToString()).PadLeft(4, '0')}: {errorText}", "[ERROR]");    }    public void Info(string log)    {        Add(log, "[INFO]");    }    private void Add(string log, string logLevel)    {        lock (_logLocker)        {            try            {                if (!Directory.Exists(LogDirectory))                {                    // Создание директории log в случае отсутствия                    Directory.CreateDirectory(LogDirectory);                }                // Запись в лог файл вместе с датой и уровнем лога.                string newFileName = Path.Combine(LogDirectory, String.Format("{0}.txt", DateTime.Now.ToString("yyyyMMdd")));                File.AppendAllText(newFileName, $"{DateTime.Now} {logLevel} {log} \r\n", Encoding.UTF8);            }            catch { }        }    }}


Код программы здесь

Подробнее..

Создаем Booking приложение с Webix UI

12.04.2021 16:18:43 | Автор: admin
Webix UIWebix UI

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

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

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

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

Немного о Webix и его возможностях

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

Источник UI магии

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

Итак, для того, чтобы начать использовать библиотеку, нужно сперва получить необходимые файлы (они же источники webix-магии). Для этого переходим на страницу загрузки, вводим необходимые данные и получаем ссылку на скачивание заветного zip-файла. Скачиваем и распаковываем его. Внутри находятся файлы license.txt, readme.txt и whatsnew.txt, которые могут заинтересовать тех, кто любит глубоко погрузиться в изучение вопроса. Кроме этого, в папке samples можно посмотреть примеры того, какие полезные вещи можно сотворить при помощи Webix UI.

Больше всего нас интересует содержимое папки codebase, а именно, два сакральных файла: webix.js и webix.css. Для того, чтобы Webix-магия начала действовать, нужно включить их в index.html файл будущего проекта:

<!DOCTYPE html><html>    <head>      <title>Webix Booking</title>      <meta charset="utf-8">      <link rel="stylesheet" type="text/css" href="codebase/webix.css">      <script type="text/javascript" src="codebase/webix.js"></script>    </head>    <body><script type="text/javascript">...</script>    </body></html>

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

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

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

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

Теперь давайте перейдем непосредственно к работе с Webix.

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

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

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

Создаем приложение Booking

Интерфейс нашего приложение будет состоять из следующих частей:

  • Тулбар

  • Форма поиска рейсов

  • Таблица рейсов

  • Форма заказа.

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

Лейаут

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

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

Сначала разделим наш лейаут на 2 одинаковых ряда. Для этого воспользуемся свойством rows:

webix.ui({    rows: [        { template:"Row One" },        { template:"Row Two" }    ]});

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

В этом примере с помощью выражения template:"Row One" мы создали простой контейнер, в который можно поместить любой HTML-контент.

В верхний ряд этого лейаута мы поместим Тулбар. В нижнем будут находиться 2 сменяемых модуля:

  • Модуль поиска рейсов

  • Модуль заказа рейсов.

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

webix.ui({  rows: [          { template:"Toolbar", height:50},          {              cols:[                { template:"Search Form" },                { template:"Data Table" }              ]          }  ]});

Теперь лейаут будет иметь следующий вид:

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

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

Здесь мы снова воспользуемся уже знакомым атрибутом cols. Стоит напомнить, что компоненты внутри cols и rows по умолчанию разделены тонкой серой линией. Такой разделитель между формой и спейсером нам ни к чему. Webix позволяет управлять им с помощью свойства type:

webix.ui({  rows: [    { template:"Toolbar", height:50 },    {      type:"clean", //убираем линию-разделитель      cols:[        { template:"Order Form" },        {}      ]    }  ]});

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

Мы создали отдельные лейауты для модулей поиска и заказа рейсов. Теперь нужно сделать их сменяемыми. Как вы уже догадались, Webix предусматривает и такую возможность. Реализуется она с помощью multiview компонента.

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

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

webix.ui({  rows:[    { template:"Toolbar", height:50 },    {      cells:[        {          id:"flight_search",          cols:[            {template:"Search Form"},            {template:"Data Table"},          ]        },        {          id:"flight_booking",          type:"clean",          cols:[            {template:"Order Form"},            {},          ]        }      ]    }  ]});

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

Тулбар

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

В файле toolbar.js создаем компонент с помощью следующей строки:

const toolbar = { view:"toolbar" };

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

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

{  view:"toolbar"  css:"webix_dark", //стилизация  cols:[    {      id:"label", //указываем id для обращения      view:"label",      label:"Flight Search", //название    }  ]}

Выше мы упоминали, что вставлять компоненты друг в друга необходимо при помощи свойств rows и cols. Поэтому компонент label мы включаем в массив свойства cols компонента toolbar.

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

Вот такими нехитрыми маневрами мы создали тулбар нашего приложения.Чтобы использовать компонент в лейауте, нужно сначала подключить файл toolbar.js в index.html и включить переменную в массив свойства rows вместо {template:Toolbar}:

webix.ui({rows: [    toolbar,    {      cells:[        ...      ]    }]});

На странице браузера мы увидим лейаут с интерфейсом тулбара:

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

  • Форма поиска рейсов

  • Таблица рейсов.

Давайте рассмотрим их детально.

Форма поиска рейсов

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

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

Webix предлагает сделать это гораздо проще. Давайте с этим разбираться.

Создадим нашу форму в файле search_form.js с помощью компонента form:

const search_form = { view:form };

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

{  view:form,  ...  elements:[     { /*элемент формы*/ },    { /*элемент формы*/ },    ...  ]  ...}

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

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

А сейчас нужно определиться с элементами, которые мы будем реализовывать. Чтобы выбрать пункты отправления и назначения, необходимо установить селекторы выбора городов. Также мы будем искать рейсы по дате отправления и возвращения. Контрол даты возвращения нужно спрятать и отображать его при необходимости. Реализуем это с помощью радиокнопок One Way и Return. При поиске нужно учитывать количество необходимых билетов. Для этого установим специальный счетчик. Ну и конечно, как же без кнопок управления формой Reset и Search. В итоге наша форма будет иметь следующий вид:

Теперь можно приступить к реализации задуманного.

Радиокнопки

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

{  view:"radio",  label:"Trip",  name:"trip_type",  value:1,  options:[    { id:1, value:"One-Way" },    { id:2, value:"Return" }  ]}

Значения радиокнопок задаем через свойство options. Помимо этого, указываем название компонента через свойство label, а имя, по которому будем получать значение, через name. По умолчанию мы будем искать полеты только в одну сторону, поэтому устанавливаем для value значение именно этой опции.

Селекторы выбора городов

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

{    view:"combo",    id:"from",    clear:"replace",    ...    placeholder:"Select departure point",    options:cities_data}

Иногда пользователю необходимо подсказать, что нужно делать с тем или иным контролом. И тут на помощь нам приходит свойство placeholder. Мы указываем нужные действия, а приложение отобразит их в поле необходимого элемента. Когда пользователь ввел или выбрал нужные данные, но потом вдруг передумал, а такое случается довольно часто, мы предоставим ему возможность очистить поле одним кликом. Для этого нужно задать свойство clear в значении replace. Теперь, когда поле будет заполнено, в правой его части появится иконка, при клике по которой введенные ранее данные исчезнут (поле будет очищено).

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

Но дело в том, что эти же данные нам нужны для нескольких компонентов, а загружать их для каждого по отдельности будет несколько затратно. Webix решает эту проблему с помощью такой сущности, как DataCollection. Внутри этого компонента необходимо всего лишь указать URL, по которому будут загружаться данные. Информация загрузится один раз и будет доступна для многоразового использования. В нашем случае объект с данными хранится в файле ./data/cities.json. Давайте создадим коллекцию и для удобства сохраним ее в переменную:

const cities_data = new webix.DataCollection({  url:"./data/cities.json"});

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

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

Селекторы выбора дат

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

{  view:"datepicker",  name:"departure_date",  ...  format:"%d %M %Y"}

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

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

Счетчик количества билетов

Представим себе ситуацию, что наш потенциальный пользователь летит в долгожданный отпуск не один, а со всей семьей или компанией друзей. В таком случае ему понадобится не один билет, а некое энное количество. Давайте позволим ему устанавливать необходимое число билетов. Для этого предусмотрен специальный элемент counter. Использовать его очень удобно. С правой и левой стороны находятся иконки со знаками плюс и минус, с помощью которых и происходит увеличение или уменьшение значения. Нужно также установить минимальное значение через свойство min, чтобы пользователь не смог установить меньше 1 билета (согласитесь, это выглядело бы абсурдно). Код элемента будет выглядеть следующим образом:

{ view:"counter", name:"tickets", label:"Tickets", min:1 }

Кнопки поиска и сброса формы

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

Для этого мы предусмотрим соответствующие кнопки Search и Reset. Определяем их с помощью элемента button. Название кнопок указываем через value, а стилизацию добавляем через свойство css. Здесь нужно уточнить, что библиотека предусматривает встроенную стилизацию кнопок. Мы будем использовать класс "webix_primary" для стилизации кнопки Search. Подробнее о стилизации кнопок можно узнать здесь.

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

{  cols:[    { view:"button", value:"Reset" },    { view:"button", value:"Search", css:"webix_primary" }  ]}

Интерфейс формы поиска мы создали. Теперь давайте интегрируем его в наш лейаут - вы еще помните о нём? Для этого, как и в примере с тулбаром, нужно включить файл search_form.js в index.html. Интерфейс формы хранится в переменной search_form, которую мы и пропишем в лейауте:

[  toolbar,  {    cells:[      {        id:"flight_search",        cols:[          search_form,          { template:"Data Table" }        ]      }, ...    ]  },]

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

Таблица рейсов

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

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

В файле datatable.js создаем таблицу рейсов с помощью компонента datatable:

const flight_table = {   view:"datatable",  url:flights_data};

Таблицу нужно заполнить данными о расписании рейсов. В нашем случае они хранятся в файле ./data/flights_data.json. Одно из преимуществ работы с таблицами Webix заключается в том, что можно просто указать путь к файлу или скрипту, а компонент сам сделает запрос, загрузит данные и отобразит их в соответствии с настройками. Проще только вообще ничего не делать.

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

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

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

Конфигурация столбцов происходит в массиве свойства columns:

columns:[  { /*конфигурации столбца*/ },  { /*конфигурации столбца*/ },  ...]

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

У столбца должно быть название, или по-простому шапка. Шапку мы задаем при помощи свойства header.

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

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

Давайте рассмотрим пример с датами.

Дело в том, что их значения хранятся и загружаются в виде строк типа "2021-03-26". Как вам должно быть известно, оперировать строчными датами не самое приятное занятие (для некоторых даже болезненное). Исходя из этого, можно сразу при загрузке перевести их в JS Date объекты. С помощью свойства scheme мы можем переопределить все строки указанного поля (данные которого попадут в столбец с таким же id) в соответствующие объекты Date перед их загрузкой в таблицу:

scheme:{  $init:function(obj){    obj.date = webix.Date.strToDate("%Y-%m-%d")(obj.date);  }}

Теперь значения дат попадают в столбец Date в виде объектов, а не строк. Так будет гораздо удобнее фильтровать рейсы при поиске билетов. Но на этом все не заканчивается. Нужно настроить их отображение непосредственно в ячейках столбца. Для этого в конфигурации столбца мы прописываем уже упомянутое свойство format в значении webix.i18n.longDateFormatStr. Теперь полученный объект Date преобразуется в заданное свойством format значение и отображается как 26 March 2021. Вот такая вот магия.

Столбцы, которые отображают названия городов, также заслуживают особого внимания. Дело в том, что данные о пунктах отправления и назначения хранятся и приходят в виде чисел. Зачем такие сложности спросите вы? Ответ простой. Сами названия городов хранятся в отдельной серверной таблице как id - value, а в данных о рейсах вместо названий хранятся только id городов. Чтобы получить название города по его id, нам нужно воспользоваться свойством collection.

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

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

Но развлечения мы оставим на вечер пятницы. Сейчас нужно представить, что потенциальный пользователь ввел данные в форму поиска, нажал кнопку Search и нашел необходимый рейс в таблице (пока нужно только представить, так как реализацией интерактива мы займемся во второй части). Какие дальнейшие действия? Безусловно, нужно как можно быстрее помочь ему забронировать рейс. Как это сделать?

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

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

А мы же продолжаем настраивать таблицу.

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

Для этого мы воспользуемся компонентом search:

const search_bar = { view:"search" };

Это текстовое поле с красивой иконкой поиск.

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

[   toolbar,   {     cells:[       {         id:"flight_search",         cols:[           search_form,           {              rows:{               search_bar,               flight_table             }           }         ]       }, ...     ]   },]

Уже всё по-взрослому. Результат в браузере будет таким:

Форма заказа

Пользователь определился с рейсом и готов сделать заказ. Давайте поможем ему сделать это. Пришло время заняться формой заказа. Она будет отображаться вместо модуля поиска рейсов при клике по кнопке Book в таблице. Форма будет иметь следующий вид:

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

const order_form = {  view:"form",  elements:[    ...  ]};

Теперь давайте опишем нужные нам элементы.

Поля ввода

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

Давайте воспользуемся элементом text и его преимуществами:

elements:[  { view:"text", name:"first_name", , invalidMessage:"First Name can not be empty" },  { view:"text", name:"last_name", , invalidMessage:"Last Name can not be empty" },  { view:"text", name:"email", , invalidMessage:"Incorrect email address" },  ...]

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

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

{  elements:[  ],  rules:{    first_name:webix.rules.isNotEmpty, //поле не должно быть пустым    last_name:webix.rules.isNotEmpty,     email:webix.rules.isEmail //значение должно быть в формате email  }}

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

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

{ ..., invalidMessage:"First Name can not be empty", ... }

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

Счетчик

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

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

{ view:"counter", id:"tickets_counter", name:"tickets", label:"Tickets", min:1 }

Чекбоксы

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

Для этого мы создадим 2 чекбокса Checked-in Baggage и Food и будем их использовать для того, чтобы повышать цену за билет, если пользователь выбрал эти дополнительные плюшки. Чекбокс реализуется с помощью элемента checkbox:

[  { view:"checkbox", name:"baggage", label:"Checked-in Baggage", checkValue:15 },  { view:"checkbox", name:"food", label:"Food", checkValue:10 }]

Через свойство checkValue мы задаем значение отмеченного чекбокса. В нашем случае за дополнительный багаж и питание пользователю придется доплатить 15 и 10 долларов соответственно.

Радиокнопки

Теперь давайте перейдем к удобствам полета. За это у нас отвечают радиокнопки Economy и Business. Реализуются они с помощью уже знакомого нам элемента radio:

{   view:"radio", name:"class", label:"Class",  options:[    { id:1, value:"Economy" },    { id:2, value:"Business" }  ]}

Мы установим класс Economy по умолчанию, а при переключении на Business стандартная цена будет удваиваться. А что вы хотите, за комфорт нужно платить.

Лейбл

Хороший сервис информативный сервис. Именно поэтому нам нужно отображать актуальную информацию о стоимости заказа. Давайте настроим отображение итоговой цены. Реализуется это с помощью уже знакомого нам компонента label:

{ view:"label", id:"order_price" }

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

Кнопки Go Back и Make Order

Наш пользователь наконец-то определился с заказом и готов его сделать или передумал и хочет вернуться назад к поиску. Давайте реализуем такую возможность. Для этого мы создадим соответствующие кнопки Go Back и Make Order с помощью знакомого нам элемента button:

{   cols:[    { view:"button", value:"Go Back" },    { view:"button", value:"Make Order" }  ] }

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

[  toolbar,  {    cells:[      {...},      {        id:"flight_booking",        type:"clean",        cols:[           order_form,            {}        ]  }]}]

Результат в браузере:

Мы описали все элементы интерфейса и создали лейаут. Теперь пришло время оживить наше приложения.

Заставляем приложение работать

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

Форма поиска

Радиокнопки

Итак, в самом верху формы поиска у нас находятся радиокнопки One-Way, которая установлена по умолчанию, и Return, при клике по которой должен отображаться спрятанный селектор даты возвращения. Как нам это реализовать? А все очень просто, нужно установить специальный обработчик на событие переключения между радиокнопками. Для работы с событиями предусмотрены специальные свойства. Самым универсальным из них является свойство on, с помощью которого можно установить обработчик сразу на несколько событий. Выглядит это следующим образом:

{  view:"radio",  ...  on:{    onChange:function(newv){      if(newv == 2){        $$("return_date").show(); //отображаем селектор даты возвращения      }else{        $$("return_date").hide(); //прячем селектор даты возвращения      }    }  }}

Теперь при переключении между радиокнопками функция будет прятать и отображать селектор даты возвращения. Реализуется это с помощью специальных методов show() и hide(), названия которых говорят сами за себя. Универсальный метод $$(id) позволяет получить доступ к соответствующему компоненту, id которого передан в качестве аргумента.

Селекторы городов

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

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

...{  options:cities_data,  on:{    onShow:function(v){      optionsData("from","to");    }  }}

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

function optionsData(first_id, second_id){  const options = $$(first_id).getList(); //получаем объект значений списка  const exception = $$(second_id).getValue(); //получаем выбранное значение  options.filter(obj => obj.id != exception); //фильтруем список}

Здесь мы используем такие полезные методы Combo, как getList() и getValue(). Первый метод получает список опций одного селектора, а второй установленное значение другого. С помощью метода filter() выпадающего листа функция фильтрует список и исключает из него выбранный город (полученный методом getValue).

Теперь перейдем к кнопкам Reset и Search.

Кнопка Search

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

function lookForFlights(){const vals = $$("search_form").getValues(); //получаем объект со значениями полей формыconst table = $$("flight_table"); //получаем доступ к таблице данныхtable.filter(function(obj){ /*условия для фильтрации*/ });}

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

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

{ view:"button", value:"Search", ... click: lookForFlights }

Кнопка Reset

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

Для начала нужно создать соответствующий обработчик:

function resetForm(){const form = $$("search_form"); form.clear(); //очищаем формуform.setValues({trip_type:1}); //устанавливаем значения радиокнопки по умолчанию$$("flight_table").filter(); //сбрасываем данные таблицы по умолчанию}

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

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

{ view:"button", value:"Reset", ... click:resetForm }

С формой поиска мы разобрались. Давайте перейдем к таблице рейсов и строке поиска.

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

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

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

function searchFlight(){  //получаем значение строки поискаconst value = $$("search").getValue().toLowerCase(); const table = $$("flight_table");  //формируем объект с совпадениямиconst res = table.find(function(obj){ /*условия поиска*/ }); table.clearCss("marker", true);  //убираем предыдущие стилиfor(let i = 0; i < res.length; i++){table.addCss(res[i].id, "marker", true); //вешаем  css класс marker}table.refresh(); //перерисовываем таблицу}

Функция получает значение строки поиска через метод getValue() и сравнивает его с данными таблицы. Для этого используется метод find(), вызванный для таблицы рейсов. Ряды таблицы, значения которых совпали с введенными данными, помечаются с помощью css класса marker (стили которого находятся в нашем css файле). После проделанных действий, нужно обязательно обновить представление с помощью метода refresh(), чтобы заданный css класс подсветил нужные ряды.

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

{  view:"search",  id:"search",  ...  on:{    onTimedKeyPress:function(){ //срабатывает при наборе текста      searchFlight(); //анализируем данные таблицы и подсвечиваем совпадения    },    onChange:function(){ //срабатывает при нажатии на иконку очистить      if(!this.getValue()){        $$("flight_table").clearCss("marker"); //убираем подсветку      }    }  }}

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

Таблица рейсов

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

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

Давайте начнем со смены модулей.

При клике на кнопку Book приложение спрячет модуль поиска и отобразит модуль с формой заказа. Нужно напомнить, что при создании лейаута, мы использовали компонент multiview. Наши модули находятся в массиве свойства cells, и каждому присвоен соответствующий id:

cells:[  {    id:"flight_search", ...  },  {    id:"flight_booking", ...  }]

Смена multiview модулей реализуется с помощью вызова метода show() для модуля, который нужно отобразить. Доступ к модулю мы получаем через универсальный метод $$(id). В качестве аргумента передаем id нужного модуля:

$$("flight_booking").show(); //отображаем форму регистрации

Эту строку нам нужно включить в тело нашего обработчика. Давайте его создадим:

function bookFlight(id){//получаем количество искомых билетовconst required_tickets = $$("search_form").getValues().tickets;//получаем количество свободных местconst available_places = flight_table.getItem(id).places;//устанавливаем максимальное количество билетов $$("tickets_counter").define("max", available_places); //устанавливаем необходимые значения в поля формы регистрации$$("flight_booking_form").setValues({//количество билетов должно быть меньше или соответствовать свободным местамtickets:required_tickets <= available_places ? required_tickets : available_places,//устанавливаем значение цены билета в скрытый инпутprice:flight_table.getItem(id).price,//устанавливаем "Эконом" класс по умолчаниюclass:1});$$("flight_booking").show(); //отображаем форму регистрации...}

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

Здесь стоит обратить внимание на такой метод для работы с таблицей, как getItem(). С его помощью мы получаем объект значений ряда таблицы, в котором находится кнопка Book. Из этого объекта мы извлекаем цену билета и количество свободных мест. В качестве аргумента передаем id соответствующего ряда.

Как вы помните, при описании тулбара мы определили, что лейбл будет сменяемым. Сменяться он будет при переходе на другой модуль. Реализуется это с помощью методов define() и refresh().

Метод define() позволяет изменить любое свойство компонента, а метод refresh() обновляет его визуальное представление:

$$("label").define("label", "Flight Booking"); //меняем лейбл на тулбаре$$("label").refresh(); //обновляем новый лейбл

Теперь нужно установить обработчик на событие клика по кнопке Book. При создании, мы присвоили ей класс webix_button и сделали это не просто так. Webix предусматривает специальный хендлер onClick, предназначенный для кликов по элементам ячеек таблицы, которые отмечены тем или иным css классом:

{  columns:[  ],  onClick:{    "webix_button":function(e,id){      bookFlight(id);    }  }}

Теперь все работает и пользователь может переходить к форме заказа.

Форма заказа

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

Изначальную цену мы устанавливаем при клике по кнопке Book в таблице рейсов. Она учитывает стоимость билета и их количество. По мере добавления пользователем плюшек или изменения количества билетов цена будет пересчитываться автоматически. Чтобы это реализовать, нам необходимо прибегнуть к старому доброму свойству on, через которое реализуется подписка на события onChange (реагирует на изменение значений пользователем) и onValues (реагирует на установку значений при клике на кнопку Book):

on:{onChange:function(){orderPrice(this.getValues());},onValues:function(){orderPrice(this.getValues());}}

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

function orderPrice(vals){//получаем количество билетовconst tickets = vals.tickets; //получаем цену с учетом класса и количества билетовconst price = vals.price*1*vals.class*tickets; //получаем стоимость дополнительного багажаconst baggage = vals.baggage * tickets;//получаем стоимость питания const food = vals.food * tickets; //формируем итоговую суммуconst total = price+baggage+food; //отображаем итоговую сумму$$("order_price").setValue(total+"$"); }

Функция-обработчик этих событий получает в качестве аргумента объект со значениями формы через метод getValues(), анализирует их и устанавливает итоговую стоимость внутри лейбла Price через метод setValue(). В отличие от метода setValues(), которому можно передать объект со значениями всех полей формы, setValue() устанавливает только одно значение элементу, для которого вызван.

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

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

function makeOrder(){const order_form = $$("flight_booking_form"); //получаем доступ к формеif(order_form.validate()){ //запускаем валидацию формыwebix.alert({ //в случае успешной валидации выводим сообщение о заказеtitle: "Order",text: "We will send you an information to confirm your Order"}).then(function(){goBack(); //очищаем валидацию и возвращаемся к таблице рейсов});}}

Функция запускает валидацию формы при помощи метода validate(). Этот метод анализирует значения полей формы, для которых мы установили правила в свойстве rules. Если значения полей соответствуют правилам, валидация пройдет успешно и метод вернет true. В противном случае метод вернет false, поля с некорректными данными подсветятся красным, а внизу появятся сообщения об ошибках. Напомню, что эти сообщения мы определяем через свойство invalidMessage.

В случае успешной валидации функция выведет сообщение о готовности заказа. Здесь стоит сказать, что Webix имеет несколько методов для вывода сообщений. Мы будем использовать webix.alert(), для которого можно указать действия, которые выполнятся при нажатии на кнопку OK метода (в нашем случаи это функция goBack()):

webix.alert({  title: "Order is successfull",  text: "We will send you an information to confirm your Order"}).then(function(){  goBack();});

Функцию goBack() мы также устанавливаем в качестве обработчика на кнопку Go Back. Она очищает валидацию с помощью метода clearValidation(), изменяет лейбл тулбара, а также возвращает нас в модуль с таблицей рейсов и формой поиска:

function goBack(){const order_form = $$("flight_booking_form"); //получаем доступ к формеconst label = $$("label"); //получаем доступ к лейблу на тулбареorder_form.clearValidation(); //очищаем валидациюlabel.define("label", "Flight Search"); //изменяем значение лейблаlabel.refresh(); //обновляем лейбл$$("flight_search").show(); //отображаем лейаут с таблицей рейсов и формой поиска}

Заключение

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

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

Подробнее..

Перевод Крупные компании, использующие Node.js

13.04.2021 12:15:42 | Автор: admin


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

Она написана на самом популярном в мире языке программирования JavaScript, поэтому открывает новые возможности для многих бизнесов. Неудивительно, что она стала высокоактуальной технологией, выбранной многими компаниями, в том числе такими крупными, как Netflix и PayPal. Какие компании используют технологию Node.js и какие выгоды она им даёт? Об этом мы расскажем в статье.

Действительно ли Node.js меняет рынок?


Согласно данным Stack Overflow, Node.js абсолютный лидер в мире технологий, занимающий 50,4% рынка. Почему же он стал столь популярным?

Согласно последнему отчёту Node.js, эта технология оказывает на бизнес значительное влияние: она обеспечивает рост продуктивности разработчиков на 68%, на 48% повышает производительность приложений, и на 13% качество обслуживания клиентов. Более того, похоже, что эти значения со временем растут:

image

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

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

Десять известных компаний, использующих Node.js для бэкенда


Учитывая длинный список преимуществ применения Node.js, легко поверить, что среди крупнейших компаний, использующих эту технологию, есть такие как НАСА, Uber и Twitter. Кто пользуется Node.js, почему они решили перейти на Node.js и чем это для них обернулось?

Netflix


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

Преимущества, полученные Netflix:

  • После перехода на технологию Node.js время запуска снизилось на 70%. Раньше загрузка интерфейса Netflix занимала до десяти секунд, а теперь всего секунду;
  • Node.js упростил интеграцию микросервисов и разбиение огромного блока информации на подробный интерфейс;
  • Переход от бэкенда к фронтенду был значительно ускорен, поскольку среда Node.js основана на JavaScript.

НАСА


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

Преимущества для НАСА:

  • Время доступа снизилось на 300%, что позволило пользователям получать информацию за секунды, а не за часы;
  • НАСА успешно перенесла старые базы данных в облако и обеспечила доступ к ним через API;
  • Node.js сократил процесс работы с базами данных с 28 шагов всего до семи, что значительно упростило научные исследования.

Trello


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

Основные преимущества для Trello:

  • Node.js предоставил возможность создания чрезвычайно облегчённого одностраничного приложения;
  • Благодаря Node.js Trello может обрабатывать обновления с нулевыми задержками;
  • Архитектура Node.js позволила сократить затраты на разработку и прототипирование.

Переход компании PayPal на Node.js


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

Полученные PayPal результаты:

  • Меньшая по размерам команда разработчиков создала приложение Node.js за более короткое время;
  • Снизилось время отклика, что привело к уменьшению времени загрузки на 35%;
  • После внедрения технологии Node.js количество пользовательских запросов в секунду удвоилось.

LinkedIn


Ещё одна компания, использующая Node.js это LinkedIn, крупнейшая социальная платформа для бизнеса и трудоустройства. Её популярность продолжает расти, а база составляет 467 миллионов пользователей из более чем 200 стран. После перехода с Ruby on Rails на Node.js компания создала приложение, работающее в десять раз быстрее старой версии. Решение было принято из-за синхронизации приложения на Ruby, которая приводила к повышению времени загрузки, особенно при увеличении объёма трафика.

Полученные LinkedIn преимущества:

  • Вся архитектура LinkedIn создавалась на JavaScript, что упростило обработку клиент-серверных взаимодействий;
  • Количество серверов сократили с тридцати до трёх, что удвоило пропускную способность по трафику.

Опыт Uber с Node.js


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

Полученные Uber выгоды:

  • Node.js позволил Uber намного быстрее обрабатывать огромный объём данных и многочисленные запросы пользователей;
  • Благодаря технологии Node.js компания Uber способна ежедневно обслуживать 14 миллионов поездок;
  • Uber повысила связность системы и снизила затраты на управление, создав более 600 конечных точек без сохранения состояния.

Переход на Node.js пример Twitter


Более 80% владельцев аккаунтов Twitter получают к ним доступ через смартфон, поэтому компания приняла решение о создании Twitter Lite приложения с минимальными функциями, способного работать даже при плохом Интернет-соединении. Кроме того, веб-сайтная версия Twitter не была оптимизирована под плохие условия соединения. Всё это привело к тому, что Twitter стала одной из компаний, использующих Node.js.

Преимущества для Twitter:

  • Twitter Lite занимает мало места от 1% до 3% памяти телефона, что удобно для пользователей мобильных устройств;
  • Приложение работает даже с подключениями 3G и 2G;
  • Затраты на поддержку Twitter Lite значительно ниже, чем у Twitter Desktop.

eBay


Ещё одним бизнесом, использующим Node.js, является eBay. Имеющий 183 миллионов пользователей eBay это крупнейшая торговая площадка, предоставляющая услуги онлайн-продаж C2C и B2C. Раньше приложение eBay работало на Java, что приводило к долгой загрузке и низкой производительности. Так как eBay это платформа с огромным объёмом трафика, ей требовалась технология, которая бы ускорила разработку, чтобы она поспевала за изменениями во фронтенде.

Выгода для eBay:

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

Groupon


Groupon крупнейшая площадка, на которой представлены купоны, скидки и выгодные предложения, она имеет базу в 40 миллионов пользователей. Когда в 2019 году Groupon достиг отметки в 200 миллионов скачиваний, то столкнулся с проблемами масштабируемости. Именно тогда компания обратила внимание на Node.js и провела крупнейшее в мире развёртывание продукта на Node.js.

Преимущества, полученные Groupon:

  • Развёртывание Node.js обеспечило высокую масштабируемость, что позволило реализовать бесперебойную работу 3400 бэкенд-сервисов;
  • Скорость загрузки удвоилась;
  • Node.js упростил и ускорил миграцию на другую платформу.

Medium


Medium это известная платформа для онлайн-публикаций с 85 миллионами пользователей, использующая Node.js. Достигнув в 2016 году планки в 7,5 миллионов постов, команда Medium ощутила потребность в управлении big data без превышения нагрузки на сервера. Кроме того, компании нужно было соответствовать постоянно растущим требованиям текстовых редакторов к публикации постов.

Преимущества для Medium:

  • Даже страницы с большими изображениями и объёмным контентом грузятся за 2,7 секунды.
  • Node.js повысил качество обслуживания пользователей и ускорил время развёртывания.

TechMagic


TechMagic это компания, специализирующаяся в разработке ПО. Мы создаём приложения с нуля и внедряем своих специалистов в команды разработки стартапов, используем различные технологии фулл-стека, в том числе и Node.js.

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

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

Заключение


Если вы всё ещё не уверены в том, что Node.js это технология будущего, перечислю других крупных игроков, использующих её в своей работе: Google, Yahoo, Mozilla, Microsoft и многие другие. Благодаря её неограниченным возможностям всё больше компаний внедряет технологию Node.js. Однажды эта набирающая обороты технология завоюет рынок и станет лучшим фреймворком для каждой компании, от стартапов до крупных компаний. Если у вас есть задумка какого-то продукта, то подумайте над использованием Node.js в качестве его бэкенда.



На правах рекламы


Мощные виртуальные серверы с процессорами AMD EPYC для разработчиков. Частота ядра CPU до 3.4 GHz. Максимальная конфигурация позволит оторваться на полную 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.

Подробнее..

Идеальный инструмент для создания прогрессивных веб-приложений или Все, что вы хотели знать о 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. Разумеется, содержимое обоих файлов можно кастомизировать.

Подробнее..

Путь IVI от монолита к микросервисам

08.04.2021 16:21:10 | Автор: admin

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

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

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

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

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

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

Поэтому следующие наши микросервисы создавались уже в отдельных репозиториях. И тут мы начали осознавать другое преимущество микросервисов: на них можно пробовать новые технологии, новые языки программирования, новые СУБД. Мы уже не были привязаны к монолиту. Так, например, в нашу разработку ворвался язык Go.

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

Другой проблемой микросервисов стали распределённые транзакции. Так, например, на IVI есть процедура объединения аккаунтов. Когда пользователь открывает только что установленное приложение, у него уже есть аккаунт, привязанный к устройству. И он может смотреть фильмы, добавлять их в очередь просмотра, оценивать. Но через некоторое время пользователь вспоминает, что у него есть зарегистрированный на сайте аккаунт, и он авторизуется под ним на устройстве. Наша задача объединить пользовательские оценки, очереди со старого и нового аккаунта. Объединение этой информации происходит во множестве микросервисов, и, в теории, должно происходить или везде, или нигде. То есть нужны распределённые транзакции, которые привносят очень большие сложности в бизнес-логику и в разрабатываемую систему. Решать эту проблему мы в итоге не стали: максимально обезопасили критическую для пользователя информацию (его покупки), а если при работе с другими данными возникнут ошибки, то их обнаружат или скрипты валидации данных, или сам пользователь придёт с жалобой в поддержку и ему там помогут.

И ещё одной проблемой стала сложность добавления нового сервиса в production. Средства CD позволяли быстро обновлять существующие сервисы, но это не касалось первоначальной установки. Она подразумевала необходимость установить новые машины в ДЦ или создать виртуалки, установить всё необходимое ПО, настроить CD. И это был небыстрый процесс, а новые доработки обычно требовались довольно срочно. Да, в будущем будут появляться различные средства для решения этой проблемы, но в то время их ещё не было. Хотя даже когда у нас начали появляться средства виртуализации, контейнеризации, оркестрации и эта проблема стала исчезать, на смену пришла другая сложность эксплуатации. Уже нельзя справиться с десятками сервисов без автоматизации доставки новых версий приложений. Сложность эксплуатации также возрастает в связи с повышением требований к мониторингу и управлению большим количеством сервисов. Решение таких эксплуатационных проблем требует множества навыков и инструментов.

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

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

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

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

При этом и наша компания разрасталась, менялась её структура, появлялись команды, развивавшие конкретные направления. И тут мы осознали ещё один недостаток монолитов и преимущество микросервисов разграничение ответственности. Для каждого микросервиса у нас появились свои ответственные команды. Чего нельзя сказать о разросшихся в размерах сервисах: за них отвечали все и одновременно никто. Стали происходить диалоги вида: Кто отвечает за сервис X? Команда A. У меня вопрос по функциональности F. А, за этим тебе надо идти в команду B..

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

Другой проблемой стала сложность поддержки актуального стека технологий. Уже давно закончилась поддержка второго Python, но один из наших сервисов до сих пор на нём работает. А другой сервис мы полгода переводили на третий Python. Да и в целом любой рефакторинг сервисов с большой кодовой базой это очень сложная задача, которая требует больших усилий для организации плавного перехода на новые технологии, или может потребовать feature freeze на длительный период. К тому же после написания всего необходимого кода очень долгое время занимает тестирование таких изменений. Тогда как микросервисы переводились на Python 3 за пару дней и очень быстро тестировались.

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

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

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

Подробнее..

Как заблокировать приложение с помощью runBlocking

10.02.2021 14:12:50 | Автор: admin

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

Напишите где-нибудь в UI потоке (например в методе onStart) такой код:

//где-то в UI потокеrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

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


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

//где-то в UI потокеHandler().post {println("Hello, World!") // отработает в UI потоке}

Или даже так:

//где-то в UI потокеrunOnUiThread {  println("Hello, World!") // и это тоже отработает в UI потоке}

Вроде конструкция очень похожа на наш проблемный код, но здесь обе части кода работают (по-разному под капотом, но работают). Чем они отличаются от кода с runBlocking?

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

Для начала небольшой дисклеймер. runBlocking редко используется в продакшн коде Android-приложения. Обычно он предназначен для использования в синхронном коде, вроде функций main или unit-тестах.

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

  • Это наглядно. Ниже мы придем к тому, что это актуально и не только для UI-потока Android-приложения. Но для наглядности лучше всего подходит пример на UI-потоке.

  • Интересно разобраться, почему всё именно так работает.

  • Всё-таки иногда мы можем использовать runBlocking, пусть даже в тестовых приложениях.

Билдер runBlocking работает почти так же, как и launch: создает корутину и вызывает в ней блок кода. Но чтобы сделать вызов блокирующим runBlocking создает особую корутину под названием BlockingCoroutine, у которой есть дополнительная функция joinBlocking(). runBlocking вызывает joinBlocking() сразу же после запуска корутины.

Фрагмент из runBlocking():

// runBlocking() function// val coroutine = BlockingCoroutine<T>(newContext, )coroutine.start(CoroutineStart.DEFAULT, coroutine, block)return coroutine.joinBlocking()

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

Кроме того, BlockingCoroutine переопределяет функцию afterCompletion(), которая вызывается после завершения работы корутины.

override fun afterCompletion(state: Any?) {//wake up blocked threadif (Thread.currentThread ()! = blockedThread)LockSupport.unpark (blockedThread)}

Эта функция просто разблокирует поток, если она была заблокирована до этого с помощью park().

Как это всё работает примерно показано на схеме работы runBlocking.

Что здесь делает Dispatchers

Хорошо, мы поняли, что делает билдер runBlocking. Но почему в одном случае он блокирует UI-поток, а в другом нет? Почему Dispatchers.Main приводит к дедлоку...

// Этот код создает дедлокrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

...,а Dispatchers.Default нет?

// А этот код создает дедлокrunBlocking(Dispatchers.Default) {  println(Hello, World!)}

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

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

public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher

Dispatchers.Default реализует класс DefaultScheduler и делегирует обработку исполняемого блока кода объекту coroutineScheduler. Его функция dispatch() выглядит так:

override fun dispatch (context: CoroutineContext, block: Runnable) =  try {    coroutineScheduler.dispatch (block)  } catch (e: RejectedExecutionException) {    //    DefaultExecutor.dispatch(context, block)  }

Класс CoroutineScheduler отвечает за наиболее эффективное распределение обработанных корутин по потокам. Он реализует интерфейс Executor.

override fun execute(command: Runnable) = dispatch(command)

А что же делает функция CoroutineScheduler.dispatch()?

  • Добавляет исполняемый блок в очередь задач. При этом существует две очереди: локальная и глобальная. Это часть механизма приоритезации внешних задач.

  • Создает воркеры. Воркер это класс, унаследованный от обычного Java Thread (в данном случае daemon thread). Здесь создаются рабочие потоки. У воркера также есть локальная и глобальная очереди, из которых он выбирает задачи и выполняет их.

  • Запускает воркеры.

Теперь соединим всё, что разобрали выше про Dispatchers.Default, и напишем, что происходит в целом.

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() запускает воркеры (под капотом Java потоки).

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

  • Исполняемый блок кода выполняется.

  • Вызывается функция afterCompletion(), которая разблокирует текущий поток с помощью LockSupport.unpark().

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

Перейдём к Dispatchers.Main

Это диспатчер, который создан специально для Android. Например, при использовании Dispatchers.Main фреймворк бросит исключение, если вы не добавляете зависимость:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:..*'

Перед началом разбора Dispatchers.Main стоит поговорить о HandlerContext. Это специальный класс, который добавлен в пакет coroutines для Android. Это диспатчер, который выполняет задачи с помощью Android Handler всё просто.

Dispatchers.Main создаёт HandlerContext с помощью AndroidDispatcherFactory через функцию createDispatcher().

override fun createDispatcher() =  HandlerContext(Looper.getMainLooper().asHandler(async = true))

И что мы тут видим? Looper.getMainLooper().asHandler() означает, что он принимает Handler главного потока Android. Получается, что Dispatchers.Main это просто HandlerContext с Handlerом главного потока Android.

Теперь посмотрим на функцию dispatch() у HandlerContext:

override fun dispatch(context: CoroutineContext, block: Runnable) {  handler.post(block)}

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

Итого, что же происходит?

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() отправляет исполняемый блок кода через Handler главного потока.

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

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

  • Из-за этого afterCompletion() никогда не вызывается.

  • И из-за этого текущий поток не будет разблокирован (через unparked) в функции afterCompletion().

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

Вот почему runBlocking с Dispatchers.Main блокирует UI-поток навсегда.

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

Совсем простое объяснение

Помните пример с Handler().post в самом начале статьи? Там код работает и ничего не блокируется. Однако мы можем легко изменить его, чтобы он был в значительной степени похож на наш код с Dispatcher.Main, и стал ещё нагляднее. Для этого можем добавить операции parking и unparking к текущему потоку, иммитируя работу функций afterCompletion() и joinBlocking(). Код начинает работать почти так же, как с билдером runBlocking.

//где-то в UI потокеval thread = Thread.currentThread()Handler().post {  println("Hello, World!") // это никогда не будет вызвано  // имитируем afterCompletion()  LockSupport.unpark(thread)}// имитируем joinBlocking()LockSupport.park()

Но этот трюк не будет работать с функцией runOnUiThread.

//где-то в UI потокеval thread = Thread.currentThread()runOnUiThread {  println("Hello, World!") // этот код вызовется  LockSupport.unpark(thread)}LockSupport.park()

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

Если всё же очень хочется использовать runBlocking в UI-потоке, то у Dispatchers.Main есть оптимизация Dispatchers.Main.immediate. Там аналогичная логика как у runOnUiThread. Поэтому этот блок кода будет работать и в UI-потоке:

//где-то в UI потокеrunBlocking(Dispatchers.Main.immediate) {   println(Hello, World!)}

Выводы

В статье я описал как безобидный билдер runBlocking может заморозить ваше приложение на Android. Это произойдет, если вызвать runBlocking в UI-потоке с диспатчером Dispatchers.Main. Приложение заблокируется по следующему алгоритму:

  • runBlocking создаёт блокирующую корутину BlockingCoroutine.

  • Dispatchers.Main отправляет на запуск исполняемый блок кода через Handler.post.

  • Но BlockingCoroutine тут же заблокирует UI поток.

  • Поэтому Main Looper никогда не получит сообщение с исполняемым блоком кода.

  • А UI не разблокируется, потому что корутина ждёт завершения исполняемого кода.

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

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

val singleThreadDispatcher = newSingleThreadContext("Single Thread")GlobalScope.launch (singleThreadDispatcher) {  runBlocking (singleThreadDispatcher) {    println("Hello, World!") // этот кусок кода опять не выполнится  }}

Если очень надо написать runBlocking в главном потоке Android-приложения, то не используйте Dispatchers.Main. Используйте Dispatchers.Default или Dispatchers.Main.immediate в крайнем случае.


Также будет интересно почитать:

Оригинал статьи на английском How runBlocking May Surprise You.
Как страдали iOS-ники когда выпиливали Realm.
О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
Кратко об истории Open Source просто развлечься (да и статья хорошая).

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

Подробнее..

Запускаем Rust-приложение на мобильной ОС Аврора

02.03.2021 10:09:04 | Автор: admin

Всем привет! Меня зовут Шамиль, я ведущий инженер-разработчик в КРОК. Помимо всего прочего мы в компании занимаемся ещё и разработкой мобильных приложений для операционной системы Аврора, есть даже центр компетенций по ней.

Для промышленной разработки мы, конечно же, пока используем связку C++ и QML, но однажды подсев на "ржавую" иглу Rust, я не мог не попробовать применить свой любимый язык программирования для написания мобильных приложений. В этой статье я опишу эксперимент по написанию простейшего приложения на Rust, предназначенного для запуска на мобильном устройстве под управлением вышеупомянутой ОС. Сразу оговорюсь, что легких путей я не искал эксперименты проводил на сертифицированной версии Авроры, которая добавила огонька в этот процесс. Но, как говорится, только защищённая ОС, только хардкор.

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

Готовим окружение

Итак, работа будет вестись из-под Ubuntu Linux с уже установленным Rust. В качестве подопытного планшета выступает Aquarius NS220 с сертифицированной ОС Аврора последней (на момент написания статьи) версии 3.2.2 с включённым режимом разработчика, который обеспечивает связь по SSH, а также привилегированный доступ с правами суперпользователя.

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

sudo apt install -y g++-arm-linux-gnueabihfrustup target add armv7-unknown-linux-gnueabihf

В сертифицированной версии ОС Аврора не разрешается запускать неподписанные приложения. Подписывать надо проприетарной утилитой из состава Aurora Certified SDK под названием ompcert-cli, которая поддерживает на входе только пакет в формате RPM. Поэтому сразу установим замечательную утилиту cargo-rpm, которая возьмёт на себя всю рутинную работу по упаковке приложения в RPM-пакет:

cargo install cargo-rpm

Саму процедуру подписывания RPM-пакета я описывать не буду, она неплохо документирована в справочных материалах ОС Аврора.

Aurora SDK можно скачать с сайта производителя.

Часть 1. Hello. World

TL;DR Исходники проекта можно найти в репозитории на Гитхабе.

Создаем минимальный проект

Создаём пустое приложение на Rust:

cargo new aurora-rust-helloworld

Пытаемся сгенерировать .spec файл для RPM-пакета:

cargo rpm init

Получаем ошибки, что не хватает некоторых полей в Cargo.toml, добавляем их:

Cargo.toml:

[package]name = "aurora-rust-helloworld"version = "0.1.0"authors = ["Shamil Yakupov <syakupov@croc.ru>"]edition = "2018"description = "Rust example for Aurora OS"license = "MIT"

Закидываем в папку .cargo конфигурационный файл с указанием правильного линкера для компоновки исполняемого файла под архитектуру ARM:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]linker = "arm-linux-gnueabihf-gcc"

Собираем RPM-пакет:

cargo rpm initcargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Всё собралось, забираем RPM из папки target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl, подписываем его, копируем на планшет и пытаемся установить:

$ devel-suPassword:# pkcon install-local ./aurora-rust-helloworld-0.1.0-1.armv7hl.rpm

Получаем ошибку:

Fatal error: nothing provides libc.so.6(GLIBC_2.32) needed by aurora-rust-helloworld-0.1.0-1.armv7hl

Смотрим версию glibc на устройстве, и понимаем, что она явно ниже той, что нам требуется:

$ ldd --versionldd (GNU libc) 2.28

Что ж, тогда попробуем забрать нужные библиотеки с планшета, закинуть их в директорию lib и слинковать с ними. Для верности будем пользоваться линкером, входящим в состав Aurora SDK, который закинем в директорию bin. Для начала посмотрим, какие именно библиотеки нам нужны. Меняем содержимое .cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Пробуем собрать:

cargo build --release --target=armv7-unknown-linux-gnueabihf

Получаем ошибки:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lgcc_saurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutilaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lrtaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lpthreadaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lmaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -ldlaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lcaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutil

Копируем недостающие библиотеки с планшета:

mkdir -p libscp nemo@192.168.2.15:/usr/lib/libgcc_s.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./libscp nemo@192.168.2.15:/usr/lib/librt.so ./libscp nemo@192.168.2.15:/usr/lib/libpthread.so ./libscp nemo@192.168.2.15:/usr/lib/libm.so ./libscp nemo@192.168.2.15:/usr/lib/libdl.so ./libscp nemo@192.168.2.15:/usr/lib/libc.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./lib

Снова пытаемся собрать, получаем новую порцию ошибок:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /lib/libc.so.6 when searching for /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /usr/lib/libc_nonshared.a when searching for /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/ld-linux-armhf.so.3

Копируем недостающее:

scp nemo@192.168.2.15:/lib/libc.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libc_nonshared.a ./libscp nemo@192.168.2.15:/lib/ld-linux-armhf.so.3 ./lib

Ещё надо подредактировать файл libc.so (который является фактически скриптом линкера), чтобы дать понять линкеру, где надо искать библиотеки:

lib/libc.so:

/* GNU ld script   Use the shared library, but some functions are only in   the static library, so try that secondarily.  */OUTPUT_FORMAT(elf32-littlearm)GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-armhf.so.3 ) )

Запускаем сборку RPM-пакета, копируем, пытаемся установить.

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

Итак, мы видим, что валидатор выдал несколько ошибок:

вот таких
Desktop file============ERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File is missing - cannot validate .desktop filePaths=====WARNING [/usr/share/aurora-rust-helloworld] Directory not foundERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File not foundWARNING [/usr/share/icons/hicolor/86x86/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/108x108/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/128x128/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/172x172/apps/aurora-rust-helloworld.png] File not foundERROR [/usr/share/icons/hicolor/[0-9x]{5,9}/apps/aurora-rust-helloworld.png] No icons found! RPM must contain at least one icon, see: https://community.omprussia.ru/doc/software_development/guidelines/rpm_requirementsLibraries=========ERROR [/usr/bin/aurora-rust-helloworld] Cannot link to shared library: libutil.so.1Symbols=======ERROR [/usr/bin/aurora-rust-helloworld] Binary does not link to 9__libc_start_main@GLIBC_2.4.Requires========ERROR [libutil.so.1] Cannot require shared library: 'libutil.so.1'

Что ж, будем бороться с каждой ошибкой по списку.

Добавляем недостающие файлы

Добавим иконки и ярлык (файл с расширением desktop) в директорию .rpm.

.rpm/aurora-rust-helloworld.desktop:

[Desktop Entry]Type=ApplicationX-Nemo-Application-Type=silica-qt5Icon=aurora-rust-helloworldExec=aurora-rust-helloworldName=Rust Hello-World

Для того, чтобы копировать нужные файлы на этапе сборки RPM-пакета, сделаем простенький Makefile:

Makefile
.PHONY: all clean install prepare release rpmall:@cargo build --target=armv7-unknown-linux-gnueabihfclean:@rm -rvf targetinstall:@scp ./target/armv7-unknown-linux-gnueabihf/release/aurora-rust-helloworld nemo@192.168.2.15:/home/nemo/@scp ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl/*.rpm nemo@192.168.2.15:/home/nemo/prepare:@rustup target add armv7-unknown-linux-gnueabihf@cargo install cargo-rpmrelease:@cargo build --release --target=armv7-unknown-linux-gnueabihfrpm:@mkdir -p ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -vf .rpm/aurora-rust-helloworld.desktop ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -rvf .rpm/icons ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Обновим aurora-rust-helloworld.spec:

.rpm/aurora-rust-helloworld.spec
%define __spec_install_post %{nil}%define __os_install_post %{_dbpath}/brp-compress%define debug_package %{nil}Name: aurora-rust-helloworldSummary: Rust example for Aurora OSVersion: @@VERSION@@Release: @@RELEASE@@%{?dist}License: MITGroup: Applications/SystemSource0: %{name}-%{version}.tar.gzSource1: %{name}.desktopSource2: iconsBuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root%description%{summary}%prep%setup -q%installrm -rf %{buildroot}mkdir -p %{buildroot}cp -a * %{buildroot}mkdir -p %{buildroot}%{_datadir}/applicationscp -a %{SOURCE1} %{buildroot}%{_datadir}/applicationsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/86x86/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/108x108/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/128x128/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/172x172/appscp -a %{SOURCE2}/86x86/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/86x86/appscp -a %{SOURCE2}/108x108/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/108x108/appscp -a %{SOURCE2}/128x128/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/128x128/appscp -a %{SOURCE2}/172x172/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/172x172/apps%cleanrm -rf %{buildroot}%files%defattr(-,root,root,-)%{_bindir}/*%{_datadir}/applications/%{name}.desktop%{_datadir}/icons/hicolor/*/apps/%{name}.png

Для сборки пакета теперь достаточно выполнить:

make rpm

Убираем зависимость от libutil.so

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

lib/libutil.so:

/* GNU ld script   Dummy script to avoid dependency on libutil.so */ASSERT(1, "Unreachable")

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

Добавляем символ __libc_start_main

Перепробовав несколько способов, остановился на том, чтобы добавить при линковке стандартный объектный файл crt1.o. Копируем его с планшета:

scp nemo@192.168.2.15:/usr/lib/crt1.o ./lib

И добавляем в команды линкера:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Однако при попытке сборки получаем ошибки:

undefined reference to `__libc_csu_fini'undefined reference to `__libc_csu_init'

Добавим заглушки этих функций в main.rs:

src/main.rs:

#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    println!("Hello, world!");}

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

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

$ aurora-rust-helloworld-bash: /usr/bin/aurora-rust-helloworld: /usr/lib/ld.so.1: bad ELF interpreter: No such file or directory

Смотрим зависимости:

$ ldd /usr/bin/aurora-rust-helloworldlinux-vdso.so.1 (0xbeff4000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xa707f000)librt.so.1 => /lib/librt.so.1 (0xa7069000)libpthread.so.0 => /lib/libpthread.so.0 (0xa7042000)libm.so.6 => /lib/libm.so.6 (0xa6fc6000)libdl.so.2 => /lib/libdl.so.2 (0xa6fb3000)libc.so.6 => /lib/libc.so.6 (0xa6e95000)/usr/lib/ld.so.1 => /lib/ld-linux-armhf.so.3 (0xa70e7000)

И видим динамическую линковку с библиотекой ld-linux-armhf.so.3. Если решать в лоб, то нужно создать символическую ссылку /usr/lib/ld.so.1 /lib/ld-linux-armhf.so.3 (и это даже будет неплохо работать). Но, к сожалению, такое решение не подходит. Дело в том, что строгий RPM-валидатор не пропустит ни пред(пост)-установочные скрипты в .spec-файле, ни деплой в директорию /usr/lib. Вообще список того, что можно, приведён здесь.

Долгое и разнообразное гугление подсказало, что у линкера GCC есть нужный нам ключ (dynamic-linker), который позволяет сослаться непосредственно на нужную зависимость. Правим config.toml:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o --dynamic-linker /lib/ld-linux-armhf.so.3"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Собираем RPM-пакет, подписываем, копируем на планшет, устанавливаем и с замиранием сердца запускаем:

$ aurora-rust-helloworldHello, world!

Часть 2. Запускаем приложение с GUI

TL;DR Исходники проекта можно найти в репозитории.

В Авроре всё очень сильно завязано на Qt/QML, поэтому сначала я думал использовать крейт qmetaobject. Однако в комплекте с ОС идёт библиотека Qt версии 5.6.3, а qmetaobject, судя по описанию, требует минимум Qt 5.8. И действительно, попытка сборки крейта приводит к ошибкам.

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

Для начала копируем проект, созданный в предыдущей части, и переименовываем его в aurora-rust-gui.

Приступаем

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

вот таких
scp nemo@192.168.2.15:/usr/lib/libstdc++.so ./libscp nemo@192.168.2.15:/usr/lib/libQt5Core.so.5 ./lib/libQt5Core.soscp nemo@192.168.2.15:/usr/lib/libQt5Gui.so.5 ./lib/libQt5Gui.soscp nemo@192.168.2.15:/usr/lib/libQt5Qml.so.5 ./lib/libQt5Qml.soscp nemo@192.168.2.15:/usr/lib/libQt5Quick.so.5 ./lib/libQt5Quick.soscp nemo@192.168.2.15:/usr/lib/libGLESv2.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libpng16.so.16 ./libscp nemo@192.168.2.15:/usr/lib/libz.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicui18n.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libicuuc.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre16.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libglib-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libsystemd.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5Network.so.5 ./libscp nemo@192.168.2.15:/lib/libresolv.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libhybris-common.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicudata.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libselinux.so.1 ./libscp nemo@192.168.2.15:/usr/lib/liblzma.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libgcrypt.so.11 ./libscp nemo@192.168.2.15:/usr/lib/libgpg-error.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libcap.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libsailfishapp.so.1 ./lib/libsailfishapp.soscp nemo@192.168.2.15:/usr/lib/libmdeclarativecache5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libmlite5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libgobject-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5DBus.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libffi.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libdbus-1.so.3 ./libscp nemo@192.168.2.15:/usr/lib/libgio-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libgmodule-2.0.so.0 ./lib

А еще копируем заголовочные файлы, которые идут в составе Aurora SDK:

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/qt5 include/qt5

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/sailfishapp include/sailfishapp

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/GLES3 include/GLES3

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/KHR include/KHR

Для сборки проекта напишем скрипт build.rs и укажем его в Cargo.toml.

build.rs:

fn main() {    let include_path = "include";    let qt_include_path = "include/qt5";    let sailfish_include_path = "include/sailfishapp";    let library_path = "lib";    let mut config = cpp_build::Config::new();    config        .include(include_path)        .include(qt_include_path)        .include(sailfish_include_path)        .opt_level(2)        .flag("-std=gnu++1y")        .flag("-mfloat-abi=hard")        .flag("-mfpu=neon")        .flag("-mthumb")        .build("src/main.rs");    println!("cargo:rustc-link-search={}", library_path);    println!("cargo:rustc-link-lib=sailfishapp");    println!("cargo:rustc-link-lib=Qt5Gui");    println!("cargo:rustc-link-lib=Qt5Core");    println!("cargo:rustc-link-lib=Qt5Quick");    println!("cargo:rustc-link-lib=Qt5Qml");}

Cargo.toml:

[package]# ...build = "build.rs"[dependencies]cpp = "0.5.6"[build-dependencies]cpp_build = "0.5.6"#...

Теперь возьмёмся за само приложение. За создание инстанса приложения у нас будет отвечать структура SailfishApp по аналогии с приложением для Авроры, написанном на C++.

src/main.rs:

#[macro_use]extern crate cpp;mod qbytearray;mod qstring;mod qurl;mod sailfishapp;use sailfishapp::SailfishApp;#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    let mut app = SailfishApp::new();    app.set_source("main.qml".into());    app.show();    app.exec();}

SailfishApp это по сути обвязка (биндинги) к соответствующему классу на C++. Берём за образец структуру QmlEngine из крейта qmetaobject.

src/sailfishapp.rs
use crate::qstring::QString;cpp! {{    #include <sailfishapp.h>    #include <QtCore/QDebug>    #include <QtGui/QGuiApplication>    #include <QtQuick/QQuickView>    #include <QtQml/QQmlEngine>    #include <memory>    struct SailfishAppHolder {        std::unique_ptr<QGuiApplication> app;        std::unique_ptr<QQuickView> view;        SailfishAppHolder() {            qDebug() << "SailfishAppHolder::SailfishAppHolder()";            int argc = 1;            char *argv[] = { "aurora-rust-gui" };            app.reset(SailfishApp::application(argc, argv));            view.reset(SailfishApp::createView());            view->engine()->addImportPath("/usr/share/aurora-rust-gui/qml");        }    };}}cpp_class!(    pub unsafe struct SailfishApp as "SailfishAppHolder");impl SailfishApp {    /// Creates a new SailfishApp.    pub fn new() -> Self {        cpp!(unsafe [] -> SailfishApp as "SailfishAppHolder" {            qDebug() << "SailfishApp::new()";            return SailfishAppHolder();        })    }    /// Sets the main QML (see QQuickView::setSource for details).    pub fn set_source(&mut self, url: QString) {        cpp!(unsafe [self as "SailfishAppHolder *", url as "QString"] {            const auto full_url = QString("/usr/share/aurora-rust-gui/qml/%1").arg(url);            qDebug() << "SailfishApp::set_source()" << full_url;            self->view->setSource(full_url);        });    }    /// Shows the main view.    pub fn show(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::show()";            self->view->showFullScreen();        })    }    /// Launches the application.    pub fn exec(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::exec()";            self->app->exec();        })    }}

Биндинги для используемых классов QByteArray, QString, QUrl копируем из того же qmetaobject и расфасовываемым по отдельным файлам. Здесь приводить их не буду, если что, исходники можно посмотреть в репозитории на GitHub.

Немного скорректируем заголовочный файл sailfishapp.h, чтобы он искал заголовочные файлы Qt в правильных местах:

include/sailfishapp/sailfishapp.h:

// ...#ifdef QT_QML_DEBUG#include <QtQuick>#endif#include <QtCore/QtGlobal>  // Было `#include <QtGlobal>`#include <QtCore/QUrl>      // Было `#include <QUrl>`class QGuiApplication;class QQuickView;class QString;// ...

Осталось только добавить файлы QML и положить их в дистрибутив RPM.

все здесь

qml/main.qml:

import QtQuick 2.6import Sailfish.Silica 1.0ApplicationWindow {    cover: Qt.resolvedUrl("cover.qml")    initialPage: Page {        allowedOrientations: Orientation.LandscapeMask        Label {            anchors.centerIn: parent            text: "Hello, Aurora!"        }    }}

qml/cover.qml:

import QtQuick 2.6import Sailfish.Silica 1.0CoverBackground {    Rectangle {        id: background        anchors.fill: parent        color: "blue"        Label {            id: label            anchors.centerIn: parent            text: "Rust GUI"            color: "white"        }    }    CoverActionList {        id: coverAction        CoverAction {            iconSource: "image://theme/icon-cover-cancel"            onTriggered: Qt.quit()        }    }}

.rpm/aurora-rust-gui.spec:

# ...Source3: qml# ...%install# ...mkdir -p %{buildroot}%{_datadir}/%{name}cp -ra %{SOURCE3} %{buildroot}%{_datadir}/%{name}/qml%cleanrm -rf %{buildroot}%files# ...%{_datadir}/%{name}/qml

Makefile:

# ...rpm:# ...@cp -rvf qml ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES# ...

Собираем:

make cleanmake releasemake rpm

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

$ devel-suPassword:# pkcon install-local ./aurora-rust-gui-0.1.0-1.armv7hl.rpmInstalling filesTesting changesFinishedInstalling filesStartingResolving dependenciesInstalling packagesDownloading packagesInstalling packagesFinishedDownloaded  aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)         Rust GUI example for Aurora OSInstalled   aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)            Rust GUI example for Aurora OS# exit$ aurora-rust-gui[D] __cpp_closure_14219197022164792912_impl:33 - SailfishApp::new()[D] SailfishAppHolder::SailfishAppHolder:15 - SailfishAppHolder::SailfishAppHolder()[D] unknown:0 - Using Wayland-EGLlibrary "libpq_cust_base.so" not found[D] __cpp_closure_16802020016530731597:42 - SailfishApp::set_source() "/usr/share/aurora-rust-gui/qml/main.qml"[W] unknown:0 - Could not find any zN.M subdirs![W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/z1.0/" does not exist[W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/" does not exist[D] onCompleted:432 - Warning: specifying an object instance for initialPage is sub-optimal - prefer to use a Component[D] __cpp_closure_12585295123509486988:50 - SailfishApp::show()[D] __cpp_closure_15029454612933909268:59 - SailfishApp::exec()

Вот так выглядит наше приложение с разных ракурсов:

 Рабочий стол с ярлыком Рабочий стол с ярлыком Главное окно приложенияГлавное окно приложения Панель задач Панель задач

Последние штрихи

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

Серия проб и ошибок показала, что надо добавить ещё один ключ линкера: -export-dynamic.

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o -rpath lib --dynamic-linker /lib/ld-linux-armhf.so.3 -export-dynamic"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

После этого всё работает так, как и ожидается.

Заключение

Понятно, что до того, как использовать Rust в проде, ещё надо решить немало вопросов. Как минимум, я предвижу сложности с дополнительными зависимостями при подключении новых крейтов, извечные танцы с бубном вокруг сегфолтов при FFI-вызовах, увязывание систем владения Qt и Rust. Некоторые интересные подробности можно почерпнуть из статьи от автора qmetaobject-rs. Наверняка, время от времени будут всплывать и другие проблемы.

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

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

Буду рад вопросам и замечаниям в комментариях. И ставьте лайк, подписывайтесь на канал :-)

Подробнее..

Как написать простое Android ToDo-приложение на Java

15.03.2021 22:22:10 | Автор: admin

Предисловие

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

Я расскажу вам как написать простенькое ToDo-приложение на Android с тремя активностями (рабочими экранами).

Ссылка на проект на Github будет в конце данной статьи.

Установка и первичная настройка

Для разработки приложения я рассмотрю использование бесплатной IDEIntellij от разработчиков JetBrains - Android Studio, у меня версия 4.1.1.

После успешной установки IDE и запуска нажимаем на самую первую кнопкуStart a new Android Studio Project. Далее появится мастер первичной подготовки проекта:

  • выберем подходящий шаблон, в моем случае это Empty Activity - он самый простой для новичков, так как при первом запуске будет всего 1 XML файл с версткой и один java файл MainActivity.

  • На следующем экране придумываем имя приложению; помните, что package name, после публикации на Google Play изменить нельзя (иначе Google Play посчитает это другим приложением (поправьте меня, если я ошибаюсь). Выбираем язык Java, так как по нему данная статья, а также, по нему больше информации в Интернете, чем по Kotlin.

  • Минимальный SDK выбираем под Android 5.0, так как данного API будет предостаточно для наших задач, заодно мы получим большой охват, в том числе старых устройств: планшеты, смартфоны, встроенные системы.

Скриншоты: установка и первичная настройка

Далее раскрываем вкладку Project и находим в каталоге Java><Ваш_Проект> файл MainActivity.java, в котором мы будем описывать все происходящее на главном экране.

Подготовка макетов (layouts) - внешний облик приложения

После рассмотрим файл MainActivity.xml, для этого нам нужно найти каталог res>layout>. Откроем MainActivity.xml для создания облика первой - главной страницы и перетягивая спанели Palette необходимые нам типы объектов.

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

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

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

Создание String-переменнойСоздание String-переменной

Для перевода интерфейса, необходимо сохранить изменения и над нашим конструктором Layout нажать на кнопку Default (en-us) и выбрать Edit Translations, далее найти слева сверху значок глобуса и нажать на него для добавления нового языка:

Переводы для интерфейсовПереводы для интерфейсов

Таким образом создадим дополнительные макеты (layouts) для оставшихся двух окон:

Скриншоты: еще два макета
Макет Activity_Settings.xmlМакет Activity_Settings.xmlМакет Activity_Advanced.xmlМакет Activity_Advanced.xml

Программируем на Java под Android

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

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

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

  • На переключателях должен отображаться тот текст, который пользователь настраивает из окна с макетом Activity_Settings.xml

  • Количество переключателей должно соответствовать заданному числу из окна макета Activity_Advanced.xml

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

  • Сброс переключателей возможен только, если переключатель Уверен/-а? включен.

  • А также, должны работать оставшиеся кнопки меню.


    Пишем следующее:

Код под спойлером: 156 строчек
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import androidx.appcompat.widget.SwitchCompat;import android.content.Intent;import android.content.SharedPreferences;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.TextView;public class MainActivity extends AppCompatActivity {    //Создаем 9 переключателей с помощью массива    SwitchCompat[] switcharray = new SwitchCompat[9];    Boolean Reset; //Булев для выключения переключателя    Button NextButton;    public int[] list_of_switches = {            R.id.switch_compat1,            R.id.switch_compat2,            R.id.switch_compat3,            R.id.switch_compat4,            R.id.switch_compat5,            R.id.switch_compat6,            R.id.switch_compat7,              R.id.switch_compat8,             R.id.switch_compat10, //переключатель "Вы уверены?" //8    };    //Нажатие кнопки Сброс    public void ResetButtonClick (View view) throws IllegalAccessException {        Reset =false;        if (switcharray[8].isChecked()) { //Если переключатель "Вы уверены?" нажат, то разрешаем переключить в false остальные переключатели            SharedPreferences.Editor editor = getSharedPreferences("save"                    ,MODE_PRIVATE).edit();            //Сохраняем в Intent значения всех переключателей в False            for (int k=0; k<10; k++) {                editor.putBoolean("value"+k, false);            }            editor.apply();            //Устанавливаем все переключатели в значение False            for (int i=0;i<9;i++){                switcharray[i].setChecked(false);            }            //Reset background color of checked SwitchCompats            for (int i = 0; i < 9; i++) {                findViewById(list_of_switches[i]).setBackgroundColor(Color.TRANSPARENT);            }        }    }    //Создание формы / открытие приложения    @Override    protected void onCreate(final Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        //Назначаем полям значения по умолчанию и сохраняем их в Intent        String[] tsfield = new String[8];        SharedPreferences prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE);        tsfield[0] = prefs.getString("KEY_F0", "Выключил газ");        tsfield[1] = prefs.getString("KEY_F1", "Выключил воду");        tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек");        tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна");        tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет");        tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь");        tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет");        tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор");        //Получаем настройки текста заголовка        String hellotext = prefs.getString("hellotitletext", "");        switcharray[6] = findViewById(list_of_switches[6]);        switcharray[7] = findViewById(list_of_switches[7]);        //Получаем настройки количества полей        String sixfields = prefs.getString("sixfields", "true");        String sevenfields = prefs.getString("sevenfields", "false");        String eightfields = prefs.getString("eightfields", "false");        if (sixfields.equals("true")){            switcharray[6].setVisibility(View.GONE);            switcharray[7].setVisibility(View.GONE);        }        else if (sevenfields.equals("true")) {            switcharray[6].setVisibility(View.VISIBLE);            switcharray[7].setVisibility(View.GONE);        }        else if (eightfields.equals("true")) {            switcharray[6].setVisibility(View.VISIBLE);            switcharray[7].setVisibility(View.VISIBLE);        }        //Создаем массив из TextView        TextView[] textarr = new TextView[8];        //Каждому переключателю назначаем текст из итерации поля tsfield        for (int i=0; i<8;i++){            textarr[i] = (TextView) findViewById(list_of_switches[i]);            textarr[i].setText(tsfield[i]);        }        //Назначаем текст заголовка            TextView textView5 = (TextView) findViewById(R.id.textView5);            textView5.setText(hellotext);        //Отображать заголовок, если соотв. поле заполнено        if(!hellotext.matches(""))        {            textView5.setVisibility(View.VISIBLE);        }        //Создаем связь каждого элемента переключателя по id из XML с соответствующей переменной типа SwitchCompat        for (int i=0;i<9;i++) {            switcharray[i] = findViewById(list_of_switches[i]);        }        //Создаем связь кнопки по id bt_next из xml переменной NextButton        NextButton = findViewById(R.id.bt_next);        //Используем SharedPreferences = "save"        SharedPreferences sharedPreferences = getSharedPreferences("save"                , MODE_PRIVATE);        //При первом запуске - все переключатели в False        for (int k=0; k<9; k++) {        switcharray[k].setChecked(sharedPreferences.getBoolean("value"+k, false));        }        //При переключении переключателей сохраняем данные, а также, проверяем их при повторном запуске        for (int k=0; k<9; k++) {            final int finalK = k;            switcharray[k].setOnClickListener(new View.OnClickListener() {                @Override                public void onClick(View view) {                    if (switcharray[finalK].isChecked()) {                        //когда переключатель включен                        Reset = true; //                        switcharray[finalK].setBackgroundColor(Color.parseColor("#c8a2c6"));                        SharedPreferences.Editor editor = getSharedPreferences("save"                                , MODE_PRIVATE).edit();                        editor.putBoolean("value" + finalK, true);                        editor.apply();                        switcharray[finalK].setChecked(true);                    } else {                        //когда переключатель выключен                        SharedPreferences.Editor editor = getSharedPreferences("save"                                , MODE_PRIVATE).edit();                        editor.putBoolean("value" + finalK, false);                        Reset = false;                        switcharray[finalK].setBackgroundColor(Color.TRANSPARENT);                        editor.apply();                        switcharray[finalK].setChecked(false);                    }                }            });        }        //Кнопка открытия страницы настроек        NextButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go to next activity                Intent intent2 = new Intent(MainActivity.this, Activity_settings.class);                startActivity(intent2);            }        });    }}

Следующим этапом будет написание кода для корректной работы макета Activity_Settings.XML, а логика его такова:

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

  • Количество полей соответствуют числу, заданному в настройках из макета Activity_Advanced.xml

  • А также, должны работать оставшиеся кнопки меню.

Код по спойлером: 124 строчки
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.net.Uri;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;public class Activity_settings extends AppCompatActivity {    //Initialize Variable    Button btBack;    Button fcSubmit;    Button btAdvanced;    //Ассоциируем поля ввода с переменными с помощью массива    EditText[] InputFields = new EditText[8];    //Назначаем полям значения по умолчанию и сохраняем их в Intent    String[] tsfield = new String[8];    //Создаем массив элементов из XML по id    public int[] list_of_fields = {            R.id.inputField0,            R.id.inputField1,            R.id.inputField2,            R.id.inputField3,            R.id.inputField4,            R.id.inputField5,            R.id.inputField6,            R.id.inputField7,    };    private SharedPreferences prefs;    //Создание формы / открытие приложения    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_settings);        //Назначаем полям значения по умолчанию и сохраняем их в Intent        prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE);        //Получаем данные по количеству используемых полей        String sixfields = prefs.getString("sixfields", "true");        String sevenfields = prefs.getString("sevenfields", "false");        String eightfields = prefs.getString("eightfields", "false");        EditText inputField71var = (EditText) findViewById(list_of_fields[6]);        EditText inputField81var = (EditText) findViewById(list_of_fields[7]);        if (sixfields.equals("true")){            inputField71var.setVisibility(View.INVISIBLE);            inputField81var.setVisibility(View.INVISIBLE);        }        else if (sevenfields.equals("true")) {            inputField71var.setVisibility(View.VISIBLE);            inputField81var.setVisibility(View.INVISIBLE);        }        else if (eightfields.equals("true")) {            inputField71var.setVisibility(View.VISIBLE);            inputField81var.setVisibility(View.VISIBLE);        }        tsfield[0] = prefs.getString("KEY_F0", "Выключил газ");        tsfield[1] = prefs.getString("KEY_F1", "Выключил воду");        tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек");        tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна");        tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет");        tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь");        tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет");        tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор");        //Назначаем полям ввода текст из SharedPreferences        for (int i=0; i<8; i++) {            InputFields[i] = (EditText) findViewById(list_of_fields[i]);            InputFields[i].setText(tsfield[i]);        }        //Создаем переменные для кнопок        btBack = findViewById(R.id.bt_back);        fcSubmit = findViewById(R.id.submit_fc);        btAdvanced = findViewById(R.id.btAdvanced);        //Кнопка Назад        btBack.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go back                Intent intent = new Intent (                        Activity_settings.this,MainActivity.class                );                startActivity(intent);            }        });        //Кнопка Расширенные настройки/Дополнительно        btAdvanced.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Open Advanced Settings                Intent intent = new Intent (                        Activity_settings.this,Activity_advanced.class                );                startActivity(intent);            }        });    }    //Ссылка-значок на внешний ресурс - ссылка на мой телеграм    public void tglink(View view){        Intent myWebLink = new Intent(android.content.Intent.ACTION_VIEW);        myWebLink.setData(Uri.parse("https://t.me/EndlessNights"));            startActivity(myWebLink);    }    //Кнопка Сохранить данные    public void SaveData(View view)    {        for (int i=0; i<8;i++) {            tsfield[i] = InputFields[i].getText().toString();        SharedPreferences.Editor editor = prefs.edit();        editor.putString("KEY_F"+i, tsfield[i]);            editor.apply();        }        // Открываем главную страницу        startActivity(new Intent(getApplicationContext(), MainActivity.class));    }}

И наконец опишем логику работы последнего окна в приложении - с Дополнительными настройками:

  • Количество полей для отображения - в данном случае выбор с помощью радиокнопок - 6, 7 или 8 полей.

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

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

  • И наконец должны работать оставшиеся кнопки меню.

Код под спойлером: 134 строчки
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.CompoundButton;import android.widget.EditText;import android.widget.RadioButton;import android.widget.RadioGroup;import android.widget.Switch;import android.widget.TextView;public class Activity_advanced extends AppCompatActivity {    Button btBack;    //Назначаем радиокнопкам значения по умолчанию    Boolean sixbool = true;    Boolean sevenbool = false;    Boolean eightbool = false;    private SharedPreferences prefsadv;    //Поле ввода текста для заголовка    private EditText hellotitletext;    RadioGroup rdGroup;    //Переменные для радиокнопок    public RadioButton r1, r2, r3;    //Переменные для передачи состояния из boolean в sharedPrefs    String sixdata;    String sevendata;    String eightdata;    Switch bgswitchvar;    private SharedPreferences prefs;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_advanced);        bgswitchvar = findViewById(R.id.bgswitch);        prefsadv = getSharedPreferences("MY_DATA", MODE_PRIVATE);        rdGroup = (RadioGroup)findViewById(R.id.radioGroup);        //Поле заголовка        String hellotitletext1 = prefsadv.getString("hellotitletext","");        hellotitletext = (EditText) findViewById(R.id.hellotitletext);        hellotitletext.setText(hellotitletext1);        //Ассоциируем переменные с полями по id из xml        r1 = findViewById(R.id.sixfields);        r2 = findViewById(R.id.sevenfields);        r3 = findViewById(R.id.eightfields);        //При нажатии на радиокнопку, вызываем функцию Update с заданным ключом        r1.setChecked(Update("rbsix"));        r2.setChecked(Update("rbseven"));        r3.setChecked(Update("rbeight"));        //При нажатии первой кнопки добавляем True с ключом rbsix в RBDATA        r1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r1_isChecked) {                SaveIntoSharedPrefs("rbsix", r1_isChecked);            }        });        //При нажатии второй кнопки добавляем True с ключом rbsix в RBDATA        r2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r2_isChecked) {                SaveIntoSharedPrefs("rbseven", r2_isChecked);            }        });        //При нажатии третьей кнопки добавляем True с ключом rbsix в RBDATA        r3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r3_isChecked) {                SaveIntoSharedPrefs("rbeight", r3_isChecked);            }        });        //Back button        btBack = findViewById(R.id.btBackadvanced);        btBack.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go back                Intent intent = new Intent (                        Activity_advanced.this,Activity_settings.class                );                startActivity(intent);            }        });    }    //Сохранение данных в SharedPreferences - ожидая ключ и значение булева типа    private void SaveIntoSharedPrefs(String key, boolean value){        SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE);        SharedPreferences.Editor editor = sp.edit();        editor.putBoolean(key,value);        editor.apply();    }    //Функция обновления значения в SharedPreferences    private boolean Update(String key){        SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE);        return sp.getBoolean(key, false);    }    //Сохраняем данные по количеству полей    public void SaveDataAdvanced(View view)    {        int checkedId = rdGroup.getCheckedRadioButtonId();        if(checkedId == R.id.sixfields) {            sixbool = true;            sevenbool = Boolean.FALSE;            eightbool = Boolean.FALSE;        }        else if (checkedId == R.id.sevenfields){            sevenbool = true;            sixbool = Boolean.FALSE;            eightbool = Boolean.FALSE;        }        else if (checkedId == R.id.eightfields){            eightbool = true;            sevenbool = Boolean.FALSE;            sixbool = Boolean.FALSE;        }        sixdata = String.valueOf(sixbool);        sevendata = String.valueOf(sevenbool);        eightdata = String.valueOf(eightbool);        String hellofield = hellotitletext.getText().toString();        SharedPreferences.Editor editor = prefsadv.edit();        editor.putString("sixfields", sixdata);        editor.putString("sevenfields", sevendata);        editor.putString("eightfields", eightdata);        editor.putString("hellotitletext", hellofield);        editor.apply();        startActivity(new Intent(getApplicationContext(), MainActivity.class));    }}

Подготовка приложения к публикации

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

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

Регистрация в Google Play

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

Далее вам предстоит оплатить пошлину в $35 за возможность публиковать приложения, это почти в 3 раза дешевле, чем в Steam, при том, что Steam просит $100 за каждое публикуемое приложение/игру, даже бесплатное, а с аккаунтом разработка, в Google Play вы можете публиковать несчётное множество приложений.

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

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

Возвращаемся в Android Studio и необходимо заполнить немного информации о нашем приложении, для этого нажимаем File>Project Structure и заполняем поля Version Code и Version Name - без них Google Play Google Play не допустит ваше приложение до публикации:

Наконец, переходим в следующий раздел: пункт меню Build>Generate Signed Bundle / APK

В открывшимся окне выбираем APK. В подразделе Key Store Path выбираем Create new, далее заполняем все поля (прямая ссылка на официальную инструкцию), далее данный ключ потребуется загрузить в консоль Google Play. Затем вернемся в Android Studio и после ввода всех необходимых данных, нажимаем Next

В следующем окне отмечаем все чекбоксы, выбираем release и нажимаем Finish - Android Studio скомпилирует подписанное приложение, которое можно опубликовать в Google Play.

Итог

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

Наконец отправляем приложение в публикацию. Сотрудники Google Play будут проверять ваше приложение в течении 2 недель, судя по официальным данным. Данное приложение рассматривали в течении 5 суток. Также, стоит учесть, что каждое обновление, также, будут проверять, но на обновления уходит не более 2-3 суток.

Ссылка на GitHub, как обещано. Ссылка на приложение в Google Play.

Подробнее..

Программа для создания desktop-файлов

31.03.2021 16:16:13 | Автор: admin

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

Немного о desktop-файлах

Вот пример desktop-файла для консольной игры nsnake:

[Desktop Entry]Version=1.1Type=ApplicationName=nsnakeGenericName=Classic snake game on the terminalNoDisplay=false//отображать в менюIcon=nsnakeExec=nsnakeTerminal=true//запускать в терминалеActions=Categories=ActionGame;Game;Keywords=snake;ncurses;textual;terminal;

Тут и так все понятно, но я все-таки прокомментировал пару позиций. Самое главное, что нужно прописать это название приложения (Name), путь до исполняемого файла (Exec) и путь до иконки (Icon). Если иконки нет и нет желания ее искать или создавать, то некоторые окружения рабочего стола установят иконку по умолчанию.

Краткое описание

Исходники приложения находятся здесь. Программа довольно простая. Выглядит она вот так:

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

В хидербаре находится кнопка, при нажатии на которую в файловом менеджере откроется папка, находящаяся по пути /.local/share/applications. Именно там программасохраняет созданные файлы.

Как работает

Приложение написано на языке Vala с помощью среды разработки GNOME Builder. Устанавливал самую свежую версию среды (40.0) из репозитория Flathub. Оказалось, что визуальный дизайнер в этой версии еще багованнее, чем в предыдущей, поэтому интерфейс делал в Glade.

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

        button_open.clicked.connect(on_open_directory);        button_create.clicked.connect(on_create_file);        directory_path = Environment.get_home_dir()+"/.local/share/applications";        GLib.File file = GLib.File.new_for_path(directory_path);         if(!file.query_exists()){//проверяем существует ли директория            alert("Error!\nPath "+directory_path+" is not exists!\nThe program will not be able to perform its functions.");            button_create.set_sensitive(false);//деактивация кнопки CREATE           }

При нажатии на кнопку CREATE вызывается метод on_create_file:

private void on_create_file (){       if(is_empty(entry_name.get_text())){//проверяем введено ли имя файла             alert("Enter the name");             entry_name.grab_focus();//устанавливаем фокус             return;         }         GLib.File file = GLib.File.new_for_path(directory_path+"/"+entry_name.get_text().strip()+".desktop");         if(file.query_exists()){//проверяем есть ли файл с таким именем            alert("A file with the same name already exists");            entry_name.grab_focus();            return;         }         var dialog_create_desktop_file = new Gtk.MessageDialog(this,Gtk.DialogFlags.MODAL,Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Create file "+file.get_basename()+" ?");          dialog_create_desktop_file.set_title("Question");          Gtk.ResponseType result = (Gtk.ResponseType)dialog_create_desktop_file.run ();          dialog_create_desktop_file.destroy();          if(result==Gtk.ResponseType.OK){              create_desktop_file();//создаем файл          }   }

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

private void create_desktop_file(){         string display;         if(checkbutton_no_display.get_active()){//проверяем первый чекбокс             display="true";         }else{             display="false";         }         string terminal;         if(checkbutton_terminal.get_active()){//проверяем второй чекбокс             terminal="true";         }else{             terminal="false";         }         string desktop_file="[Desktop Entry]Encoding=UTF-8Type=ApplicationNoDisplay="+display+"Terminal="+terminal+"Exec="+entry_exec.get_text().strip()+"Icon="+entry_icon.get_text().strip()+"Name="+entry_name.get_text().strip()+"Comment="+entry_comment.get_text().strip()+"Categories="+entry_categories.get_text().strip();//записываем содержимое будущего файла в переменную        string path=directory_path+"/"+entry_name.get_text()+".desktop";        try {            FileUtils.set_contents (path, desktop_file);//создаем файл        } catch (Error e) {            stderr.printf ("Error: %s\n", e.message);        }        GLib.File file = GLib.File.new_for_path(path);         if(file.query_exists()){//проверяем существование файла             alert("File "+file.get_basename()+" is created!\nPath: "+path);         }else{             alert("Error! Could not create file");         }       }

Чтобы просмотреть готовые файлы существует метод on_open_directory. Он срабатывает при нажатии на кнопку в хидербаре.

private void on_open_directory(){            try{                Gtk.show_uri_on_window(this, "file://"+directory_path, Gdk.CURRENT_TIME);            }catch(Error e){                alert("Error!\n"+e.message);            }       }

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

private void on_open_exec(){        var file_chooser = new Gtk.FileChooserDialog ("Choose a file", this, Gtk.FileChooserAction.OPEN, "_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.ACCEPT);        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {            entry_exec.set_text(file_chooser.get_filename());        }        file_chooser.destroy ();   }

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

private void on_open_icon () {        var file_chooser = new Gtk.FileChooserDialog ("Select image file", this, Gtk.FileChooserAction.OPEN, "_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.ACCEPT);    Gtk.FileFilter filter = new Gtk.FileFilter ();file_chooser.set_filter (filter);//установка фильтра для изображенийfilter.add_mime_type ("image/jpeg");        filter.add_mime_type ("image/png");        Gtk.Image preview_area = new Gtk.Image ();file_chooser.set_preview_widget (preview_area);//установка области предпросмотраfile_chooser.update_preview.connect (() => {string uri = file_chooser.get_preview_uri ();string path = file_chooser.get_preview_filename();if (uri != null && uri.has_prefix ("file://") == true) {try {Gdk.Pixbuf pixbuf = new Gdk.Pixbuf.from_file_at_scale (path, 250, 250, true);preview_area.set_from_pixbuf (pixbuf);//установка изображенияpreview_area.show ();//показываем область предпросмотра} catch (Error e) {preview_area.hide ();//скрываем область предпросмотра}} else {preview_area.hide ();}});        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {            entry_icon.set_text(file_chooser.get_filename());        }        file_chooser.destroy ();       }

Метод для вывода сообщений пользователю:

private void alert (string str){          var dialog_alert = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, str);          dialog_alert.set_title("Message");          dialog_alert.run();          dialog_alert.destroy();       }

Чтобы проверить введено ли какое-либо значение в текстовое поле используется такой метод:

private bool is_empty(string str){          return str.strip().length == 0;        }

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

Дополнительная ссылка на SourceForge. До встречи в следующих постах!

Подробнее..

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

05.04.2021 00:18:36 | Автор: admin

Вступление

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

  • Чтоб заработать кучу денег и прославиться?

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

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

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

Начальная разработка. Ожидание-реальность

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

Рис.1. Ожидаемый результатРис.1. Ожидаемый результат

могут превратится в анекдот. Хорошо если у тебя получится что-то такое:

Рис.2. Хороший результатРис.2. Хороший результат

Но если в итоге выйдет что-то такое:

Рис.3. Ржачный результатРис.3. Ржачный результат

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

Что лучше? 2д или 3д на начальном этапе?

Многие начинающие разработчики могут сказать, что 2д проще. Тем что это 2д. Ну согласиться я не могу. Лучше то, что нравится. Нравится 3д? делай в 3д. Нравится 2д? Делай в 2д. Ведь разница тут только в координатной плоскости. А суть та же самая. Ведь что в 2д что в 3д тебе придется двигать персонажа. Что там что там обрабатывать триггеры. Что там, что там писать ИИ (искусственный интеллект) для врага. То, что имеет меньшую размерность не значит, что проще. Как говорит знаменитая пословица: Главное не размер, а умение пользоваться. Так что главное не размерность координатной плоскости, а то как ты умеешь с ней обращаться. Везде есть свои плюсы и минусы. Если рассматривать на уровне графики, то в 2д тебе нужно нарисовать спрайт и потом секвенцию кадров для анимации, ну а в 3д, нужно смастерить 3д модель и анимировать с помощью костевой анимации (если живое существо) или достаточно просто создать шар и используя ключи анимации просто двигать его, вращать, масштабировать и т.д. Но если нет художественного вкуса, то может получится что-то консервное (вспомним Хагрида). Так что, если говорить уж про размерность, то каждый выбирает по вкусу. Тот, кто хорошо владеет 3д пространством, тот с легкостью может перейти на 2д, а тот, кто хорошо владеет 2д, то тому нужно представить еще одну ось и адаптироваться.

Стоит ли платить за обучение?

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

Unity vs Unreal Engine (UE)

Как ты уже догадался речь пойдет о игровых движках. Какой лучше выбрать? Ответ дам простой какой понравится. Ведь в этом то я тебя ограничивать не буду. Есть много игровых движков, как профессиональных, так и простых. Но Unity и UE считаются самыми популярными. Но ты можешь посмотреть и множество других, таких как CryEngine, Godot, Creation Engine или какой-то конструктор по типу Construct, или вообще можешь написать свй и не от кого не зависеть.

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

  • Unity может справиться даже из слабыми ПК, ну конечно не из самыми древними мумиями, но 4 гб оперативы и 1 гб видеокарты потянет. Не для масштабного ААА проекта, а для какой-то простенькой игрушки вполне сойдет.

    Конечно программировать будешь на C#, так как JavaScript был выпилен из движка. Ну а если ты жестокий фанат JS, то можешь скачать раннюю версию и наслаждаться разработкой. Так же можешь использовать внешние плагины чтоб программировать например на Python.
    А вообще на этом движке можешь создавать хоть ААА проекты, хоть обычные 2D платформеры для различных устройств (хоть для Android, PC, IOS, tvOS (только представь, твой чудик, который создашь, будет бегать по всех теликах планеты)) так как он считается кроссплатформенным. Только набей свой комп хорошей начинкой и в бой за орденами.

  • Unreal Engine как минимум 8 гб оперативы точно нужно. Так как это мощный игровой движок и Unity уступать ничем не собирается. Тут без проблем создавай что хочешь, хоть ААА, хоть 2D, хоть 2D с элементами ААА. Только одень свой ПК пристойно, чтоб он не залагал только при нажатии на сам ярлык. А так этот движок тоже кроссплатформенный, как и Unity. Правда язык программирования тут С++ и BluePrint (визуальный скриптинг для тех кто не шарит в программировании). Все что душе угодно. Разработчики так устроили это логово чтоб заманить всех кто шарит и тех кто не шарит в программировании. Все что вашей душе угодно лишь бы вы хоть что-то делали.

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

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

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

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

А если работа не приносит удовольствия, то это каторга. Особенно если эта каторга за копейки. Многие, кто работают на нелюбимой работе просто говорят, что они ничего не умеют и это единственный их доход, а на лучшую работу нужно лучшие навыки. ТАК БЛИН ЧТО ЖЕ МЕШАЕТ ТЕБЕ ИХ ПРИОБРЕСТИ? Ты можешь временно работать на этой работе для того чтоб получать з/п и с голоду не откинуться, а в свободное время можешь приобретать навыки в любимом занятии.

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

Выводы

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

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

Удачи!

Подробнее..

Простое сложное программирование

15.04.2021 16:11:54 | Автор: admin


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

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

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

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

Выбор попугаев для измерения


Я не стал придумывать свои или вычислять эмпирические метрики программного кода, и в качестве попугая решил взять самую простую метрику SLOC (англ.Source Lines of Code) количество строк исходного текста компилятора, которая очень легко вычисляется.

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

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

Но для численной оценки сложности кода в рамках одного проекта, метрика SLOC подходит хорошо.

Методика подсчета SLOC


Изначально попробовал использовать простой bash скрипт с поиском по маске и подсчетом числа строк в файлах исходника через wc -l. Но через некоторое время стало понятно, что приходится изобретать очередной велосипед.
Ну вы поняли:



Поэтому решил взять уже готовый. После быстрого поиска остановился на утилите SLOCCount, которая умеет анализировать почти три десятка типов исходников.
Список типов файлов для автоматического анализа
1. Ada (.ada, .ads, .adb)
2. Assembly (.s, .S, .asm)
3. awk (.awk)
4. Bourne shell and variants (.sh)
5. C (.c)
6. C++ (.C, .cpp, .cxx, .cc)
7. C shell (.csh)
8. COBOL (.cob, .cbl) as of version 2.10
9. C# (.cs) as of version 2.11
10. Expect (.exp)
11. Fortran (.f)
12. Haskell (.hs) as of version 2.11
13. Java (.java)
14. lex/flex (.l)
15. LISP/Scheme (.el, .scm, .lsp, .jl)
16. Makefile (makefile) not normally shown.
17. Modula-3 (.m3, .i3) as of version 2.07
18. Objective-C (.m)
19. Pascal (.p, .pas)
20. Perl (.pl, .pm, .perl)
21. PHP (.php, .php[3456], .inc) as of version 2.05
22. Python (.py)
23. Ruby (.rb) as of version 2.09
24. sed (.sed)
25. SQL (.sql) not normally shown.
26. TCL (.tcl, .tk, .itk)
27. Yacc/Bison (.y)



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

Меня изначально интересовал объем исходников на С/С++ и может быть еще на Ассемблере, если таких файлов окажется достаточно для много. Но после начала работы очень обрадовался, что не стал изобретать велосипед, а взял готовую тулзу, т.к. она отдельно считала статистику исходных файлов синтаксического анализатора Yacc/Bison (.y), который и определяет фактическую сложность парсера (читай сложность синтаксиса языка программирования).

Старые исходники gcc брал с gcc.gnu.org/mirrors.html, но перед запуском анализатора удалили каталоги других компиляторов (ada, fortran, java и т.д.), чтобы они не попадали в итоговую статистику.

Результаты в попугаях.


Таблица




Объем кода синтаксического анализатора Yacc/Bison


Объем общей которой базы GCC (только для языков C и C++)

Выводы


К сожалению синтаксический анализатор Yacc/Bison использовался только до 3 версии, а после его использование свелось на нет. Поэтому оценить сложность синтаксиса С/С++ с помощью объема кода парсера можно лишь примерно до 1996-98 года, после чего его стали постепенно выпиливать, т.е. чуть менее, чем за десять лет. Но даже за этот период объем кодовой базы синтаксического анализатора вырос двукратно, что примерно соответствует по времени реализации стандарта C99.

Но даже если не учитывать код синтаксического анализатора, то объем общей кодовой базы так же коррелирует с внедрением новых стандартов C++: C99, С11 и C14.

На графике не видно выраженного пика для С+17 и следующих версий, но могу предположить, что при текущем объеме кода (более 4 миллионов строк только С и С++ кода), несколько тысяч строк, необходимых для поддержки синтаксических конструкций новых стандартов просто незаметно.

Вывод первый очевидный. Рост сложности инструментов разработки


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

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

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

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

Вывод второй порог входа


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

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


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

Итоговый вывод не утешительный


Если рассматривать увеличение сложности только самого ПО, то это одно дело. Вот к примеру
Статистика ядра Linux с вики
17 сентября 1991: Linux версии 0.01 (10 239 строк кода).
14 марта 1994: Linux версии 1.0.0 (176 250 строк кода).
Март 1995: Linux версии 1.2.0 (310 950 строк кода).
9 июня 1996: Linux версии 2.0.0 (777 956 строк кода).
25 января 1999: Linux версии 2.2.0, изначально довольно недоработанный (1 800 847 строк кода).
4 января 2001: Linux версии 2.4.0 (3 377 902 строки кода).
18 декабря 2003: Linux версии 2.6.0 (5 929 913 строк кода).
23 марта 2009: Linux версии 2.6.29, временный символ Linux тасманский дьявол Tuz (11 010 647 строк кода).
22 июля 2011: релиз Linux 3.0 (14,6 млн строк кода).
24 октября 2011: релиз Linux 3.1.
15 января 2012: релиз Linux 3.3 преодолел отметку в 15 млн строк кода.
23 февраля 2015: первый релиз-кандидат Linux 4.0 (более 19 млн строк кода).
7 января 2019: первый релиз-кандидат Linux 5.0 (более 26 млн строк кода).

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

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

Блокнот на языке Vala

25.04.2021 20:19:15 | Автор: admin

В этом посте я расскажу о простом блокноте на языке программирования Vala. Программа создавалась с использованием среды разработки GNOME Builder и редактора интерфейсов Glade.

Внешний вид

Вот так приложение выглядит:

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

Иерархия элементов интерфейса в редакторе Glade:

Создание заметки

Нажав на первую кнопку, пользователь сразу же создает заметку с именем по умолчанию. За это отвечает следующий метод:

private void on_add_clicked(){        GLib.File file = GLib.File.new_for_path(directory_path+"/"+date_time());        try {            FileUtils.set_contents (file.get_path(), "");//создаем пустой файл        } catch (Error e) {            stderr.printf ("Error: %s\n", e.message);        }        if(!is_empty(text_view.buffer.text)){              text_view.buffer.text = "";        }         show_notes();//показываем список заметок      }

Метод date_time, который дает имя заметке:

private string date_time(){         var now = new DateTime.now_local ();         return now.format("%d")+"."+now.format("%m")+"."+now.format("%Y")+"  "+now.format("%H")+":"+now.format("%M")+":"+now.format("%S");    }

Идем дальше. Метод для показа списка:

private void show_notes () {           list_store.clear();           list = new GLib.List<string> ();            try {            Dir dir = Dir.open (directory_path, 0);            string? file_name = null;            while ((file_name = dir.read_name ()) != null) {                list.append(file_name);            }        } catch (FileError err) {            stderr.printf (err.message);        }         Gtk.TreeIter iter;           foreach (string item in list) {               list_store.append(out iter);               list_store.set(iter, Columns.TEXT, item);           }       }

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

Удаление заметки

За удаление заметок отвечает вот такой метод:

private void on_delete_clicked(){         var selection = tree_view.get_selection();           selection.set_mode(Gtk.SelectionMode.SINGLE);           Gtk.TreeModel model;           Gtk.TreeIter iter;           if (!selection.get_selected(out model, out iter)) {               alert("Choose a note");               return;           }           GLib.File file = GLib.File.new_for_path(directory_path+"/"+item);         var dialog_delete_file = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL,Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Delete note "+file.get_basename()+" ?");         dialog_delete_file.set_title("Question");         Gtk.ResponseType result = (Gtk.ResponseType)dialog_delete_file.run ();         dialog_delete_file.destroy();         if(result==Gtk.ResponseType.OK){         FileUtils.remove (directory_path+"/"+item);//удаляем файл         if(file.query_exists()){            alert("Delete failed");//не получилось удалить         }else{             show_notes();             text_view.buffer.text = "";//очищаем текстовую область         }      }   }

Удаление происходит только после подтверждения этого действия пользователем.

Сохранение заметок

Для сохранение заметок существует следующий код:

private void on_save_clicked(){         var selection = tree_view.get_selection();           selection.set_mode(Gtk.SelectionMode.SINGLE);           Gtk.TreeModel model;           Gtk.TreeIter iter;           if (!selection.get_selected(out model, out iter)) {               alert("Choose a note");               return;           }         if(is_empty(text_view.buffer.text)){             alert("Nothing to save");             return;         }         GLib.File file = GLib.File.new_for_path(directory_path+"/"+item);        var dialog_save_file = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL,Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Save note "+file.get_basename()+" ?");         dialog_save_file.set_title("Question");         Gtk.ResponseType result = (Gtk.ResponseType)dialog_save_file.run ();         if(result==Gtk.ResponseType.OK){         try {            FileUtils.set_contents (file.get_path(), text_view.buffer.text);//записываем в файл содержимое текстовой области        } catch (Error e) {            stderr.printf ("Error: %s\n", e.message);        }          show_notes();      }      dialog_save_file.destroy();      }

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

Сохранение заметок под другим именем

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

private void on_save_as_clicked(){        var selection = tree_view.get_selection();           selection.set_mode(Gtk.SelectionMode.SINGLE);           Gtk.TreeModel model;           Gtk.TreeIter iter;           if (!selection.get_selected(out model, out iter)) {               alert("Choose a note");//нужно выбрать заметку из списка               return;           }        if(is_empty(text_view.buffer.text)){             alert("Nothing to save");//нечего сохранять             return;         }        var dialog_save_note = new Gtk.Dialog.with_buttons ("Save note", this, Gtk.DialogFlags.MODAL);var content_area = dialog_save_note.get_content_area ();        entry_name = new Gtk.Entry();        var label_name = new Gtk.Label.with_mnemonic ("_Name:");        var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 20);        hbox.set_border_width(15);        hbox.pack_start (label_name, false, true, 0);        hbox.pack_start (entry_name, true, true, 0);content_area.add (hbox);dialog_save_note.add_button ("OK", Gtk.ResponseType.OK);dialog_save_note.add_button ("CLOSE", Gtk.ResponseType.CLOSE);dialog_save_note.response.connect (on_save_response);dialog_save_note.show_all ();      }

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

Для обработки нажатий на кнопки OK и CLOSE понадобится метод on_save_response:

private void on_save_response (Gtk.Dialog dialog, int response_id) {        switch (response_id) {case Gtk.ResponseType.OK:if(is_empty(entry_name.get_text())){    alert("Enter the name");//нужно ввести имя        entry_name.grab_focus();        return;}GLib.File select_file = GLib.File.new_for_path(directory_path+"/"+item);GLib.File edit_file = GLib.File.new_for_path(directory_path+"/"+entry_name.get_text().strip());if (select_file.get_basename() != edit_file.get_basename() && !edit_file.query_exists()){                FileUtils.rename(select_file.get_path(), edit_file.get_path());//переименовываем файл                if(!edit_file.query_exists()){                    alert("Rename failed");//не получилось переименовать                    return;                }                try {                 FileUtils.set_contents (edit_file.get_path(), text_view.buffer.text);              } catch (Error e) {                     stderr.printf ("Error: %s\n", e.message);            }            }else{                if (select_file.get_basename() != edit_file.get_basename()) {                    alert("A note with the same name already exists");//такое имя уже есть                    entry_name.grab_focus();                    return;                }                try {                 FileUtils.set_contents (edit_file.get_path(), text_view.buffer.text);              } catch (Error e) {                     stderr.printf ("Error: %s\n", e.message);             }            }            show_notes();        dialog.destroy();break;case Gtk.ResponseType.CLOSE:        dialog.destroy();        break;case Gtk.ResponseType.DELETE_EVENT:dialog.destroy();break;}}

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

Отображение текста заметки

Чтобы показать содержимое заметки используется такой код:

private void on_select_item () {           var selection = tree_view.get_selection();           selection.set_mode(Gtk.SelectionMode.SINGLE);           Gtk.TreeModel model;           Gtk.TreeIter iter;           if (!selection.get_selected(out model, out iter)) {               return;           }           Gtk.TreePath path = model.get_path(iter);           var index = int.parse(path.to_string());           if (index >= 0) {               item = list.nth_data(index);           }          string text;            try {                FileUtils.get_contents (directory_path+"/"+item, out text);            } catch (Error e) {               stderr.printf ("Error: %s\n", e.message);            }            text_view.buffer.text = text;//показываем текст заметки       }

На этом все! До встречи в следующих постах!


Дата-центр ITSOFT размещение и аренда серверов и стоек в двух дата-центрах в Москве. За последние годы UPTIME 100%. Размещение GPU-ферм и ASIC-майнеров, аренда GPU-серверов, лицензии связи, SSL-сертификаты, администрирование серверов и поддержка сайтов.

Подробнее..

Подборка 150 ресурсов для управления и работы ИТ-команды

22.05.2021 16:14:00 | Автор: admin

Привет! На связи компанияKODE. Мы занимаемся разработкой мобильных приложений, голосовых интерфейсов, IoT и других цифровых решений для государства и крупного бизнеса в России и Европе с 2013 года.

Руководители наших отделов собрали полноценную библиотеку IT-компании: сайты, блоги, книги, онлайн-курсы, подкасты, Telegram- и YouTube-каналы. Подборка будет полезна менеджерам, аналитикам, разработчикам, дизайнерам и QA.

Направления:

Сохраняйте, чтобы не потерять!Используйте и совершенствуйтесь.


Проджект-менеджмент

Сайты:

Книги:

Курсы:

YouTube-каналы:

Telegram-каналы:

  • Psilonsk канал Сергея Колганова об управлении проектами и продуктами.

  • Селиховкин о проектном управлении в другом формате.

Аналитика

Книги:

Курсы:

Telegram-каналы:

UX/UI-дизайн

Сайты:

Книги:

Курсы:

Фильмы:

YouTube-каналы:

Android-разработка

Сайты:

Книги:

Курс:

Подкасты:

  • Fragmented подкаст о том, как стать лучшим разработчиком ПО.

  • Сушите вёсла о разработке, аналитике, тестировании и других аспектах создания приложений.

  • CoRecursive истории, скрывающиеся за кодом, от экспертов в мире разработки.

  • Signals And Threads интервью о тонкостях разработки с инженерами из глобальной торговой компании Jane Street.

  • Software Engineering Radio еженедельные беседы о ПО.

  • Microsoft Research Podcast о передовых технологиях Microsoft Research.

Telegram-каналы:

Twitter:

iOS-разработка

Сайты:

Книги:

Курсы:

Подкаст:

  • Подлодка еженедельное аудиошоу о разработке.

YouTube- и Telegram-каналы:

  • YouTube-каналПола Хадсонапо SwiftUI.

  • Telegram-каналdeprecated чат для новичков, где разбирают сложные для них вопросы.

Backend-разработка

Книги:

YouTube-каналы:

Telegram-каналы:

Frontend-разработка

Ресурсы и сайты:

Блоги:

  • Дэн Абрамов личный блог разработчика, одного из авторов Redux.

  • Katz Got Your Tongue статьи Иегуды Каца, соавтора Ember.js и ответственного за разработку плагинов в jQuery.

Книги:

Подкасты:

  • FrontoWeek важные события фронтенда за неделю.

  • Веб-стандарты ещё один новостной канал.

  • UnderJS обсуждения JS на Frontend и Backend, React Native, Linux.

  • Фронтенд Юность вся правда о фронтенд-разработке.

  • Frontend Weekend интервью с известными людьми из веб-разработки.

  • Пятиминутка React подкаст о React и смежных технологиях в мире JavaScript и фронтенда.

  • kamyshev.talk об архитектуре, коде и гибких навыках.

YouTube-каналы:

Telegram-каналы:

QA

Сайты:

  • ПорталSoftware Testing сотни тематических статей, подборок книг по тестированию и обзор новостей отрасли.

  • БлогQCoder концентрат полезных знаний.

Курсы:

Telegram-каналы:

YouTube-каналы:

  • Любительский канал Алексея Баренцева полезные видео для начинающих тестировщиков.

  • QAGuild об автоматизации тестирования и ИТ.

  • Heisenbug доклады с международной технической QA-конференции.

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

DevOps

Книги:

Telegram-каналы:


Не забудьте поделиться с коллегами и теми, кому может быть интересна эта подборка!

Подробнее..

Подборка 150 ресурсов для управления и работы IT-команды

22.05.2021 18:12:52 | Автор: admin

Привет! На связи компанияKODE. Мы занимаемся разработкой мобильных приложений, голосовых интерфейсов, IoT и других цифровых решений для государства и крупного бизнеса в России и Европе с 2013 года.

Руководители наших отделов собрали полноценную библиотеку IT-компании: сайты, блоги, книги, онлайн-курсы, подкасты, Telegram- и YouTube-каналы. Подборка будет полезна менеджерам, аналитикам, разработчикам, дизайнерам и QA.

Направления:

Сохраняйте, чтобы не потерять!Используйте и совершенствуйтесь.


Проджект-менеджмент

Сайты:

Книги:

Курсы:

YouTube-каналы:

Telegram-каналы:

  • Psilonsk канал Сергея Колганова об управлении проектами и продуктами.

  • Селиховкин о проектном управлении в другом формате.

Аналитика

Книги:

Курсы:

Telegram-каналы:

UX/UI-дизайн

Сайты:

Книги:

Курсы:

Фильмы:

YouTube-каналы:

Android-разработка

Сайты:

Книги:

Курс:

Подкасты:

  • Fragmented подкаст о том, как стать лучшим разработчиком ПО.

  • Сушите вёсла о разработке, аналитике, тестировании и других аспектах создания приложений.

  • CoRecursive истории, скрывающиеся за кодом, от экспертов в мире разработки.

  • Signals And Threads интервью о тонкостях разработки с инженерами из глобальной торговой компании Jane Street.

  • Software Engineering Radio еженедельные беседы о ПО.

  • Microsoft Research Podcast о передовых технологиях Microsoft Research.

Telegram-каналы:

Twitter:

iOS-разработка

Сайты:

Книги:

Курсы:

Подкаст:

  • Подлодка еженедельное аудиошоу о разработке.

YouTube- и Telegram-каналы:

  • YouTube-каналПола Хадсонапо SwiftUI.

  • Telegram-каналdeprecated чат для новичков, где разбирают сложные для них вопросы.

Backend-разработка

Книги:

YouTube-каналы:

Telegram-каналы:

Frontend-разработка

Ресурсы и сайты:

Блоги:

  • Дэн Абрамов личный блог разработчика, одного из авторов Redux.

  • Katz Got Your Tongue статьи Иегуды Каца, соавтора Ember.js и ответственного за разработку плагинов в jQuery.

Книги:

Подкасты:

  • FrontoWeek важные события фронтенда за неделю.

  • Веб-стандарты ещё один новостной канал.

  • UnderJS обсуждения JS на Frontend и Backend, React Native, Linux.

  • Фронтенд Юность вся правда о фронтенд-разработке.

  • Frontend Weekend интервью с известными людьми из веб-разработки.

  • Пятиминутка React подкаст о React и смежных технологиях в мире JavaScript и фронтенда.

  • kamyshev.talk об архитектуре, коде и гибких навыках.

YouTube-каналы:

Telegram-каналы:

QA

Сайты:

  • ПорталSoftware Testing сотни тематических статей, подборок книг по тестированию и обзор новостей отрасли.

  • БлогQCoder концентрат полезных знаний.

Курсы:

Telegram-каналы:

YouTube-каналы:

  • Любительский канал Алексея Баренцева полезные видео для начинающих тестировщиков.

  • QAGuild об автоматизации тестирования и ИТ.

  • Heisenbug доклады с международной технической QA-конференции.

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

DevOps

Книги:

Telegram-каналы:


Не забудьте поделиться с коллегами и теми, кому может быть интересна эта подборка!

Подробнее..

Перевод Где поместить свой сервер, чтобы обеспечить максимальную скорость? Насколько это важно?

26.03.2021 18:19:00 | Автор: admin

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

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


По мере роста сетевых задержек могут происходить странные вещи: толстые сайты могут стать более быстрыми (особенно если они полностью обслуживаются из CDN), а тонкие сайты, использующие API-интерфейсы, могут стать более медленными. Для пользователя настольного ПК/ноутбука типичная задержка достигает 200 мс, а для мобильного пользователя 4G составляет 300400 мс. В этой статье предполагается, что пропускная способность равна 40 Мбит/с, поддерживается TLS, задержка CDN равна 40 мс и нет существующих соединений. Исходная точка здесь означает основной веб-сервер (в отличие от пограничных кэшей CDN).

Почему местоположение имеет значение

Время пересечения Интернета добавляется ко времени, затраченному на ответ на запрос. Даже если ваш API-интерфейс способен ответить на запрос за 1 мс, когда пользователь находится в Лондоне, а API-сервер в Калифорнии, пользователю всё равно придется ждать ответа около 130 мс.

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

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

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

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

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

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

API-интерфейсы, или сети CDN

Существует сеть, в которой операции выполняются быстрее, это сеть дистрибуции содержимого (Content Distribution Network, CDN). Вместо того чтобы пройти весь путь до Калифорнии, возможно, вы сможете получить часть веб-страницы из кэша в Центральном Лондоне. Такой подход экономит время. Операция может занять всего 50 мс. Экономия достигает 60 %. Кэш отлично работает для CSS-файлов, изображений и JavaScript, т. е. для ресурсов, которые не меняются для всех пользователей. Он не так хорошо работает для ответов на API-вызовы, так как ответы различны для каждого пользователя, а порой и всегда.

Количественный подход

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

Вот что я сделал:

  1. Я взял собственные журналы доступа за две недели в сентябре сразу после того, как опубликовал что-то новое. За этот период я получил около миллиона запросов от 143 тыс. уникальных IP-адресов. Я исключил очевидных роботов (на которые пришлись около 10 % запросов).

  2. Я использовал базу данных GeoIP компании Maxmind для привязки каждого IP-адреса в этих журналах доступа к соответствующим географическим координатам.

  3. Затем я использовал опубликованные на сайте WonderNetwork данные о задержках в интернет-соединениях между примерно 240 городами мира.

  4. Я сопоставил (полуавтоматически, что было довольно мучительно) названия этих городов с идентификаторами Geonames, что позволило получить координаты городов.

  5. Затем я загрузили всё вышеперечисленное в базу данных Postgres с установленным расширением PostGIS для выполнения географических запросов.

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

Результаты

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

  • среднее (P50);

  • для трёх четвертей запросов (P75);

  • для 99 % запросов (P99).

Все числа выражены в миллисекундах.

Все результаты см. в таблице

City

p50

p75

p99

Manhattan

74

97

238

Detroit

89

115

245

Secaucus

71

96

246

Piscataway

75

98

251

Washington

82

105

253

Chicago

90

121

253

Kansas City

98

130

254

Indianapolis

96

125

254

St Louis

96

127

256

Cincinnati

92

121

257

Houston

104

134

257

Syracuse

77

102

257

Scranton

78

103

258

Quebec City

83

113

259

South Bend

92

118

259

Montreal

83

104

259

Charlotte

91

110

259

Salem

74

98

259

Buffalo

80

111

259

Albany

75

100

260

Monticello

94

123

260

Baltimore

80

105

260

Asheville

95

118

260

New York

77

103

261

Berkeley Springs

84

112

261

Minneapolis

102

133

261

Barcelona

102

148

261

Dallas

112

140

262

Des Moines

104

131

262

San Jose

139

165

263

Brunswick

77

101

264

Atlanta

88

113

264

San Francisco

136

168

264

Halifax

80

102

265

Philadelphia

77

100

266

Basel

97

146

267

Green Bay

103

131

267

Pittsburgh

88

117

267

Bern

99

147

267

Denver

112

141

267

Miami

103

129

267

Raleigh

88

111

268

Knoxville

114

135

268

Boston

77

105

268

Valencia

108

148

268

Jackson

105

132

268

Memphis

101

131

268

Jacksonville

95

122

268

Madrid

95

138

268

London

76

130

268

San Diego

138

162

269

San Antonio

112

138

269

Salt Lake City

120

151

269

Toronto

87

111

269

Cleveland

97

122

269

Austin

113

141

270

Colorado Springs

110

136

270

Orlando

103

126

270

Antwerp

93

137

271

Oklahoma City

114

147

271

Saskatoon

115

140

272

Lansing

98

127

272

Seattle

141

164

272

Columbus

92

120

273

Bristol

76

129

274

Tampa

104

130

274

Lausanne

95

139

274

Ottawa

85

111

274

Falkenstein

91

137

275

Maidstone

76

129

275

Paris

80

129

275

Toledo

102

129

275

Savannah

117

146

276

The Hague

82

138

276

Liege

87

136

277

Lincoln

100

124

277

New Orleans

115

142

278

Amsterdam

82

140

278

Las Vegas

136

163

279

Vienna

102

149

279

Coventry

80

132

279

Cromwell

80

106

280

Arezzo

109

160

280

Cheltenham

79

131

280

Sacramento

137

167

280

Alblasserdam

82

137

281

Vancouver

142

165

281

Fremont

131

157

283

Gosport

76

137

284

Frankfurt

93

136

284

Carlow

88

136

285

Phoenix

128

153

285

Portland

132

159

285

Cardiff

78

131

285

Luxembourg

87

137

285

Bruges

83

135

285

Eindhoven

85

133

285

Groningen

87

139

286

Manchester

80

137

286

Brussels

90

139

287

Brno

106

148

287

Edinburgh

84

136

287

Nuremberg

89

136

288

Albuquerque

125

159

289

Los Angeles

141

164

289

Ljubljana

110

152

289

Lugano

97

147

290

Zurich

103

146

290

Dronten

84

133

290

Newcastle

87

147

290

Rome

96

147

291

Dusseldorf

90

140

291

Munich

98

144

291

Venice

106

156

292

Edmonton

139

165

292

Copenhagen

96

145

292

St Petersburg

113

163

293

Dublin

85

143

293

Redding

142

178

293

Vilnius

110

162

293

Belfast

79

125

294

Nis

113

158

294

Douglas

87

143

294

Rotterdam

82

139

295

Bergen

107

157

295

Strasbourg

89

141

295

Roseburg

148

172

296

Graz

104

147

296

San Juan

117

141

298

Warsaw

108

161

299

Frosinone

105

153

299

Riyadh

159

206

300

Prague

103

152

301

Ktis

102

158

302

Mexico

139

164

302

Belgrade

113

160

302

Guadalajara

128

155

303

Milan

96

146

305

Bratislava

102

154

306

Osaka

181

240

307

Zagreb

103

150

308

Tallinn

108

162

308

Helsinki

105

156

308

Hamburg

127

166

309

Oslo

98

153

311

Bucharest

120

162

311

Riga

113

159

312

Panama

150

177

313

Tokyo

188

238

313

Kiev

119

168

313

Stockholm

102

153

314

Budapest

110

162

314

Kharkiv

128

169

315

Gothenburg

115

167

316

Pristina

122

167

316

Tirana

128

184

316

Geneva

96

142

316

Siauliai

113

163

317

Cairo

133

182

318

Sapporo

196

255

318

Bogota

170

188

319

Palermo

119

183

320

Gdansk

107

152

320

Caracas

149

176

320

Sofia

114

161

321

Westpoort

79

134

321

Honolulu

173

196

321

Roubaix

102

157

321

Kazan

138

190

322

Winnipeg

169

190

322

Varna

120

173

322

Tel Aviv

138

194

322

Lisbon

115

166

324

Jerusalem

145

198

324

Ankara

139

195

327

Heredia

164

188

327

Athens

128

183

329

Reykjavik

127

180

329

Paramaribo

166

194

330

Algiers

120

173

332

Chisinau

127

180

333

Bursa

135

188

334

Thessaloniki

134

187

336

Limassol

141

186

337

Lyon

95

145

340

Mumbai

204

248

340

Medellin

163

186

344

Valletta

120

176

345

Baku

160

205

346

Melbourne

227

269

346

Fez

149

198

348

Tunis

124

180

348

Koto

217

254

348

Dubai

192

243

350

Tbilisi

153

208

351

Malaysia

195

235

352

Hyderabad

214

260

354

Bangalore

212

252

355

Izmir

137

187

357

Adelaide

241

272

359

Chennai

221

248

359

Moscow

127

172

359

Lahore

217

270

361

Novosibirsk

163

206

362

Sydney

237

272

363

Karaganda

180

231

363

Vladivostok

223

264

364

Taipei

265

293

364

Lima

169

199

364

Istanbul

135

182

366

Hong Kong

199

223

366

Auckland

244

291

367

Jakarta

207

245

368

Seoul

231

277

371

Beirut

136

195

372

Accra

168

216

373

Singapore

190

246

374

Sao Paulo

193

213

375

Joao Pessoa

182

220

378

Perth

243

267

379

Ho Chi Minh City

253

287

380

Wellington

251

295

383

Brasilia

226

249

384

Manila

251

281

385

Pune

202

251

386

Dhaka

231

268

386

Phnom Penh

243

267

386

Santiago

202

230

390

Lagos

191

233

391

Quito

162

188

392

New Delhi

230

264

395

Johannesburg

237

283

398

Bangkok

222

254

401

Canberra

262

295

402

Dar es Salaam

214

267

407

Dagupan

239

268

408

Christchurch

257

309

409

Hanoi

235

264

415

Cape Town

216

262

417

Buenos Aires

232

253

417

Guatemala

217

249

418

Brisbane

261

288

422

Indore

304

352

457

Zhangjiakou

236

264

457

Nairobi

233

277

468

Kampala

244

287

480

Hangzhou

239

267

517

Shenzhen

242

275

523

Shanghai

300

367

551

Montevideo

738

775

902

Вы также можете загрузить все результаты как csv-файл, если так проще.

Результат: восточное побережье Северной Америки хорошо, прямо в Атлантике лучше

Все лучшие места находятся в Северной Америке. Это, вероятно, не стало полным сюрпризом, учитывая, что это довольно плотный кластер носителей английского языка, от которого не так далеко (с точки зрения задержки), в Великобритании/Ирландской Республике, находится другой кластер. Кроме того, в Европе много тех, для кого английский второй язык. Лучше всего находиться прямо в Атлантике: в штатах Нью-Джерси и Нью-Йорк есть много отличных мест для P99, и показатели в верхней части, между P50 и P99, варьируются не слишком сильно.

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

В настоящее время мой сервер находится в Хельсинки. Это был самый дешёвый вариант, что необычно для Финляндии. За этот сервер я плачу около трёх фунтов в месяц, всего лишь. Если бы я переместил его куда-нибудь в Нью-Джерси и потратил больше денег, в целом пользователи определённо сэкономили бы время: половина операций передачи и подтверждения была бы завершена за 75, а не за 105 мс, что позволило бы сэкономить 30 %. За несколько операций передачи и подтверждения время, вероятно, увеличилось бы примерно на шестую долю секунды по сравнению со средним показателем загрузки первой страницы, что не так уж плохо. Если вы не можете сказать, что этот веб-сайт очень обременителен для веб-браузеров с точки зрения визуализации, сокращение времени ожидания в сети сделает его значительно быстрее.

Поскольку на этом сайте ничего не генерируется динамически, в действительности мне лучше всего использовать CDN. Это действительно сэкономило бы много времени для всех: обслуживание из CDN почти в два раза лучше (~40 мс) установки сервера в самом быстром месте (71 мс).

Как это может меняться со временем

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

Город

Расстояние (км)

Реальная задержка

Теоретический максимум

Фактор замедления

Нью-Йорк

5 585

71

37

1,9

Лима

10 160

162

68

2,4

Джакарта

11 719

194

78

2,5

Каир

3 513

60

23

2,6

Санкт-Петербург

2 105

38

14

2,7

Бангалор

8 041

144

54

2,7

Богота

8 500

160

57

2,8

Буэнос-Айрес

11 103

220

74

3,0

Лагос

5 006

99

33

3,0

Москва

2 508

51

17

3,0

Сан-Паулу

9 473

193

63

3,1

Бангкок

9 543

213

64

3,3

Гонконг

9 644

221

64

3,4

Стамбул

2 504

60

17

3,6

Лахор

6 298

151

42

3,6

Токио

9 582

239

64

3,7

Ханчжоу

9 237

232

62

3,8

Шанхай

9 217

241

61

3,9

Mumbai

7 200

190

48

4,0

Тайбэй

9 800

268

65

4,1

Дакка

8 017

229

53

4,3

Сеул

8 880

269

59

4,5

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

Как видно из таблицы, задержка в Нью-Йорке находится в пределах коэффициента 2 от значения для скорости света, но маршруты в другие места, такие как Дакка и Сеул, намного медленнее: они в 4 раза превышают значения для скорости света. Вероятно, маршрут между Лондоном и Нью-Йорком так хорошо оптимизирован по вполне понятным причинам. Я сомневаюсь, что то, что между этими городами в основном лежит океан, представляет большую проблему, так как подводные кабели можно прокладывать напрямую. Добираться до Сеула или Дакки придётся более окольным путём.

Вероятно, следует упомянуть, что новые протоколы обещают уменьшить количество операций передачи и подтверждения. Версия TLS 1.3 позволяет создать зашифрованный сеанс с одной операцией передачи и подтверждения, а HTTP3 может объединить операции передачи и подтверждения протоколов HTTP и TLS. Это означает, что теперь нужны лишь три такие операции: одна для DNS, одна-единственная операция передачи и подтверждения для соединения и зашифрованного сеанса, и, наконец, третья для темы запроса.

Некоторые люди ложно надеются, что новые протоколы, такие как HTTP3, устранят необходимость в объединении (bundling) JavaScript/CSS. Это основано на недоразумении: в то время как HTTP/3 удаляет некоторые исходные операции передачи и подтверждения, эта версия не удаляет последующие операции передачи и подтверждения для дополнительных данных JavaScript или CSS. Так что объединение, к сожалению, никуда не делось.

Слабые стороны данных

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

Во-первых, база данных GeoIP неоднозначно показывает местоположение IP-адресов. Заявленная (т. е., вероятно, оптимистичная) точность колеблется до 1000 км в некоторых случаях, хотя для моего набора данных утверждается, что средняя точность составляет 132 км со стандартным отклонением 276 км. Это не так уж точно, но я думаю, всё ещё полезно.

Мой источник данных о задержке, WonderNetwork, действительно сообщает данные о задержке на момент их получения (30 ноября 2020 года) в отличие от долгосрочных данных. Иногда в определённых местах Интернет барахлит.

У WonderNetwork много станций, но их охват не идеален. На Западе всё отлично в Великобритании представлены даже второстепенные города (вроде Ковентри). Их охват во всём мире по-прежнему хорош, но более неоднозначен. У них не так много мест в Африке или Южной Америке, а некоторые задержки в Юго-Восточной Азии кажутся странными: задержка между Гонконгом и Шэньчжэнем составляет 140 мс, тогда как эти города находятся всего в 50 км друг от друга. Этот фактор замедления более чем в тысячу раз превышает значение для скорости света. Для других городов континентального Китая проверка связи также показывает плохие результаты (что странно), хотя и не в таком масштабе. Может быть, эти коммунисты проверяют каждый ICMP-пакет вручную?

Другая проблема с данными о задержке заключается в отсутствии истинных координат центров обработки данных, в которых находятся серверы. Мне пришлось самому геокодировать данные с помощью некоторых сценариев и большого количества ручного ввода данных в Excel (я опубликовал эту таблицу Excel в github, чтобы никому не пришлось делать эту работу заново). Я очень старательно проверял данные, но ошибки всё ещё могут быть.

Однако, по моему мнению, самая большая слабость заключается в том, что все начинают отсчёт прямо от центра своего ближайшего города. На практике это не так и добавляемое из-за этого смещение может варьироваться. Здесь, в Великобритании, домашний доступ к Интернету это сложный процесс, основанный на отправке высокочастотных сигналов по медным телефонным линиям. Моя собственная задержка для других хостов в Лондоне достигает 9 мс, что плохо для такого короткого расстояния, но всё ещё на 31 мс лучше среднего значения. Многие маршрутизаторы потребительского уровня не очень хороши и добавляют значительную задержку. Печально известная проблема излишней сетевой буферизации ещё один распространённый источник задержки. Особенно это влияет на процессы, для хорошей работы которых требуется последовательный уровень задержки. В качестве примера можно привести видеоконференц-связь и многопользовательские компьютерные игры. Использование сети мобильных телефонов тоже не помогает. Сети 4G в хороших условиях добавляют около 100 мс задержки, но, конечно, всё гораздо хуже, когда уровень сигнала низок и есть много ретрансляций на уровне канала.

Я пытался предположить глобальную среднюю задержку на километр (около 0,03 мс), чтобы компенсировать расстояние до ближайшего города, но обнаружил, что это просто добавило кучу шума к моим результатам, так как для многих IP-адресов в моём наборе данных окольный путь был нереалистичным: ближайший город, который я им приписал, совсем не так близок.

Универсальность

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

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

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

Бесплатные операции передачи и подтверждения

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

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

Операции передачи и подтверждения легко добавить случайно. Особенно удивительным источником операций передачи и подтверждения служат предварительные запросы общего доступа к ресурсам независимо от источника (CORS). По соображениям безопасности, связанным с предотвращением атак на основе межсайтового скриптинга, браузеры должны проверять определённые HTTP-запросы, созданные кодом JavaScript. Для этого на тот же URL-адрес предварительно отправляется запрос с помощью специальной команды OPTIONS. На основе полученного ответа принимается решение о допустимости исходного запроса. Правила, определяющие точный момент отправки предварительных запросов, сложны, но в сети появляется удивительное количество запросов: в частности, включая POST-запросы JSON к поддоменам (например, к поддомену api.foo.com от домена foo.com) и сторонние веб-шрифты. В предварительных проверках на основе CORS-запросов используется другой набор заголовков кэширования по сравнению с остальным HTTP-кэшированием, которые редко задаются правильно и в любом случае применимы только к последующим запросам.

В наши дни многие сайты написаны как одностраничные приложения, где вы загружаете некоторый статический пакет JavaScript (надеюсь, из CDN), а затем создаёте (надеюсь, небольшое) количество API-запросов внутри своего браузера, чтобы решить, что показывать на странице. Есть надежда, что этот процесс станет быстрее после первого запроса, так как не придётся перерисовывать весь экран, когда пользователь попросит загрузить вторую страницу. Как правило, это не помогает, потому что одна HTML-страница обычно заменяется несколькими последовательными API-вызовами. Пара последовательных API-вызовов к исходному серверу почти всегда обрабатывается медленнее перерисовки всего экрана, особенно в сети мобильной связи.

Я всегда считаю немного вздорной ситуацию, когда я получаю загружающуюся панель на веб-странице вы уже отправили мне страницу, но почему сразу не отправили нужную мне страницу?! Один из величайших парадоксов Интернета заключается в том, что, хотя Google не очень хорошо справляется с обходом таких одностраничных приложений, эта компания, безусловно, создаёт большое их количество. Особенно дьявольской является консоль поиска (веб-сайт, ранее известный как средства веб-мастера). Я полагаю, Google не нужно слишком беспокоиться об оптимизации поисковых систем (SEO).

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

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

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

Смотрите также

В прошлом году в центре APNIC проанализировали производительность CDN по всему миру и пришли к выводу, что задержка 40 мс типична. Мне бы хотелось, чтобы в эту публикацию были включены процентильные данные, но у меня сохраняется смутное впечатление, что сети CDN лучше всего работают на Западе и менее хорошо в Южной Америке, Китае и Африке, что является проблемой, учитывая, что большинство серверов находится на Западе.

Пока я писал эту статью, произошла вспышка клубов, основанных на весе страницы, таких как Клуб 1МБ и, предположительно, более элитный Клуб 512К. Пожалуй, я одобряю это чувство (и всё это во имя веселья, я уверен). Я думаю, что они слишком подчёркивают размер передаваемых данных. Если вы в Лондоне запрашиваете динамически генерируемую страницу из Калифорнии, весь процесс всё равно займёт большую часть секунды (130 мс умножить на 5 операций передачи и подтверждения), независимо от того, насколько велик размер этой страницы.

На карту подводных кабелей всегда приятно смотреть. Если вы хотите увидеть признак разной важности разных мест: Нормандские острова (население 170 тыс. человек) имеют 8 подводных кабелей, в том числе два, которые просто соединяют Гернси и Джерси. У Мадагаскара (население 26 млн. человек) их всего четыре. Я также считаю забавным то, что, хотя Аляска и Россия довольно близки, между ними нет ни одного кабеля.

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

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

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

Робопрактика в режиме онлайн для мобильных разработчиков в red_mad_robot

12.03.2021 10:04:49 | Автор: admin

Салют мобильным! Роботы открывают весеннюю робопрактику для iOS- и Android-разработчиков. Проходить всё будет в онлайне, а стартуем уже в апреле. Познакомим с проектными бизнес-процессами и внутренней кухней компании, активируем режим Turbo Boost и погрузимся в мир разработки. Лучших практикантов пригласим к себе в команду.

Что будет

Робопрактика продлится 9 недель. Обещаем дружную команду и постоянную прокачку: мы с удовольствием поделимся своим опытом и знаниями. Робопрактику проведут лучшие технические специалисты red_mad_robot: Артем Кулаков, Осип Фаткуллин, Сергей Иванов, Даниил Субботин, Антон Глезман modestman, Алексей Тюрнин и другие. Ты всегда сможешь подёргать за рукав опытных разработчиков и получить ответ на все вопросы.

Кого мы ждём

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

Что предстоит изучить

Робопркатика состоит из лекций и семинаров. Чтобы сосредоточиться на самом важном, разделим практикантов на две рабочие группы: iOS и Android. Дадим сложные и интересные домашние задания и организуем занятия на Swift/Kotlin один-два раза в неделю с 18:30 до 20:30 по МСК. А ещё практиканты будут работать над проектом, который мы подготовили специально для них!

Проектирование:

  • MVC, MVP, MVVM в iOS и Android-приложениях;

  • уместное и правильное применение шаблонов проектирования;

  • разработка по принципам повторного использования;

  • поддержка кодовой базы в чистоте и актуальном состоянии после каждого WWDC и Google I/O;

  • Android Architecture Components.

Многопоточность:

  • работа с Kotlin Coroutines в Android;

  • от NSOperation до OSAtomic и POSIX в iOS.

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

  • модель угроз: как уберечь доверчивых пользователей от них самих;

  • защита соединения между клиентом и сервером;

  • хорошие практики шифрования и хранения данных на устройстве;

  • OWASP Mobile Top-10;

  • root на Android, как с этим жить и писать безопасные приложения.

UI:

  • как стать UI-джедаем и AutoLayout-ниндзя;

  • утилиты, которые мы написали, чтобы облегчить себе работу.

Автоматизация сборки:

  • как работает сборка в Xcode: таргеты, схемы, конфигурации и воркспейсы;

  • автоматизация с помощью Fastlane.

Клиент-серверные взаимодействия:

  • устройство баз данных и как это нас касается;

  • REST, проектирование хороших API;

  • эффективное взаимодействие с inhouse-командой backend.

Коммуникация в производстве:

  • как общаться с BA, DES, QA и не сойти с ума;

  • бизнес-процессы вне разработки: тест-кейсы, нарезка и прочие точки контакта.

Как записаться

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

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

Хочу на iOS

Хочу на Android

Если не успел подать заявку

Не беда. Разработчиков с опытом мы приглашаем на собеседование, а начинающих приглашаем на робопрактику в следующем году. Если есть вопросы, пиши на school@redmadrobot.com обо всём расскажем.

Подробнее..

Жизнь без AppStore и Google Play работаем с Huawei Mobile Services и AppGallery

07.04.2021 16:22:59 | Автор: admin

С конца 2019 Huawei поставляет Android-смартфоны без сервисов Google, в том числе без привычного всем магазина приложений Google Play. В качестве альтернативы китайская компания предлагает собственные разработки Huawei Mobile Services (HMS), а также магазин AppGallery. В этом тексте я разработчик Технократии Алина Саетова расскажу, как с этим жить и работать.

В статье мы рассмотрим:

  • начало работы c Huawei-системой

  • внедрение Huawei Mobile Services в приложение

  • отладка и тестирование на удаленных устройствах Huawei

  • публикация в AppGallery

Видеоверсию статьи смотрите здесь на канале Технократии.

С чего начать?

Чтобы взаимодействовать с Huawei-системой, нужно завести Huawei ID. Это аналог google-аккаунта, с помощью которого предоставляется доступ к сервисам системы. Далее нужно зарегистрировать аккаунт разработчика: индивидуальный или корпоративный.

  • Индивидуальному разработчику нужно ввести свои ФИО, адрес, телефон, почту. В отличие от регистрации аккаунта разработчика в Google Play, нужны также сканы паспорта и банковской карты. Да-да, документы требуются для удостоверения личности. Huawei обещает удалить их после регистрации.

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

Ждем одобрения аккаунта. За 1-2 дня Huawei обещают проверить наши данные. После этого можно подключать приложение к HMS. Для этого заходим в консоль AppGallery Connect.

  1. Создаем проект, а в нем добавляем приложение

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

2.Помещаем конфигурационный файл agconnect-services.json в корневую папку приложения. Также сохраняем хэш SHA-256. Он потребуется для аутентификации приложения, когда оно попытается получить доступ к службам HMS Core.

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

keytool -list -v -keystore <keystore path> -alias <key alias> -storepass <store password> -keypass <key password>

Для работы некоторых сервисов нужно указать место хранения данных:

3.Добавляем зависимости в проект Android Studio.В build.gradle на уровне проекта:

buildscript {      repositories {          google()          jcenter()          maven { url 'https://developer.huawei.com/repo/' }      }      dependencies {      ....        classpath 'com.huawei.agconnect:agcp:1.4.2.301'     }  }allprojects {      repositories {          google()          jcenter()          maven {url 'https://developer.huawei.com/repo/'}      }  }

В build.gradle в модуле app:

apply plugin: 'com.android.application'apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'apply plugin: 'kotlin-kapt'...apply plugin: 'com.huawei.agconnect'android {...}dependencies {...implementation "com.huawei.agconnect:agconnect-core:1.4.1.300...}

4.Для предотвращения обфускации AppGallery Connect сервисов, Huawei рекомендует прописать следующие правила в файле proguard-rules.pro на уровне модуля app:

  • Для ProGuard:

-ignorewarnings -keep class com.huawei.agconnect.**{*;}
  • Для DexGuard:

-ignorewarnings-keep class com.huawei.agconnect.** {*;} -keepresourcexmlelements ** -keepresources */*

Первоначальная настройка проекта с Huawei Mobile Services завершена.

Внедряем HMS сервисы в проект

Почти на каждый сервис Google у Huawei есть альтернатива:

  • Push Kit. Отправка пуш-уведомлений пользователям.

  • Auth Service. В дополнение к привычным способам аутентификации здесь присутствует вход по Huawei ID.

  • Crash Service. Cервис для отслеживания крашей приложения.

  • Cloud Storage, Cloud DB. Хранение различных файлов и база данных.

  • Location Kit. Получение местоположения пользователя.

  • Analytics Kit. Анализ статистических данных приложения.

  • In-App Purchases. Совершение покупок в приложении.

  • Cloud Testing, Cloud Debugging. Тестирование приложений на удаленных устройствах Huawei.

Этот список можно продолжать долго у Huawei довольно обширный перечень сервисов. Как же подключить их в наш проект?

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

  • Полностью заменяем GMS сервисы на HMS сервисы

  • Делаем комбинацию GMS и HMS сервисов в одном проекте

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

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

  1. В меню выбираем HMS > Convertor > New Conversion:

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

3.Здесь плагин представляет результаты анализа проекта: какие GMS сервисы у нас содержатся и какие из них конвертируемые. Также нам предлагается проверить sdk version для соответствия требованиям HMS.

На этом шаге мы должны выбрать стратегию конвертации:

  • Add HMS API. На основе существующих в проекте GMS APIs генерируется XMS adapter (как дополнительный модуль в проекте). Он представляет собой прослойку между нашим кодом и непосредственно вызовом сервисов. Это такие Extension-классы, в которых лежит код, поддерживающий HMS и GMS сервисы одновременно. В runtime определяется поддерживаемый девайсом вид сервисов и вызываются соответствующие методы.

  • To HMS API полностью заменяются GMS APIs на HMS APIs.

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

По клику на каждый пункт произойдет навигация в файл, где будет предложена конвертация:

Если был выбран способ Add HMS API, мы можем посмотреть на сгенерированный xms адаптер. Вот так, например, выглядит метод из класса ExtensionUser:

А вот размер xms адаптер модуля при использовании лишь одного API с аутентификацией пользователя:

По итогу, APK нашего приложения увеличивается (old size - это APK приложения с only GMS, new size - APK с GMS и HMS одновременно):

Не сказать, что разница велика, но если в приложении будет использоваться несколько API?

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

В политике Google Play есть замечание:

Any existing app that is currently using an alternative billing system will need to remove it to comply with this update. For those apps, we are offering an extended grace period until September 30, 2021 to make any required changes. New apps submitted after January 20, 2021 will need to be in compliance.

Что это значит для нас? Теперь, если приложение одновременно поддерживает HMS и GMS сервисы, и в нем есть In-App Purchases, то Google Play не допустит его публикации, а существующим приложениям придется удалить этот функционал.В итоге, если был выбран первый способ конвертации (Add HMS API), мы имеем:

  • Большое количество сгенерированных классов.

  • Увеличенный размер APK приложения.

  • Невозможность публикации приложения в Google Play, если в нем есть In-App Purchases.

  • Неполную поддержку одновременной работы HMS & GMS для некоторых сервисов.

Решение: Более привлекательным вариантом кажется второй способ конвертации простая замена GMS APIs на HMS APIs. Но вместе с этим используем product flavors, чтобы получать сборки приложения отдельно для Google Play и AppGallery.

Product Flavors

Создадим два product flavor - hms и gms:

  • Общий код будет располагаться в директории main/

  • Укажем sourceSets в файлах build.gradle модулей (только там, где необходимо разделение на hms и gms)

  • Код с GMS имплементацией будет в папке gms/, а с HMS соответственно в hms/

  • У hms flavora указываем applicationIdSuffix = .huawei

  • Если же нет необходимости заводить целые файлы отдельно для каждого flavora, то можно проверять текущий flavor через BuildConfig.FLAVOR

android {        flavorDimensions 'services'    productFlavors {        hms {            dimension 'services'            applicationIdSuffix '.huawei'        }        gms {            dimension 'services'        }    }}

По умолчанию, Android Studio заводит sourceSet main, в котором содержатся общие файлы с кодом. Создаем папки для каждого flavora:

New -> Folder -> Выбираем нужный тип папки:

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

android {        productFlavors {        ...    }    sourceSets {        hms {            java {                srcDirs 'src/hms/java'            }            ...        }    }}

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

Пример. Мы используем Auth API. У нас будет абстракция интерфейс AuthRepository, хранящийся в main/, а его имплементации для разных сервисов лежат в gms/ и hms/ директориях тогда в сборку, например, для HMS, попадет именно имплементация с huawei сервисами.

Если проект многомодульный, то в каждом модуле необходимо прописать flavorы и при необходимости source sets. Код с flavorами можно вынести в отдельный файл.

Создадем .gradle файл в корневой папке проекта, назовем его flavors.gradle:

ext.flavorConfig = {    flavorDimensions 'services'    productFlavors {        hms {            dimension 'services'            ext.mApplicationIdSuffix = '.huawei'        }        gms {            dimension 'services'        }    }    productFlavors.all { flavor ->        if (flavor.hasProperty('mApplicationIdSuffix') && isApplicationProject()) {            flavor.applicationIdSuffix = flavor.mApplicationIdSuffix        }    }}def isApplicationProject() {    return     project.android.class.simpleName.startsWith('BaseAppModuleExtension')}

Помимо самих flavorов, в экстеншене flavorConfig лежит код с циклом по flavorам там будет определяться app модуль, которому присваивается applicationIdSuffix.

Затем в каждом модуле прописываем следующее:

apply from: "../flavors.gradle"android {    buildTypes {        ...    }    ...    with flavorConfig}

Для использования подходящих плагинов во время процесса компиляции можем добавлять такие if-else конструкции:

apply plugin: 'kotlin-kapt'...if(getGradle().getStartParameter().getTaskNames().toString().toLowerCase().contains("hms")) {    apply plugin: 'com.huawei.agconnect'} else {    apply plugin: 'com.google.gms.google-services'    apply plugin: 'com.google.firebase.crashlytics'}...

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

// FirebasegmsImplementation platform('com.google.firebase:firebase-bom:26.1.0')gmsImplementation 'com.google.firebase:firebase-crashlytics-ktx'gmsImplementation 'com.google.firebase:firebase-analytics-ktx'// Huawei serviceshmsImplementation 'com.huawei.agconnect:agconnect-core:1.4.2.300'hmsImplementation 'com.huawei.hms:push:5.0.4.302'hmsImplementation 'com.huawei.hms:hwid:5.0.3.301'

Тестируем и отлаживаем приложение

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

У Huawei есть облачная платформа DigiX Lab, в которой представлены 2 сервиса.

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

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

Тесты можно запускать либо с помощью плагина в Android Studio:

Либо в консоли AppGallery, выгрузив туда свой APK:

Служба облачной отладки решает проблему отсутствия реальных устройств Huawei. Предоставляется список удаленных устройств, а разовый сеанс работы до 2 часов. Сервис дает 24 часа работы бесплатно после подтверждения личности. Можно подавать заявки на продление срока действия неограниченное количество раз. Отладка также доступна из Android Studio и консоли.

Публикуем приложение в AppGallery

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

1.Переходим в AppGallery Connect и заполняем данные:

2.Грузим иконку приложения и скриншоты. Есть возможность прикрепить видео.

3.Указываем страны/регионы для публикации и грузим APK приложения. Кроме того, нужно загрузить подпись приложения.

4.Отмечаем способ покупок в приложении и рейтинг.

5.Грузим политику конфиденциальности (обязательно) и предоставляем данные тестового аккаунта, если это необходимо. Указываем дату публикации.

6.Нажимаем кнопочку Отправить на проверку и ждем! Проверка по регламенту занимает около 3-5 дней.

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

  1. Политика конфиденциальности не соответствует стандарту

    • Отсутствует ссылка на политику конфиденциальности.

    • Ссылка на политику конфиденциальности недоступна.

    • Ссылка на политику конфиденциальности ведет на официальный сайт компании, на котором нет ссылки на политику конфиденциальности.

  2. Указанный статус Гонконга и Макао не соответствует стандарту.Гонконг и Макао не могут быть указаны как страны на странице выбора региона. Китай очень трепетно относится к этому. Пример:

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

Функция для оценки и написания отзыва в приложении содержит ссылку на сторонние магазины приложений без ссылки на AppGallery

Итоги

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

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

Подписывайтесь на наш Telegram-канал Голос Технократии, где мы пишем о новостях из мира ИТ и высказываем свое мнение о важных событиях.

Подробнее..

Гайд по тестированию рекламы для мобильных приложений

15.06.2021 18:13:51 | Автор: admin

Тестировать рекламные механики не так просто, как может показаться. Главные действующие лица здесь сторонние SDK, которые не особо подконтрольны команде разработки. А так как рекламные интеграции важная часть наших мобильных приложений, то ниже вместе с @maiscourt и @santypa расскажем, как мы это делаем.

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


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

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

И тут нам понадобятся некоторые инструменты.

Инструменты

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

  1. Сниффер для анализа трафика (у нас Charles).

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

  3. Своя админка с фича-тогглами, где можно включить/отключить, или изменить наши эксперименты.

  4. Наша дебаг-панель.

  5. VPN.

  6. Внешние гайдлайны.

  7. Внутренняя база знаний и Confluence для её хранения.

  8. Чек-листы.

  9. Zephyr для хранения тест-кейсов.

Пройдёмся по каждому.

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

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

Инструмент 2. Тестовая админка медиатора. Медиатор это специальная платформа, которая позволяет подключать приложение сразу к нескольким рекламным сетям, а также управлять показом рекламы (например, Google AdMob, Fyber и другие). Ещё во время онбординга мы проводим обучающие курсы по рекламе, где сотрудники в тестовой админке медиатора создают свой тестовый юнит для настройки параметров рекламы под себя.

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

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

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

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

Инструмент 5. VPN. В основном используется для проверки задач, связанных с GDPR и CCPA. Для тестирования GDPR подходит VPN с возможностью получения IP европейской страны. Для тестирования CCPA необходим VPN с возможностью получения калифорнийского IP.

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

  • рекламные креативы и их идентификаторы, которые используются для настройки и получения тестовой рекламы;

  • форматы запросов и ответов рекламной SDK, а также параметры, из описания которых понимаем, за что они отвечают, и какие возможные значения для них допустимы;

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

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

Инструмент 8. Чек-листы. Наравне с внешней и внутренней документацией для проверок различных задач используем чек-листы (пример можно посмотреть ниже в разделе про обновление SDK). Для такого типа задач, как проверка обновлений SDK, обновлений медиатора или адаптеров, мы используем уже составленный чек-лист, который изменяется по мере необходимости.

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

Инструмент 9. Тест-кейсы. Тест-кейсы неотъемлемая часть тестирования любого проекта/функциональности, в том числе рекламы. Тест-кейсы разделены по приоритетам, что позволяет использовать risk-based testing, о котором будет рассказано подробнее ниже. В тест-кейсах фигурируют такие проверки, как загрузка и показ рекламы, запросы на рекламу, работа разных механик (например: водопад, аукцион), а также запрос и отображение рекламы от партнёрских рекламных сетей. Данные проверки в полной мере позволяют убедиться, что рекламный функционал работает без сбоев/корректно.

Задачи

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

Разберём, с чем приходится сталкиваться на постоянной основе:

  1. Обновление SDK.

  2. Тестирование форматов.

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

  4. Регрессионное тестирование.

  5. Смоук тестирование.

  6. Другие задачи (юридические вопросы, локализации, эксперименты, аналитика, рефакторинг и так далее).

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

  • Вёрстка. Проверяем всё: центрирование, размер, отображение на устройствах с разными разрешениями экранов.

  • Пользовательские сценарии. Тап по контенту/кнопке и по privacy icon, возврат в приложение, воспроизведение и остановка видеорекламы.

  • Репорт (отправка жалоб, связанных с рекламой). Пользователь может пожаловаться на рекламный контент или сообщить о технических проблемах.

  • Соответствие стандартам GDPR и CCPA.

  • Отправка аналитики. Внутренняя и внешняя (партнёру и медиатору).

  • Технические проверки. Например, уход в фон во время загрузки рекламы.

Задача 2. Тестирование форматов. Баннерная и нативная рекламы у нас закрепились и работают стабильно, но мы пробуем интегрировать и другие виды, в частности, fullscreen-рекламу. В целом, тестирование Rewarded Video и Interstitial во многом схоже с тестированием других видов: проверяется корректная загрузка и отображение рекламы, а также отправка аналитики.

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

Отдельно нужно сказать про механику Rewarded Video после окончания просмотра рекламы (или просмотра до определённой временной метки) пользователь должен получить вознаграждение. Поэтому очень важно хорошо протестировать этот функционал.

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

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

Задача 4. Регрессионное тестирование. Раз в две недели iFunny релизится на iOS и Android. Независимо от количества рекламных задач, попавших в релизную ветку, мы проводим регрессионное тестирование рекламного функционала/блока. В регрессионных паках собраны следующего рода проверки:

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

  • Работа нативной и баннерной рекламы, а также рекламных сетей, с которыми сотрудничаем по сути, это упрощённый чек-лист, используемый для тестирования SDK (добавления, обновления).

  • Работа разных механик: водопад и биддинг (что это такое, можно ознакомиться здесь).

  • Проверка на соответствие юридическим нормам.

  • Проверки каких-то наших внутренних разработок (например, эксперименты с дизайном).

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

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

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

Задача 6. Другое. К тестированию других задач можно отнести:

  • юридические задачи, например, связанные с GDPR и CCPA;

  • задачи локализации (для пользователей iFunny из Бразилии);

  • эксперименты, связанные с дизайном рекламы;

  • задачи аналитики;

  • рефакторинг, оптимизации. Например, оцениваем доступный нам для анализа контент на предмет валидности.

Бонус. Онбординг новых сотрудников

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

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

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

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

Вместо заключения

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

Подробнее..

Категории

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

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