<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Блог Владислава Иванова</title>
        <link>https://vladivanov.me/</link>
        <description>Мой блог про всякие технические штуки и не только. Просто по приколу.</description>
        <lastBuildDate>Tue, 28 Apr 2026 18:17:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>awesome</generator>
        <language>ru</language>
        <image>
            <title>Блог Владислава Иванова</title>
            <url>https://static.vladivanov.me/meta/logo.png</url>
            <link>https://vladivanov.me/</link>
        </image>
        <copyright>Распространяется под лицензией CC BY-NC-SA 4.0. Все права защищены 2026, Владислав Иванов</copyright>
        <category>Разработка</category>
        <category>IT</category>
        <category>Технологии</category>
        <category>Фронтэнд</category>
        <category>Блог</category>
        <item>
            <title><![CDATA[Интернационализация и локализация]]></title>
            <link>https://vladivanov.me/i18n-and-l10n/</link>
            <guid>8547b508-fcfc-4f4a-b236-c1fc358ab156</guid>
            <pubDate>Mon, 28 Aug 2023 17:24:09 GMT</pubDate>
            <description><![CDATA[В этой статье я расскажу о том, как я сделал свой блог мультиязычным.]]></description>
            <content:encoded><![CDATA[<p>Привет!</p>
<p>Я задумывал этот блог как более серьезный по сравнению с <a href="https://yungvldai.ru/">моим первым блогом</a>. В целях расширения охвата я принял решение писать статьи на английском языке. Однако, все же некоторую часть статьи лично мне проще сформулировать на русском, а затем перевести на английский. Таким образом, после каждой публикации в моей “кузне” остается довольно много материала, который можно было бы немного причесать и опубликовать, но, к сожалению, технической возможности сделать это до недавнего времени не было.</p>
<p>Итак, сегодня я расскажу о том, как я проапгрейдил свой блог, чтобы он начал поддерживать мультиязычность, а также расскажу о проблемах, с которыми столкнулся.</p>
<h3>Теория</h3>
<p>Минутка душноты, как обычно.</p>
<p><b>Интернационализация</b>&nbsp;— это процесс разработки программных приложений, которые потенциально могут адаптироваться к различным языкам и регионам без инженерных изменений.</p>
<p><b>Локализация</b>&nbsp;— это процесс адаптации интернационализированного программного обеспечения для определенного региона или языка путем добавления&nbsp;локальных&nbsp;компонентов и переведенного текста.</p>
<p>В этом проекте я занимался и тем, и тем.</p>
<h3>Данные</h3>
<p>Разработка (имеется ввиду этап, когда мы уже перешли к написанию кода) любой фичи в моем блоге начинается с типов. Будь то пропсы компонента,&nbsp;<a href="https://ru.wikipedia.org/wiki/DTO">DTOs</a>&nbsp;или модель базы данных. Все это описывается с помощью TypeScript. Такой подход обеспечивает довольно очевидный эффект - при изменении в одном месте что-то разваливается в другом и, обычно, остается только починить нужным образом, чтобы фича заработала.</p>
<p>Я веду разработку блога в монорепозитории, поэтому и <code class="inline-code">api</code>, и <code class="inline-code">web-client</code> сервиса лежат в одном&nbsp;<a href="https://git-scm.com/">Git</a>&nbsp;репозитории. Также, у меня есть модуль <code class="inline-code">shared</code>, в нем описаны различные project-wide константы (например существующие роли, а теперь и поддерживаемые языки), а также DTO. В итоге, и контроллеры в API, и data-layer в React компонентах используют одни и те же типы. Хочу подчеркнуть, что <code class="inline-code">shared</code> модуль представляет собой просто папку, откуда просто импортируется то, что нужно. Не NPM пакет, не что-либо еще, а обычная папка c TS файлами.</p>
<p>Для обращения к БД я использую SQL query builder <a href="https://kysely.dev/"><b>Kysely</b></a>. Примерно так это выглядит:</p>
<pre><code>const { id } = await pg
    .selectFrom(&#039;articles&#039;)
    .select([&#039;id&#039;])
    .where(&#039;slug&#039;, &#039;=&#039;, slug)
    .executeTakeFirst();</code></pre>
<p>Этот запрос достанет <code><code class="inline-code">id</code></code> статьи по ее <code><code class="inline-code">slug</code></code>. На мой взгляд запись выглядит довольно понятной и лаконичной, к тому же, <b>Kysely</b> подскажет возвращаемый тип (и даже для более сложных запросов с <code class="inline-code">JOIN</code> или подзапросами). А так как каждый контроллер знает какой тип он должен отдать на клиент получается, что весь проект, несмотря на то, что разворачивается каждое приложение по отдельности, составляет единую систему.</p>
<p>В плане подходов к разработке фичей в моем блоге, данный случай, конечно, не стал исключением. В первую очередь, для поддержки мультиязычности в структуру БД пришлось внести довольно большие изменения. Все, что может быть локализовано переехало в отдельную табличку, и в итоге у каждой <code><code class="inline-code">Article</code></code> может быть сколько угодно <code><code class="inline-code">ArticleContent</code></code>, но есть unique constraint для пары <code><code class="inline-code">lang</code></code> и <code><code class="inline-code">article_id</code></code>.</p>
<img src="https://static.vladivanov.me/uploads/9d25ad46-74f8-4f5a-b748-0c33ad7f552d.webp" alt="Таблицы Articles и ArticleContents" />
<p>Я решил не заводить отдельную таблицу <code><code class="inline-code">Languages</code></code>, поэтому поле <code><code class="inline-code">lang</code></code> - это просто <code><code class="inline-code">varchar</code></code>, куда записывается двухбуквенный код языка по стандарту <a href="https://www.loc.gov/standards/iso639-2/php/code_list.php">ISO 639-1</a>. Я решил не делать этого, потому что для добавления языка недостаточно добавить запись в БД, это довольно объемная работа, в том числе и в коде.</p>
<p>После обновления схемы данных развалились почти все контроллеры, которые были на нее завязаны, я починил все ошибки и местами обновил также DTO. После обновления DTO посыпались ошибки уже со стороны клиентской части, они также были починены и на самом деле мультиязычные статьи к этому моменту были уже готовы. Хотя, в этом проекте предстояло еще много работы по переводу интерфейса, об этом расскажу далее.</p>
<h3>Шрифт</h3>
<p>Когда я придумывал этот блог, я хотел, чтобы он выглядел строго. Может быть даже как какое-нибудь печатное издание. Я перепробовал несколько шрифтов и мой выбор пал на <a href="https://fonts.google.com/specimen/Libre+Baskerville">Libre Baskerville</a>.</p>
<p>Baskerville — это шрифт с засечками (serif), разработанный в 1750-х годах Джоном Баскервиллем (1706–1775) в Бирмингеме, Англия. Baskerville классифицируется как переходный шрифт, задуманный как усовершенствование того, что сейчас называют шрифтами старого стиля того периода.</p>
<p>Libre Baskerville — это веб-шрифт, оптимизированный для основного текста. Он основан на шрифте American Type Founder's Baskerville 1941 года, но имеет более высокую <a href="https://fonts.google.com/knowledge/glossary/x_height">x-height</a>, более широкие <a href="https://fonts.google.com/knowledge/glossary/counter">counters</a> и немного меньший контраст, что делает его хорошо подходящим для чтения на экране.</p>
<p>Этот шрифт очень мне нравится и на мой субъективный взгляд очень хорошо подходит для моего блога. Однако, в ходе проектирования данной фичи было выяснено, что Libre Baskerville не поддерживает кириллические глифы. То есть при попытке отрендерить текст с русскими буквами шрифт фоллбэкнется на serif (скорее всего, это будет Times New Roman) и это будет выглядеть, мягко говоря, не очень:</p>
<img src="https://static.vladivanov.me/uploads/fcc70f4f-abe5-4715-a813-219a04c8d16a.webp" alt="Старый интерфейс с фоллбэком Times New Roman" />
<p>Конечно, это никуда не годится, поэтому я стал искать пути решения этой проблемы. Я перепробовал с десяток бесплатных шрифтов, похожих на Libre Baskerville, однако мои поиски не увенчались успехом. Тогда я даже подумал о том, чтобы приобрести платную версию шрифта Baskerville на <a href="https://www.paratype.com/fonts/pt/baskerville-display-pt">сайте Paratype</a>. За два начертания мне пришлось бы выложить 60 долларов, плюс там есть дополнительные сложности типа увеличивающейся стоимости подписки в зависимости от просмотров страницы, а еще довольно странное требование “<i>May not use the font to create content by visitors to your websites</i>”. Это довольно странно, потому что в моем случае это требование как будто бы ограничивает использование шрифта в форме для написания комментариев.</p>
<p>В общем, довольно много сомнительных и непонятных моментов, да еще и деньги придется платить, причем 60 долларов это относительно много, это примерно полгода оплаты услуг за хостинг, поэтому и этот вариант я отмел.</p>
<p>В итоге не оставалось ничего другого кроме как взять Libre Baskerville, благо он распространяется под <a href="https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&amp;id=OFL">OFL</a>, и дорисовать кириллические глифы. Я подумал, что это не очень сложно, к тому же некоторые буквы выглядят так же как и латинские и их можно просто скопировать. На самом деле это и правда несложно, но это довольно большой объем очень кропотливой работы. Конечно же, я понимаю, что я не прочувствовал на себе создание шрифта с нуля. У меня уже были готовые гайдлайны от авторов Libre Baskerville и даже некоторые буквы для сравнения. Честно говоря, думаю, что с нуля шрифт я бы не смог создать.</p>
<p>Так, потихоньку я рисовал букву за буквой. В итоге было нужно нарисовать 33 * 2 (заглавные + строчные) * 2 (regular + bold) = 132 буквы. Некоторые буквы я скопировал без изменений, некоторые сделал из других (например <b>П</b> легко сделать из <b>Н</b>, а <b>Ж</b> из двух <b>К</b>). Некоторые же пришлось прям почти с нуля рисовать (например <b>Ф</b> или <b>Л</b>), этим объясняется то, что данные буквы вышли чуть более кривыми, чем остальные. Хотя, по правде говоря, все буквы получились кривоватыми, это лучше чем фоллбэк на Times New Roman и в целом результат меня устроил.</p>
<img src="https://static.vladivanov.me/uploads/47dfb59b-d726-432f-80b0-85e2a194ed33.webp" alt="Рисование кириллических глифов" />
<p>Я использовал <a href="https://www.glyphrstudio.com/">Glyphr Studio</a> и, в целом, это довольно удобная программа, но иногда у нее течет память и она вылетает, так что приходилось сохранять каждый раз, когда очередная буква была нарисована. Увы, кое-что все таки приходилось рисовать дважды.</p>
<p>А Вам, уважаемые читатели, прямо сейчас совершенно бесплатно доступны два этих замечательных шрифта (лицензируется OFL):</p>
<p>- <a href="https://static.vladivanov.me/fonts/BlogBaskerville-Bold.ttf">BlogBaskerville-Bold.ttf</a></p>
<p>-&nbsp;<a href="https://static.vladivanov.me/fonts/BlogBaskerville-Regular.ttf">BlogBaskerville-Regular.ttf</a></p>
<p>- <a href="https://static.vladivanov.me/fonts/OFL.txt">Лицензия</a></p>
<h3>Интерфейс</h3>
<p>С интерфейсом все куда проще. Я использую&nbsp;<a href="https://www.npmjs.com/package/react-intl"><b>react-intl</b></a>&nbsp;для интернационализации. В коде используются ключи, затем по ключу в зависимости от языка подставляется нужный перевод. Также, из коробки поддерживаются плюрализация и подстановка значений, а также форматирование времени, дат, чисел и так далее.</p>
<p>Но хочу рассказать о некоторых практиках, которые помогут сделать работу с переводами проще.</p>
<p>Первая рекомендация - это хранить переводы рядом с компонентом. Давайте рассмотрим на примере компонента <code><code class="inline-code">ArticleStats</code></code> (это компонент для отображения статистики статьи, Вы могли видеть его на главной, в сайдбаре на страннице каждой статьи, а еще в результатах поиска):</p>
<pre><code>📦ArticleStats
 ┣ 📂__Icon
 ┃ ┗ 📜ArticleStats__Icon.css
 ┣ 📂__Name
 ┃ ┣ 📜ArticleStats__Name.css
 ┃ ┗ 📜ArticleStats__Name.tsx
 ┣ 📂__Stat
 ┃ ┣ 📜ArticleStats__Stat.css
 ┃ ┗ 📜ArticleStats__Stat.tsx
 ┣ 📂__Value
 ┃ ┣ 📜ArticleStats__Value.css
 ┃ ┗ 📜ArticleStats__Value.tsx
 ┣ 📂ArticleStats.helpers
 ┃ ┗ 📜capitalize.ts
 ┣ 📂ArticleStats.i18n &lt;- папка с переводами
 ┃ ┣ 📜ArticleStats.en.json
 ┃ ┗ 📜ArticleStats.ru.json
 ┣ 📜ArticleStats.css
 ┗ 📜ArticleStats.tsx</code></pre>
<p>В файле <code class="inline-code">ArticleStats.en.json</code> лежат ключи в таком виде:</p>
<pre><code>{
    &quot;ArticleStats.published&quot;: &quot;Published&quot;,
    &quot;ArticleStats.views&quot;: &quot;{views, plural, one {view} other {views}}&quot;,
    &quot;ArticleStats.mins&quot;: &quot;{mins, plural, one {min} other {mins}}&quot;
}</code></pre>
<p>В итоге все файлики переводов для одного компонента лежат в одном месте, а воедино их легко собрать простым скриптом на Node.js. Затем этот один большой файл читается на сервере и в память загружаются сразу все переводы для всех языков, а на клиент при SSR отдаются ключи только для нужного языка. Хочу заметить, что все ключи префиксятся именем блока, это нужно для того, чтобы уменьшить вероятность возникновения коллизий.</p>
<p>Второе, что я мог бы посоветовать - это запретить использование чего угодно, кроме строковых литералов при указании ключей. Поясню. Чтобы получить строку в <b>react-intl</b>, вы должны отрендерить элемент <code><code class="inline-code">FormattedMessage</code></code>:</p>
<pre><code>&lt;FormattedMessage id=&quot;ArticleStats.published&quot; /&gt;</code></pre>
<p>или вызвать метод&nbsp;<code class="inline-code">intl.formatMessage</code>:</p>
<pre><code>intl.formatMessage({ id: &#039;ArticleStats.published&#039; })</code></pre>
<p>И иногда я видел варианты с шаблонными строками, тернарными операторами и это на самом деле все только усложняет.</p>
<p>Например, вместо:</p>
<pre><code>&lt;FormattedMessage id={`ArticleStats.${action}`} /&gt; // action: &#039;published&#039; | &#039;created&#039; | &#039;updated&#039;</code></pre>
<p>Гораздо лучше написать:</p>
<pre><code>const actionsMap = {
    published: &lt;FormattedMessage id=&quot;ArticleStats.published&quot; /&gt;,
    created: &lt;FormattedMessage id=&quot;ArticleStats.created&quot; /&gt;,
    updated: &lt;FormattedMessage id=&quot;ArticleStats.updated&quot; /&gt;
};

&lt;div&gt;
    {actionsMap[action]}
&lt;/div&gt;</code></pre>
<p>Код стал чуть посложнее (хотя и не сильно), зато теперь такие вызовы можно искать хоть обычными регулярками, не говоря уже о <a href="https://www.npmjs.com/package/esquery">запросах</a> по <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE">AST</a> (<a href="https://astexplorer.net/">вот тут</a> можно поиграться с этим). Статический анализ кода в данной ситуации очень полезен, потому что так у Вас будет возможность сразу узнать о том, какие ключи не используются, чтобы их удалить! Кода становится меньше, бандлы становятся тоньше и ничего лишнего - красота.</p>
<p>На самом деле, можно еще больше улучшить DX (не придется запускать странные скрипты и разработчик сразу будет узнавать о проблеме), написав кастомное правило для <a href="https://eslint.org/">ESLint</a>. Автофиксить его, скорее всего, не получится, но и поправить такое место будет нетрудно. В большинстве случаев подобные сложные выражения раскрываются в хэшмап, либо <code class="inline-code">switch-case</code>. Если же ключа два, то можно продолжить использовать тернарный оператор, просто снаружи, а не внутри.</p>
<h3>Алгоритм</h3>
<p>В первую очередь смотрим на query параметр <code><code class="inline-code">lang</code></code> , если он есть и его значение равно одному из поддерживаемых языков, то берется указанный язык, алгоритм останавливается. Иначе, смотрим на одноименную куку. Так же, если кука есть и ее значение равно одному из поддерживаемых языков, то устанавливается указанный язык. Если же и куки нет (либо указан неизвестный язык), мы попытаемся распарсить заголовок <code><code class="inline-code">Accept-Language</code></code>, если и это не удастся, либо в <code class="inline-code">Accept-Language</code> не указан ни один поддерживаемый язык, то выбирается английский (<code class="inline-code">en</code>).</p>
<p>После определения языка происходит “залипание” на него с помощью куки. После этого сменить его можно будет только явно перейдя на страницу с параметром <code><code class="inline-code">lang</code></code>.</p>
<h3>SEO</h3>
<p>Поисковой робот должен знать о том, что на моем сайте поддерживается мультиязычность. При запросе страницы без query параметров GoogleBot’у (или другому поисковому роботу), будет отдаваться версия на английском (он не передает куки и заголовок <code class="inline-code">Accept-Language</code>), а значит алгоритм будет фоллбэкаться на <code class="inline-code">en</code>.</p>
<p>Для того, чтобы сообщить язык текущей страницы используется атрибут <code class="inline-code">lang</code> у элемента <code class="inline-code">html</code>.</p>
<img src="https://static.vladivanov.me/uploads/3c058729-fce2-4237-ac44-a82673fae251.webp" alt="HTML с lang атрибутом" />
<p>А для ссылок на другие страницы используется элементы <code class="inline-code">link</code> с атрибутом <code class="inline-code">rel</code>&nbsp;равным <code class="inline-code">alternate</code>. Выглядит это вот так:</p>
<pre><code>&lt;link rel=&quot;alternate&quot; hreflang=&quot;en&quot; href=&quot;https://vladivanov.me/&quot;&gt; &lt;!-- на себя саму тоже нужна ссылка --&gt;
&lt;link rel=&quot;alternate&quot; hreflang=&quot;en&quot; href=&quot;https://vladivanov.me/?lang=en&quot;&gt;
&lt;link rel=&quot;alternate&quot; hreflang=&quot;ru&quot; href=&quot;https://vladivanov.me/?lang=ru&quot;&gt;</code></pre>
<h3>Выкатка</h3>
<p><b>Kysely</b> умеет генерировать миграции, но в итоге и изменение структуры, и переливку данных я делал вручную прям в контейнере с БД на проде с помощью <b>psql</b>😁. Сначала я завел новую таблицу, завел индексы для поиска, сделал дамп таблицы <code><code class="inline-code">Articles</code></code>, потом накатил новую версию сервиса и уже потом дропнул старые ненужные колонки. Было немного страшно, хотя у меня вроде как есть еженедельные бэкапы от DigitalOcean, с другой стороны я никогда не пробовал из них восстанавливаться.</p>
<p>Вся фича с мультиязычностью была внесена под фичефлаг и обычные пользователи не могли ей воспользоваться, после тестирования и создания в админке версий для всех статей на русском, я вынес фичу из под фичефлага, но пока отключил определение языка по заголовку <code class="inline-code">Accept-Language</code>.</p>
<p>Сейчас вместе с этой статьей фича начала работать согласно описанию, в интерфейс была добавлена кнопка для переключения языка, а чуть позже я сделаю email рассылку для подписчиков. Это довольно значимое событие в истории развития моего блога.</p>
<h3>Заключение</h3>
<p>Это был крайне захватывающий проект, с учетом того, что он охватил весь спектр работ. А это и разработка, и администрирование БД и даже вопросы касательно дизайна (типографии). Я очень рад, что в моем блоге появилась мультиязычность, а еще я рад поделиться с Вами этим замечательным и ярким опытом.</p>
<p>Пишите в комментарии, если Вам интересно как работает мой блог, я готов рассказать Вам о любой его части.</p>
<p>Спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://static.vladivanov.me/uploads/bff1d22e-52df-47f7-bb13-139c6461ba73.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Модульные системы JavaScript]]></title>
            <link>https://vladivanov.me/module-systems-in-javascript/</link>
            <guid>678e4b7f-86a5-48f7-ac5c-60c2812f2e5c</guid>
            <pubDate>Tue, 20 Jun 2023 14:53:28 GMT</pubDate>
            <description><![CDATA[Что такое модульные системы в JavaScript? Почему их так много? Какие проблемы из-за этого могут возникнуть и как их решить? А что выбрать в 2023 году? Я расскажу в этой статье.]]></description>
            <content:encoded><![CDATA[<p>Всем привет! 👋</p>
<p>Если вы когда-нибудь имели удовольствие разрабатывать что-либо на JavaScript, думаю хотя бы раз в жизни вы сталкивались с чем-то типа:</p>
<pre><code>SyntaxError: Cannot use import statement outside a module</code></pre>
<p>(если нет, то все еще впереди! 😉)</p>
<p>В особенности сложно понимать что происходит, когда исходный код в процессе сборки подвергается транспиляции и одна модульная система превращается в другую.</p>
<p>Итак, сегодня я постараюсь рассказать про модульные системы JavaScript/TypeScript, как так получилось что их несколько и что лично я бы рекомендовал использовать в 2023 году.</p>
<h3>Немного теории</h3>
<p>Принцип модульности является средством упрощения задачи проектирования программного обеспечения (ПО) и распределения процесса разработки между группами разработчиков.</p>
<p>Именно необходимость разработки больших программных систем привела к появлению&nbsp;<a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">модульного программирования</a>,&nbsp;когда вся программа разбивается на составные части, называемые модулями, причем каждый из них имеет свой контролируемый размер, четкое назначение и детально проработанный интерфейс (aka API)</p>
<p><b>Модуль</b>&nbsp;— это последовательность логически связанных фрагментов, оформленных как отдельная часть программы. Во многих языках оформляется в виде отдельного&nbsp;файла с исходным кодом&nbsp;или поименованной непрерывной её части.</p>
<p>Конечно, принцип модульности не обошел стороной и JavaScript. Напротив, именно в JavaScript были реализованы сразу несколько подходов к разделению кода, но обо всем по порядку.</p>
<h3>IIFE</h3>
<p>Начнем, пожалуй, с <a href="https://developer.mozilla.org/ru/docs/Glossary/IIFE">IIFE</a>, хоть это и не модульная система, а нативный механизм языка, я считаю, что для полноты картины нам нужно его рассмотреть. IIFE представляет собой функцию, которая объявляется и сразу же вызывается. Эта техника используется ради изоляции - переменные, объявленные в теле такой функции недоступные извне, что помогает избежать конфликтов имен и загрязнения глобального пространства имен. Пример такой функции можно увидеть ниже:</p>
<pre><code>;(function () {
  const a = 5;

  console.log(a) // 5
})()

console.log(a) // Uncaught ReferenceError: a is not defined</code></pre>
<p>Если вы предпочитаете использовать JS без <code><code class="inline-code">;</code></code>, то перед объявлением <b>IIFE</b> я рекомендую ставить <code><code class="inline-code">;</code></code> как показано выше, чтобы выражение не интерпретировалось как вызов функции. Если же вы за <code><code class="inline-code">;</code></code> повсюду, то таких проблем у вас не возникнет.</p>
<p>Кстати, <b>IIFE</b> может быть асинхронной, а это значит, что в теле такой функции допустимо использовать ключевое слово <code class="inline-code">await</code>, но это не совсем по теме этой статьи.</p>
<p>Несмотря на то, что <b>IIFE</b> дает удобный (и нативный) способ изоляции кода, он не дает удобного механизма разделения кода на файлы. Конечно, вы можете использовать сколько угодно тегов <code class="inline-code">script</code> на странице, однако на больших проектах очень сложно отслеживать зависимости модулей друг от друга. И хотя, с точки зрения определения модуля в программировании, которое я привел выше, модуль не обязательно должен представлять собой отдельный файл, практика показывает, что работать так значительно проще, поэтому со временем появились более мощные инструменты и подходы.</p>
<h3>AMD</h3>
<p><b>AMD</b> (Asynchronous Module Definition) - это модульная система, предназначенная для использования в браузерной среде. <b>AMD</b> предоставляет механизм обнаружения и разрешения зависимостей модулей, что позволяет автоматически загружать и выполнять модули в правильном порядке.</p>
<p><b>AMD </b>разрабатывалась группой разработчиков, которые были недовольны направлением, выбранным <b>CommonJS</b>. Фактически, <b>AMD</b> отделилась от <b>CommonJS</b> на раннем этапе своего развития.</p>
<p>Одной из главных особенностей <b>AMD</b> является возможность асинхронной загрузки модулей. Когда модуль запрашивается для загрузки, <b>AMD</b> загрузчик асинхронно загружает зависимости модуля, обеспечивая правильный порядок загрузки.</p>
<p>Ниже приведен пример как это можно использовать:</p>
<pre><code>define(&#039;sounds&#039;, 
  [&#039;dog&#039;, &#039;audio&#039;], 
  function (dog, audio) {
    return {
      bark: function() {
        return audio.play(dog.getVoice());
      }
    }
  };
});</code></pre>
<p>Здесь мы реализуем модуль <code class="inline-code">sounds</code> и явно говорим о его зависимостях. Одним из самых популярных инструментов, реализующих <b>AMD</b> является&nbsp;<a href="https://requirejs.org/"><b>RequireJS</b></a>. Приведу пример того, как он может использоваться:</p>
<pre><code>requirejs(
  [&#039;helper/util&#039;],
  function(util) {
    // This function is called when scripts/helper/util.js is loaded.
    // If util.js calls define(), then this function is not fired until
    // util&#039;s dependencies have loaded, and the util argument will hold
    // the module value for &quot;helper/util&quot;.
  }
);</code></pre>
<h3>CommonJS (CJS)</h3>
<p>В то время как в браузерных окружениях использовалась <b>AMD</b>, в node-окружениях использовалась еще одна модульная система - <b>CommonJS</b>.&nbsp;Основное различие между <b>AMD</b> и <b>CommonJS</b> заключается в поддержке асинхронной загрузки модулей.</p>
<p>Если <b>AMD</b> в наше время встретить довольно трудно, то <b>CommonJS</b> буквально повсюду. Тут важно заметить, что <b>CommonJS</b> все еще является модульной системой Node.js по-умолчанию.</p>
<p>Ниже приведен пример кода с использованием <b>CommonJS</b>:</p>
<pre><code>const fs = require(&#039;fs&#039;);
const dog = require(&#039;./dog&#039;);

module.exports = {
  barkToFile: function () {
    fs.writeFileSync(&#039;./bark.txt&#039;, dog.voiceToString());
  }
};</code></pre>
<p>Для импорта модулей в <b>CommonJS</b> используется ключевое слово <code><code class="inline-code">require</code></code>. Это функция и она принимает путь (на самом деле не совсем путь, что достижимо благодаря механизму резрешения модулей) к модулю и возвращает экспортируемые значения этого модуля.</p>
<p>Любые свойства, добавленные в <code><code class="inline-code">exports</code></code> или присвоенные <code><code class="inline-code">module.exports</code></code>, становятся экспортируемыми значениями модуля.</p>
<p>В <b>CommonJS</b> модули загружаются синхронно и разрешаются во время выполнения. Когда модуль первоначально загружается, его код выполняется, и его экспортированные значения становятся доступными для импорта в других модулях.</p>
<p>В браузере нет встроенной функции <code class="inline-code">require</code> и доступа к файловой системе, поэтому <b>CommonJS</b> в браузере не поддерживается, однако можно получить схожее API, используя подход <b>AMD</b> и библиотеку <b>RequireJS</b>:</p>
<pre><code>define(
  function(require, exports) {
    const dog = require(&quot;dog&quot;);

    exports.barkToConsole = function() {
      console.log(dog.voiceToString());
    }
  }
);</code></pre>
<h3>ES Модули (ESM)</h3>
<p>И вот мы, наконец, добрались до первой модульной системы, которая описана в стандарте ECMAScript. <b>ES модули</b> появились вместе с ES6, в 2015 году. И да, вы все правильно поняли, на протяжении 20 лет в JavaScript не было стандартизованной модульной системы. Я думаю этот синтаксис также Вам знаком:</p>
<pre><code>import api from &#039;api.js&#039;;
import dog from &#039;./dog.js&#039;;

export function makeBarkRequest () {
  api.post(&#039;/bark&#039;, (err) =&gt; {
    if (err) return;

    dog.bark();
  });
}</code></pre>
<p>Несмотря на стандартность, <b>ES модули</b> выключены по-умолчанию. В Node окружении вы должны использовать <code><code class="inline-code">type: module</code></code> в вашем package.json, либо <code><code class="inline-code">.mjs</code></code> расширения файлов. В браузерных окруженях для использования синтаксиса <b>ESM</b> внутри тега <code class="inline-code">script</code> необходимо установить атрибут <code class="inline-code">type</code> в значение <code><code class="inline-code">module</code></code>. Поддержка <b>ES модулей</b> впервые появилась в Node 12 под флагом <code><code class="inline-code">--experimental-modules</code></code>. В более старших версиях <b>ES модули</b> вышли из под флага.</p>
<p>Модульные системы на этом закончились, однако есть еще пара подходов, которые важно рассмотреть.</p>
<h3>UMD</h3>
<p><b>UMD</b> (Universal Module Definition) - это шаблон или подход к созданию модулей, который позволяет модулям работать как в среде <b>CommonJS</b>, так и в среде <b>AMD</b>, а также сделать их доступными как глобальные переменные, если загрузчик модулей отсутствует.</p>
<p>Основная идея <b>UMD</b> заключается в создании модуля, который может автоматически адаптироваться к различным средам выполнения и загрузчикам модулей.</p>
<p><b>UMD</b> использует условные конструкции, чтобы определить, какой из существующих модульных систем доступен, и в зависимости от этого выбирает соответствующий способ экспорта и импорта модуля.</p>
<p>Выглядит это примерно так:</p>
<pre><code>(function (root, factory) {
  if (typeof define === &#039;function&#039; &amp;&amp; define.amd) {
    // AMD env
    define([&#039;dependency&#039;], factory);
  } else if (typeof exports === &#039;object&#039;) {
    // CommonJS env
    module.exports = factory(require(&#039;dependency&#039;));
  } else {
    // global
    root.ModuleName = factory(root.Dependency);
  }
}(this, function (dependency) {
  // module logic
  return {
    // exports
  };
}));</code></pre>
<p>Если среда выполнения поддерживает <b>AMD</b> (проверка <code><code class="inline-code">define</code></code>), модуль определяется с использованием <code><code class="inline-code">define</code></code> и указываются зависимости для загрузчика модулей.</p>
<p>Если среда выполнения поддерживает <b>CommonJS</b> (проверка <code><code class="inline-code">exports</code></code>), модуль экспортируется с помощью <code><code class="inline-code">module.exports</code></code>, и зависимости разрешаются через <code><code class="inline-code">require</code></code>.</p>
<p>Если ни одна из проверок не прошла, модуль "экспортируется" путем присвоения его свойства глобальному объекту (<code><code class="inline-code">root</code></code>), который в данном случае представляет глобальное пространство имен.</p>
<h3>SystemJS</h3>
<p><b>SystemJS</b> - это универсальный загрузчик модулей JavaScript, предназначенный для использования в браузере и среде выполнения Node.js. Он разработан с учетом поддержки и загрузки различных модульных форматов, таких как <b>AMD</b>, <b>CommonJS</b>, <b>UMD</b> и <b>ES модули</b>, позволяя разработчикам использовать разные форматы модулей в одном проекте.</p>
<p>Выглядит это примерно так:</p>
<pre><code>System.import(&#039;dog.js&#039;).then(function(module) {
  console.log(module.bark());
}).catch(function(error) {
  console.error(&#039;Failed to load module:&#039;, error);
});</code></pre>
<p>Причем на месте <code><code class="inline-code">dog.js</code></code> может быть модуль, описанный в ЛЮБОМ формате из вышеперечисленных.</p>
<p>Но я бы сказал, что если у вас возникла потребность в использовании разных модульных систем в одном проекте, то скорее всего что-то не так с вашим проектом.</p>
<p>На самом деле, скорее всего вы не будете иметь дело ни с <b>AMD</b>, ни с <b>UMD</b>, ни с <b>SystemJS</b>. <b>ESM</b> и <b>CJS</b> же очень распространены и вы будете видеть их на самом деле вне зависимости от того, в каком окружении разрабатываете.</p>
<h3>Бандлеры и транспиляторы</h3>
<p>Материал, который мы сейчас рассмотрели не является сложным сам по себе. Думаю, не составит труда опредеделить используемый подход при просмотре исходного кода. Однако, в реальной разработке код который мы пишем не попадает в продакшен в неизменном виде. На бэкенде обычно достаточно транспилятора: самым распространенным сценарием кажется транспиляция TS кода в JS (конечно, есть решения, например <a href="https://www.npmjs.com/package/ts-node"><b>ts-node</b></a> или <a href="https://deno.com/"><b>deno</b></a>, но это тема для другой статьи). На фронтенде все еще сложнее - код проходит не только через транспилятор, но и через различные инструменты, называемые бандлерами, минификаторами и тд.</p>
<p>У транспиляторов (например, <a href="https://www.npmjs.com/package/typescript"><b>tsc</b></a>) есть возможность генерировать код с использованием всех модульных систем, которые мы рассмотрели:&nbsp;<b>ESM</b>,&nbsp;<b>CJS</b>,&nbsp;<b>AMD</b>,&nbsp;<b>UMD</b>&nbsp;и&nbsp;<b>SystemJS</b>. А <a href="https://www.typescriptlang.org/play">здесь</a> Вы можете поиграться с этим самостоятельно. Например, вот такой код:</p>
<pre><code>import dog from &#039;./dog&#039;;
import audio from &#039;./audio&#039;;

export const bark = () =&gt; {
  audio.play(dog.bark())
}</code></pre>
<p>Не изменится, если собрать его с помощью <b>tsc</b> с настройкой <code class="inline-code">module</code>, установленной в <code class="inline-code">'esnext'</code>, однако если собрать код тем же инструментом, но с настройкой <code class="inline-code">module</code>&nbsp;, установленной в <code class="inline-code">'commonjs'</code>, то код примет такой вид:</p>
<pre><code>&quot;use strict&quot;;
var __importDefault = (this &amp;&amp; this.__importDefault) || function (mod) {
    return (mod &amp;&amp; mod.__esModule) ? mod : { &quot;default&quot;: mod };
};
Object.defineProperty(exports, &quot;__esModule&quot;, { value: true });
exports.bark = void 0;
const dog_1 = __importDefault(require(&quot;./dog&quot;));
const audio_1 = __importDefault(require(&quot;./audio&quot;));
const bark = () =&gt; {
    audio_1.default.play(dog_1.default.bark());
};
exports.bark = bark;</code></pre>
<p>Если разобраться что здесь происходит и откинуть сгенерированные хелперы, то можно заметить, что код, который мы писали с использованием <b>ESM</b> превратился в код c использованием <b>CJS</b> и это и есть корень всех ошибок!</p>
<p>Отсюда появляется путаница. Так как в исходниках вы используете <b>ESM</b>, кажется логичным установить <code class="inline-code">type</code>&nbsp;равный <code class="inline-code">'module'</code> настройку в package.json, но не тут то было. Реально Node.js будет запускать уже собранный <b>tsc</b> код, который, как мы убедились,&nbsp;<b>ESM</b> не использует и вы получите ошибку:</p>
<pre><code>ReferenceError: require is not defined</code></pre>
<p>Возможна и обратная ситуация: например, например, мы собрали код без изменений, и в нем остался синтаксис <b>ESM</b>. Но настройку <code class="inline-code">type</code>&nbsp;равную <code class="inline-code">'module'</code> мы не установили, тогда мы получим ошибку из начала статьи:</p>
<pre><code>SyntaxError: Cannot use import statement outside a module</code></pre>
<p>Все это осложняется тем, что некоторые пакеты прекращают поддержку <b>CJS</b>. Одним из таких пакетов является <a href="https://www.npmjs.com/package/chalk"><b>chalk</b></a>. С 5 мажорной версии они перестали поддерживать <b>CJS</b> и при попытке сделать <code class="inline-code">require</code> вы получите ошибку:</p>
<pre><code>Error [ERR_REQUIRE_ESM]: require() of ES Module</code></pre>
<p>А теперь представьте ситуацию, что у вас есть TS код:</p>
<pre><code>import chalk from &#039;chalk&#039;; // &gt;= 5
import dog from &#039;./dog&#039;;

export const colorfulBark = () =&gt; {
  console.log(chalk.green(dog.barkToString()));
}</code></pre>
<p>И вы решили собрать его с помощью <b>tsc</b>&nbsp;с <code class="inline-code">module</code>&nbsp;равной <code class="inline-code">'commonjs'</code>. Вы получите вот такой код:</p>
<pre><code>&quot;use strict&quot;;
var __importDefault = (this &amp;&amp; this.__importDefault) || function (mod) {
    return (mod &amp;&amp; mod.__esModule) ? mod : { &quot;default&quot;: mod };
};
Object.defineProperty(exports, &quot;__esModule&quot;, { value: true });
exports.colorfulBark = void 0;
const chalk_1 = __importDefault(require(&quot;chalk&quot;)); // &lt;-- require(&#039;chalk&#039;)
const dog_1 = __importDefault(require(&quot;./dog&quot;));
const colorfulBark = () =&gt; {
    console.log(chalk_1.default.green(dog_1.default.barkToString()));
};
exports.colorfulBark = colorfulBark;</code></pre>
<p>А при попытке запустить его - ошибку:</p>
<pre><code>Error [ERR_REQUIRE_ESM]: require() of ES Module</code></pre>
<p>которая говорит о том, что какой то из наших модулей (в данном случае <b>chalk</b>) не поддерживает <b>CJS</b>. И это очень запутанно, так как в исходном коде у нас нет никаких <code class="inline-code">require</code>! А в уже собранном коде бывает довольно сложно разобраться, если проект достаточно большой.</p>
<p>В данном случае помогут два варианта:</p>
<ol><li>Установть type в значение 'module' в package.json, что также потребует установки&nbsp;module в значение 'esnext' в tsconfig.json (возможно, сломается что-то другое 😁).</li><li>Даунгрейднуть chalk до версии 4.1.2, в которой CJS еще поддерживался. То есть проблема различия модульных систем заставляет Вас использовать не самые новые версии пакетов, что, конечно же, нехорошо.</li></ol>
<p>Вообще, хочу дополнительно отметить, что <code class="inline-code">type</code>, установленный в значение <code class="inline-code">'module'</code> это очень серьезная project-wide настройка, которая влияет буквально на все, поэтому поменять ее, обычно, крайне трудно. Например, если вы используете <a href="https://jestjs.io/ru/">jest</a> с <code class="inline-code">type</code> равным <code class="inline-code">'module'</code>, а потом резко по какой-то причине решили перейти на <code class="inline-code">type</code> равный&nbsp; <code class="inline-code">'commonjs'</code> у вас перестанут работать top-level <code class="inline-code">await</code> в тестах (которые, например могли бы использоваться для моков отдельных модулей).</p>
<h3>И что же делать?</h3>
<p>Я не могу давать рекомендации относительно уже существующих проектов, потому что, как я уже сказал, изменение настройки <code class="inline-code">type</code>, обычно, даётся крайне трудно и в каждом отдельном случае могут возникнуть разные проблемы. Но, конечно, я бы рекомендовал перейти на <code class="inline-code">type</code> равный <code class="inline-code">'module'</code>, если есть такая возможность.</p>
<p>Новые же проекты я бы рекомендовал начинать с&nbsp;<code class="inline-code">type</code>&nbsp;равным&nbsp;<code class="inline-code">'module'</code>. Все больше публичных пакетов отказываются от поддержки <b>CJS</b>. К тому же, <b>ESM</b> дает дополнительные возможности, например top-level <code class="inline-code">await</code>.</p>
<p>Конечно, есть нюанс. При использовании <b>ESM </b>расширения файлов необходимы при импортах модулей (<code class="inline-code">'./startup/index.js'</code>). Также не поддерживается автоматическое разрешение импорта папки в <code class="inline-code">index.js</code> файл (directory index), то есть путь нужно указывать полностью.</p>
<p>Это проблема, если вы используете <b>tsc</b>, так как TypeScript не модифицирует пути импортов при транспиляции. Эту проблему можно решить специальным тулингом или просто использовать в импортах <code class="inline-code">.js</code> расширение. TypeScript сможет понять о каком файле (<code class="inline-code">.ts</code>) идет речь, а после транспиляции в коде будет валидный путь. Кстати моя IDE (vscode) даже смогла правильно подсказать путь с <code class="inline-code">.js</code> расширением.</p>
<p>Как альтернативу для TypeScript проектов можно рассмотреть <code class="inline-code">ts-node</code>, но запускать его необходимо с флагом <code><code class="inline-code">--transpile-only</code></code>. Конечно, нужно помнить, что это другой, отличный от стандартной <b>node</b> рантайм и у него есть свои не менее интересные особенности.</p>
<p>Еще одним решением этой проблемы могут быть bare specifiers в сочетании с настройкой&nbsp;<code><code class="inline-code">moduleResolution</code> равной <code class="inline-code">'nodenext'</code></code>, но тогда нужно еще решить проблему копирования package.json файлов в папку с билдом (причем структуру необходимо сохранить).</p>
<p>Но все же для новых JavaScript проектов я бы рекомендовал использовать <code class="inline-code">type</code> равный <code class="inline-code">'module'</code> и только его. В случае с TypeScript не все так просто, однако я бы тоже рекомендовал использовать&nbsp;<code class="inline-code">type</code>&nbsp;равный&nbsp;<code class="inline-code">'module'</code>&nbsp;и указывать <code class="inline-code">.js</code> расширения. Да, это немного странно, но это лучшее решение из тех, что есть, как по мне.</p>
<p>Выше мы рассмотрели вопросы использования модульных систем в node-окружениях. А как же быть в браузерных? Обычно, в браузерных окружениях таких проблем не возникает, так как используются различные инструменты типа <a href="https://webpack.js.org/">webpack</a>, который сам по себе поддерживает и <b>ESM</b> синтаксис, и <b>CJS</b>. На выходе обычно мы имеем бандл, в котором весь код просто сконкатенирован в один и никаких модульных систем (а значит и проблем с ними там нет). <b>Webpack</b> также предоставляет решение для разбиений приложения на файлы (чанки) с целью асинхронной загрузки по требованию, что тоже есть модульная система, но это тема для отдельной статьи.</p>
<p>Если говорить о более новых инструментах, например <a href="https://vitejs.dev/"><b>Vite</b></a>, то он построен на <b>ESM</b> и использует в браузере <code class="inline-code">script</code> с атрибутом&nbsp;<code class="inline-code">type</code>&nbsp;равном&nbsp;<code class="inline-code">'module'</code>. Быстродействие <b>Vite</b> построено на <b>ESM</b>. Насколько мне известно, альтернативы этому нет, поэтому выбирать не приходится.</p>
<h3>Заключение</h3>
<p>Пока я писал эту статью я нашел в интернете множество других статей на тему сравнения модульных систем в JavaScript. Однако, все эти статьи лишь поверхностно рассматривают различия и сходства этих систем. Я же постарался рассмотреть не только модульные системы, но и проблемы с которыми обычно сталкиваются разработчики и даже предложил несколько решений. Надеюсь, вам это будет полезно в ваших проектах.</p>
<p>Спасибо за внимание и до встречи!</p>
<h3>Ссылки</h3>
<ol><li>Modular programming, просмотрено 19 июня 2023, https://en.wikipedia.org/wiki/Modular_programming</li><li>IIFE, просмотрено 19 июня 2023, https://developer.mozilla.org/en-US/docs/Glossary/IIFE</li><li>RequireJS, просмотрено 19 июня 2023, https://requirejs.org/</li><li>ESM, просмотрено 19 июня 2023, https://nodejs.org/docs/latest-v18.x/api/esm.html</li><li>UMD, просмотрено 19 июня 2023, https://github.com/umdjs/umd</li><li>AMD, просмотрено 19 июня 2023, https://en.wikipedia.org/wiki/Asynchronous_module_definition</li></ol>]]></content:encoded>
            <enclosure url="https://static.vladivanov.me/uploads/2f0f3d0f-828b-4898-8ea3-788d6189510a.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Как поиграть в QR код]]></title>
            <link>https://vladivanov.me/how-to-play-qr-code/</link>
            <guid>70932b84-34cf-4b74-b091-4071a6ca34c6</guid>
            <pubDate>Fri, 13 Jan 2023 00:55:51 GMT</pubDate>
            <description><![CDATA[Можно ли сделать веб-игру и поместить в нее QR-код?]]></description>
            <content:encoded><![CDATA[<p>Привет!</p>
<p>QR коды - очень крутая штука. Но что если я скажу вам, что в них можно еще и поиграть. Интересно? Тогда поехали!</p>
<p>Итак, сегодня я расскажу о том как я написал игру flappy bird и поместил ее в QR код. Задача не такая простая как кажется на первый взгляд. В качестве челленджа мы хотим, чтобы вся игра содержалась внутри QR кода и для того чтобы поиграть нам был нужен только QR код, то есть нам не подходит вариант с ссылкой на игру внутри QR кода. Это неинтересно!</p>
<p>Чаще всего, когда вы сканируете QR код данные из декодера попадают в поисковую строку браузера. Конечно, есть и другие сценарии использования, однако я пока предлагаю остановиться на этом. Значит наша задача заключается в том, чтобы передать исходный код нашей игры с через строку браузера. Но можем ли мы так сделать? Конечно, можем и помогут нам в этом <a href="https://developer.mozilla.org/ru/docs/Web/HTTP/Basics_of_HTTP/Data_URLs">Data&nbsp;URLs</a>. Мы можем проверить работоспособность данного метода просто взяв любой валидный HTML код, закодировав его в base64 и добавив вначале префикс <code><code class="inline-code">data:text/html;base64,</code></code>. Далее нам нужно просто получившуюся строку закодировать в QR код. Звучит довольно просто, но здесь есть одно не очень приятное обстоятельство: мы не можем закодировать в QR код бесконечное количество данных.</p>
<h3>Ограничения</h3>
<p>По спецификации QR коды делятся на версии. Номера версий варьируются от 1 до 40. Каждая версия имеет особенности в конфигурации и количестве точек(модулей) составляющих QR-код. Версия 1 содержит 21×21 модулей, версия 40 — 177×177. От версии к версии размер кода увеличивается на 4 модуля на сторону.</p>
<img src="https://static.vladivanov.me/uploads/12c8126e-35d8-4c5a-9b45-1a133a6a6266.webp" alt="Версии QR кодов" />
<p>Каждой версии соответствует определенная емкость с учетом уровня коррекции ошибок. Чем больше информации необходимо закодировать и чем больший уровень избыточности используется, тем большая версия кода нам потребуется.</p>
<p>QR код поддерживает несколько режимов кодирования:</p>
<table><tr><td>Mode</td><td>Characters</td><td>Compression</td></tr><tr><td>Числовой</td><td>0, 1, 2, 3, 4, 5, 6, 7, 8, 9</td><td>3 символа представлены 10 битами</td></tr><tr><td>Буквенно-цифровой</td><td>0–9, A–Z (только верхний регистр), пробел, $, %, *, +, -, ., /, :</td><td>2 символа представлены 11 битами</td></tr><tr><td>Байт</td><td>Символы из набора ISO/IEC 8859-1</td><td>Каждый символ представлен 8 битами</td></tr><tr><td>Кандзи</td><td>Символы из системы Shift JIS, основанной на JIS X 0208</td><td>2 кандзи символа представлены 13 битами</td></tr></table>
<p>Также, QR-коды имеют специальный механизм увеличения надежности хранения зашифрованной информации. Для кодов созданных с самым высоким уровнем надежности могут быть испорчены или затерты до 30% поверхности, но они сохранят информацию и будут корректно прочитаны. Для исправления ошибок используется алгоритм&nbsp;<a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%B4_%D0%A0%D0%B8%D0%B4%D0%B0_%E2%80%94_%D0%A1%D0%BE%D0%BB%D0%BE%D0%BC%D0%BE%D0%BD%D0%B0">Рида-Соломона</a>.</p>
<p>В таблице ниже показано максимальное количество сохраняемых символов в каждом режиме кодирования и для каждого уровня исправления ошибок:</p>
<table><tr><td>Mode</td><td>L (~7%)</td><td>M (~15%)</td><td>Q (~25%)</td><td>H (~30%)</td></tr><tr><td>Числовой</td><td>7089</td><td>5596</td><td>3993</td><td>3057</td></tr><tr><td>Буквенно-цифровой</td><td>4296</td><td>3391</td><td>2420</td><td>1852</td></tr><tr><td>Байт</td><td>2953</td><td>2331</td><td>1663</td><td>1273</td></tr><tr><td>Кандзи</td><td>1817</td><td>1435</td><td>1024</td><td>784</td></tr></table>
<p>Алфавит base64 содержит 64 символа и разделен на группы:</p>
<ul><li>Заглавные буквы (индексы 0-25):&nbsp;A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z</li><li>Строчные буквы (индексы 26-51):&nbsp;a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z</li><li>Цифры (индексы 52-61):&nbsp;0, 1, 2, 3, 4, 5, 6, 7, 8, 9</li><li>Специальные символы (индексы 62-63):&nbsp;+, /</li></ul>
<p>In addition to these characters, the equal sign (<code>=</code>) is used for padding.</p>
<p>Помимо этих символов, используется знак равенства (=) в качестве заполнителя.</p>
<p>Самый оптимальный режим кодирования для моей задачи - это<b>&nbsp;</b>побайтовый. Что касается уровня исправления ошибок я думаю <b>M</b> уровня будет достаточно, поэтому мы можем рассчитывать на то, что сможем закодировать 2331 символов, это чуть больше 2 килобайт. Однако, следует помнить, что закодированная в base64 строка длиннее исходной примерно на 30%.</p>
<h3>Инструменты</h3>
<p>Для удобства разработки необходимо оптимизировать процесс генерации QR кода. Также, необходимо произвести некоторую предобработку исходного HTML кода, а именно <a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B8%D0%BD%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)">минификацию</a>, так как у нас есть строгое ограничение по размеру.</p>
<p>Для этих целей был написан специальный скрипт на NodeJS:</p>
<pre><code>import { minify } from &#039;html-minifier&#039;;
import { readFileSync, writeFileSync } from &#039;fs&#039;;
import { resolve, join } from &#039;path&#039;;
import { toFile } from &#039;qrcode&#039;;
import { execSync } from &#039;child_process&#039;;
import { fileURLToPath } from &#039;url&#039;;

const kb = (bytes) =&gt; (bytes / 1024).toFixed(2);

const __dirname = fileURLToPath(new URL(&#039;.&#039;, import.meta.url));

const input = readFileSync(resolve(__dirname, &#039;../index.html&#039;), { encoding: &#039;utf-8&#039; });

console.log(&#039;Original size is&#039;, kb(input.length), &#039;KB&#039;, `(${input.length} bytes)`);

const minified = minify(input, {
  collapseWhitespace: true,
  minifyCSS: true,
  minifyJS: {
    mangle: {
      // переменные на верхнем уровне 
      // по-умолчанию не минифицируются
      toplevel: true
    }
  }
});

console.log(&#039;Minified size is&#039;, kb(minified.length), &#039;KB&#039;, `(${minified.length} bytes)`);

const outDir = resolve(__dirname, &#039;../output&#039;);

execSync(`mkdir -p ${outDir}`);
writeFileSync(join(outDir, &#039;index.min.html&#039;), minified);

const b64 = Buffer.from(minified).toString(&#039;base64&#039;);
const output = `data:text/html;base64,${b64}`;

console.log(&#039;b64 size is&#039;, kb(output.length), &#039;KB&#039;, `(${output.length} bytes)`);

writeFileSync(join(outDir, &#039;b64.txt&#039;), output);
toFile(
  join(outDir, &#039;image.png&#039;), 
  [{ data: output, mode: &#039;byte&#039; }], // режим кодирования
  { errorCorrectionLevel: &#039;m&#039; } // уровнь исправления ошибок
);

console.log(&#039;Done!&#039;);</code></pre>
<p>Таким образом, после запуска скрипта мы имеем на выходе готовый QR код, а также детализацию по размеру кода, что поможет нам в дальнейшей оптимизации.</p>
<h3>Разработка</h3>
<p>Для начала создадим минимальный валидный HTML документ:</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;title&gt;Flappy bird&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>Я хотел бы чтобы в игре были как можно более адекватные текстуры, однако мы договорились не загружать ничего дополнительно, поэтому придется программировать рисование текстур. Можно было бы развлекаться с <code class="inline-code">div</code>, <code class="inline-code">box-shadow</code>, различными градиентами или SVG, но все эти подходы достаточно многословны и особо нет эффективных способов минифицировать такой код. Поэтому я решил выбрать <code class="inline-code">canvas</code>:</p>
<pre><code>...
  &lt;body&gt;
    &lt;canvas id=&quot;c&quot; width=&quot;360&quot; height=&quot;640&quot;&gt;&lt;/canvas&gt;
  &lt;/body&gt;
...</code></pre>
<p>Я создал DOM элемент и установил атрибут <code class="inline-code">id</code>. </p>
<p>Теперь в коде я могу просто обратиться к нему как <code class="inline-code">window.c</code> или просто по имени <code class="inline-code">c</code> и нет никакой необходимости в использовании методов поиска DOM элементов в документе по типу <code class="inline-code">querySelector</code> или <code class="inline-code">getElementById</code>, имена которых на самом деле достаточно длинные и не поддаются сокращению. Кстати, хотел бы обратить внимание на этот момент. Мы будем использовать <code class="inline-code">canvas</code>, то есть для рисования мы будем использовать различные методы <code class="inline-code">CanvasRenderingContext2D</code>, и есть смысл присваивать эти методы в переменные, потому что минифицировать свойства объекта мы не можем, однако для присваивания нам необходимо вызвать <code class="inline-code">bind</code> , чтобы передать <code class="inline-code">this</code> контекст, коим в данном случае является контекст рендеринга <code class="inline-code">canvas</code>. Вот о чем я говорю:</p>
<pre><code>let ctx = c.getContext(&#039;2d&#039;);

ctx.fillRect(...)
ctx.fillRect(...)
ctx.fillRect(...)</code></pre>
<p>После минификации даст что-то типа такого (проблельные символы оставлены для понятности):</p>
<pre><code>let a = c.getContext(&#039;2d&#039;);

a.fillRect(...)
a.fillRect(...)
a.fillRect(...)</code></pre>
<p>В то время как такой кусок кода:</p>
<pre><code>let ctx = c.getContext(&#039;2d&#039;);

let fillRect = ctx.fillRect.bind(ctx);

fillRect(...)
fillRect(...)
fillRect(...)</code></pre>
<p>Превратится во что-то типа такого:</p>
<pre><code>let a = c.getContext(&#039;2d&#039;), b = a.fillRect.bind(a);

b(...)
b(...)
b(...)</code></pre>
<p>То есть профит от такого присвоения есть, но не всегда. Для каждого отдельного случая нужно считать длину строк. Например, для метода <code class="inline-code">fillRect</code> такое присвоение имеет смысл только если этот метод вызывается в коде 2 или более раз.</p>
<p> Конечно, есть и другие способы присвоения функции, например:</p>
<pre><code>let fillRect = (...args) =&gt; ctx.fillRect(...args);</code></pre>
<p>Но такой вариант будет длиннее после минификации по сравнению с кодом, где используется <code><code class="inline-code">bind</code></code>. Поэтому я решил использовать код с <code><code class="inline-code">bind</code></code>, так как не смог найти вариант, который был бы короче.</p>
<p>Кстати, повсеместно в коде используется <code><code class="inline-code">let</code></code> , потому что он на 2 символа короче <code><code class="inline-code">const</code></code>&nbsp;🙂.</p>
<h4>Текстуры</h4>
<p>Я уже говорил о том, что хотел бы использовать как можно более похожие на оригинал текстуры для своей игры. Например, примерно такую птицу я бы хотел видеть в игре:</p>
<img src="https://static.vladivanov.me/uploads/987473bf-d968-410c-9ea8-0ceb89b5151c.webp" alt="Текстура птицы" />
<p>При этом у нас нет возможности загрузить картинки через интернет, поэтому я посчитал, что самым коротким способом будет хранить изображения в переменных типа string прямо в исходном коде. Но как уместить картинки в 1.6 килобайта с учетом того, что есть еще и код игры?</p>
<p>Можно заметить, что для того, чтобы нарисовать текстуру выше нам нужно всего 5 цветов. В то время как популярные форматы изображений такие как JPEG или PNG позволяют кодировать миллионы цветов, что достигается достаточно высокой разрядностью цвета. Например, цветные изображения JPEG хранят&nbsp;24 бита на пиксель, а PNG до 48 бит. Поэтому вариант с тем чтобы просто закодировать картинку в base64 сразу отпал.</p>
<p>Такая глубина цвета нам не нужна, а значит решить эту проблему можно с помощью разработки собственной палитры. Давайте представим, что каждому цвету мы назначили букву латинского алфавита, тогда изображение выше примет вид:</p>
<pre><code>uuuuuuaaaaaauuuuu
uuuuaacccabbauuuu
uuuaccccabbbbauuu
uuacccccabbbabauu
uaccccccabbbabauu
uaaaaacccabbbbauu
adddddacccaaaaaau
adddddaccaeeeeeea
uaaaaaccaeaaaaaau
uuaccccccaeeeeeau
uuuaacccccaaaaauu
uuuuuaaaaauuuuuuu

legend:
u - transparent (не знаю почему u, видимо потому что undefined)
a - #000
b - #fff
c - #f71
d - #eec
e - #f20</code></pre>
<p>Размер такой картинки - 204 байта. Однако, только посмотрев на эту картинку виден огромный потенциал для сжатия! Первое, что приходит на ум - это <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%B4%D0%BB%D0%B8%D0%BD_%D1%81%D0%B5%D1%80%D0%B8%D0%B9">RLE</a>. Если коротко, то суть алгоритма заключается в замене повторяющихся символов на символ + кол-во повторов: `aaaaa` -&gt; `a5`. В случае с последовательностью выше сжатие работает достаточно эффективно - 32.35 %. Минус такого подхода в том, что при неповторяющихся последовательностях алгоритм работает в другую сторону: `abcde` -&gt; `a1b1c1d1e1`, хотя это и поддается оптимизации: `abcdeeeee` -&gt; `-4abcd5e` или `abcdeee`&nbsp; -&gt;&nbsp; `abcd3e`</p>
<p>Давайте рассмотрим другой способ. Допустим цветов у нас будет не больше 16, тогда можно особо не напрягаясь достигнуть сжатия 50% для ЛЮБОГО набора данных. Каждому символу необходимо присвоить код. И в результирующую строку записывать не сам символ, а результат выражения:</p>
<pre><code>char((code(color1) &lt;&lt; 4) | code(color2))
// char - получения символа по его коду
// code - получение кода по символу</code></pre>
<p>Тогда в 8 битах мы будем хранить информацию о 2 пикселях. Но есть один нюанс. Коды символам нужно присваивать с 2 (<code class="inline-code">0b10</code>). Почему? В таблице ASCII символов до кода 32 идут служебные символы. Я не нашел способа их напечатать на Mac, а также не уверен относительно того можно ли использовать их в скрипте. Скорее всего, можно, но потребуется экранирование. В общем, для решения этой проблемы достаточно добавить смещение 2 и тогда в результате функции <code class="inline-code">code</code> будет всегда получаться число больше 32, а значит и печатаемый символ.</p>
<p>В результате с помощью первого алгоритма (RLE с оптимизацией единичных символов) я получил:</p>
<pre><code>u6a6u9a2c3ab2au7ac4ab4au5ac5ab3abau3ac6ab3abau3a5c3ab4au2ad5ac3a6uad5ac2ae6aua5c2aea6u3ac6ae5au4a2c5a5u7a5u7</code></pre>
<p>А в результате второго:</p>
<pre><code>ÌÌÌ333ÌÌÌÌÃ5U4CÌÌÌÃUU4DCÌÌÃUUSDCCÌÃUUU4D4&lt;Ì335U4DCÌ6ff5U333ÃffcU7wwsÃ33U7333ÌÃUUU7ww&lt;ÌÃ5UU33&lt;ÌÌÌ33&lt;ÌÌÌ</code></pre>
<p>И разница составила всего 6 байт. Конечно, результат второго алгоритма можно попробовать еще прогнать через RLE, но надо помнить о том, что раскодировать это все тоже придется кодом скрипта, который тоже занимает какое то место и я решил, что RLE меня вполне устраивает.</p>
<p>Кстати, можно было бы попробовать закодировать исходную строку <a href="https://neerc.ifmo.ru/wiki/index.php?title=%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A5%D0%B0%D1%84%D1%84%D0%BC%D0%B0%D0%BD%D0%B0#:~:text=Huffman's%20algorithm)%20%E2%80%94%20%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%BF%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B3%D0%BE,PKZIP%202%2C%20LZH%20%D0%B8%20%D0%B4%D1%80.">алгоритмом Хаффмана</a>, и даже заранее вычислить кодовое дерево, но в таком случае довольно трудно контроллировать непоявление служебных непечатаемых символов, а код реализации декодера достаточно громоздкий по сравнению с кодом декодера RLE или битовых последовательностей.</p>
<p>Таким образом, в игре появились текстуры:</p>
<pre><code>let birdTexture = `a6u8a2c3ab2au6ac4ab4au4ac5ab3abau2ac6ab3abau2ac7ab4au2ac8a6uac7ad6auac5ada6u2ac6ad5au3a2c5a5u6a5`;
let wing1Texture = `a4u2ae4auae5a2e5auae3au3a3`;
let wing2Texture = `a5uae5a2e5aua5u`;
let wing3Texture = `a5uae5a2e4auae3au3a4`;
let pipeTexture = &#039;g6h4g4h9h3g4h4g9g4f9f3&#039;;
let widePipeTexture = &#039;g3&#039; + pipeTexture + &#039;f3&#039;;</code></pre>
<p>После минификации этот код будет весить примерно 220 байт, что вполне приемлемо.</p>
<p>Но пока что это всего лишь обычные строки. Для их превращения в изображения тоже нужно проделать определенные действия:</p>
<pre><code>let draw = (t, x, y, w, i = 0, pw = 2, ph = 2) =&gt; {
  while (t.length) {
    let c = t[0], times = +t[1]; 
    // Повтороений не должно быть больше 9
    // То есть вместо записи a10 можно записать a5a5
    fillStyle(c);
    t = t.slice(times !== times ? 1 : 2);

    if (c === &#039;u&#039;) i += (times || 1);
    else for (let until = (times || 1) + i; i &lt; until; i++) fillRect(x + (i % w) * pw, y + ((i / w) | 0) * pw, pw, ph);
  }
}</code></pre>
<p>Итак, что же здесь происходит? Первым аргументом мы передаем текстуру - одну из тех строк, которые мы рассмотрели выше. Далее идут координаты <code class="inline-code">x</code> и <code class="inline-code">y</code> - это смещение рисования относительно левого верхнего угла canvas. Так как текстура у нас представлена строкой необходимо указать <code class="inline-code">w</code> (ширину), это позволит вовремя переносить рисование на “следующую строку”. Аргумент <code class="inline-code">i</code> тоже заведен с целью оптимизации - если текстура начинается с пустых пикселей (<code class="inline-code">'u'</code>), то их можно не указывать, а счетчик <code class="inline-code">i</code> сразу перенести на кол-во таких пикселей. Благодаря этому мы можем избавляться от&nbsp;<code class="inline-code">'u'</code> как мы избавляемся от незначащих нулей в числе. <code class="inline-code">pw</code>, <code class="inline-code">py</code> - ширина и высота пикселя соответсвтенно. Так как бОльшая часть трубы выглядит таким образом:</p>
<img src="https://static.vladivanov.me/uploads/db41b856-4e2e-4cb6-a05e-f924ecfe5461.webp" alt="Текстура трубы" />
<p>Мы можем вызвать <code class="inline-code">draw</code> всего один раз и указать <code class="inline-code">ph</code>, равный высоте трубы, вместо N вызовов <code class="inline-code">draw</code>, где N высота трубы в пикселях / <code class="inline-code">ph</code>. Сделано для быстродействия.</p>
<h4>Код</h4>
<p>Вся игра у нас (как и любая другая, насколько мне известно) будет крутиться в бесконечном цикле:</p>
<pre><code>let tick = 0;

let setup = () =&gt; {
  vSpeed = flyUpSpeed; // птица взлетает в самом начале
  score = playing = 0;
  y = 308; // начальное положение - центр экрана
  pipes = [[100, gate()], [788, gate()]]; 
  // инициализируем первые 2 трубы,
  // про трубы чуть дальше раскажу подробнее
}

let render = () =&gt; {
  ...

  tick++;
  requestAnimationFrame(render);
}

setup();
render();</code></pre>
<p>Первым делом, конечно, надо нарисовать фон. Я просто заливаю его цветом <code class="inline-code">#0ac</code> (цвета используем трехбуквенные). Я думал насчет генерации фона, но не придумал как уместить этот код в наш размер, поэтому просто залил. Далее идет рисование труб:</p>
<pre><code>pipes.map(pipe =&gt; {
  pipe[0] -= hSpeed; // двигаем трубы каждый рендер

  let [px, py] = pipe;

  draw(&#039;a2&#039; + pipeTexture + &#039;a2&#039;, px, 0, 64, 0, 1, height);
  fillStyle(&#039;a&#039;);
  fillRect(px - 3, py - 32, 69, 284);
  draw(widePipeTexture, px - 1, py - 30, 66, 0, 1, 28);
  draw(widePipeTexture, px - 1, py + 222, 66, 0, 1, 28);
  fillStyle(&#039;i&#039;);
  fillRect(px - 3, py, 69, 220);

  ...
})</code></pre>
<p>В памяти содержится информация только о двух трубах, потому что больше на экран просто не влезет. Игра спроектирована так, что при уходе трубы с индексом равным 0 за экран, она удаляется, а игрок получает очко:</p>
<pre><code>if (px &lt; -64) {
  score++;
  bestScore = score &gt; bestScore ? score : bestScore; // тернарный оператор короче, чем Math.max
  pipes = [...pipes.slice(1), [pipes[1][0] + 284, gate()]];
}</code></pre>
<p>Информация от трубах хранится в виде массива кортежей <code class="inline-code">[x,y]</code>, где <code class="inline-code">x</code> - расстояние от левого края <code class="inline-code">canvas</code> до начала трубы, <code class="inline-code">y</code> - расстояние от верхнего края экрана до конца верхней трубы (затем идет разрыв, куда должна пролететь птица, а потом начинается нижняя труба). y для каждой трубы генерируется в момент создания функцией <code class="inline-code">gate</code>:</p>
<pre><code>let gate = () =&gt; (Math.random() * 292 + 64) | 0;</code></pre>
<p>Расстояния рассчитаны таким образом, чтобы игрок не мог врезаться в трубу с индексом, отличным от 0, поэтому и проверка на столкновение проводится только для одной трубы:</p>
<pre><code>let [[px, py]] = pipes; // берем координаты только первой трубы

if ((px &lt; 198 &amp;&amp; px &gt; 98 &amp;&amp; (y &lt; py || py + 188 &lt; y)) || (y &lt; 0 || y &gt; 616)) {
  // game over
  setup();
}</code></pre>
<p>Кстати, как вы уже заметили, все константы заранее посчитаны, потому что так код занимает меньше места.</p>
<p>После труб рисуется птица. У нее всегда один и тот же <code class="inline-code">x</code>, а вот <code class="inline-code">y</code> постоянно изменяется:</p>
<pre><code>let gravityAcceleration = .5;
let flyUpSpeed = -12;

vSpeed += gravityAcceleration;
y += vSpeed;

...

draw(birdTexture, 164, y, 16, 5);

// над птицей рисуем анимированные крылья
let wingsPhase = ((tick / 4) | 0) % 4;

wingsPhase % 2 
  ? draw(wing2Texture, 162, y + 10, 7, 1) 
  : !wingsPhase
  ? draw(wing1Texture, 162, y + 6, 7, 1) 
  : draw(wing3Texture, 162, y + 12, 7, 1);

...

c.onclick = () =&gt; { 
  // взлет вверх при клике
  vSpeed = flyUpSpeed;
}</code></pre>
<p>Ну и в конце рисуем текст, чтобы сказать игроку во что он вообще играет, сколько очков набрал и показать экран проигрыша:</p>
<pre><code>fillStyle(&#039;b&#039;);
fillText(`Score: ${score} | Best: ${bestScore}`, 26);

if (!playing) {          
  fillText(played ? &#039;Game over&#039; : &#039;Flappy bird&#039;, 210, 32);
  fillText(&#039;Click to play&#039; + (played ? &#039; again&#039; : &#039;&#039;), 430);

  !played &amp;&amp; fillText(&#039;QR code edition&#039;, 230, 12, 246);
}</code></pre>
<p>Мы рассмотрели почти весь код, однако, кое что я все же опустил, но вы можете найти полный код проекта и даже поиграться самостоятельно на <a href="https://github.com/yungvldai/flappy-bird">странице проекта на Github</a>.</p>
<h3>Сборка</h3>
<p>После того как наш код дописан можно приступить к сборке. Благодаря тому, что мы позаботились об инструментах заранее нам нужно всего лишь запустить:</p>
<pre><code>npm i &amp;&amp; npm run build</code></pre>
<p>Скрипт сам создаст папку output и поместит туда результат, а именно:</p>
<ol><li>Минифицированный HTML;</li><li>Data URL;</li><li>PNG-картинку с QR кодом.</li></ol>
<p>А также, выведет детализацию по размерам:</p>
<pre><code>Original size is 3.82 KB (3907 bytes)
Minified size is 1.53 KB (1569 bytes)
b64 size is 2.06 KB (2114 bytes)
Done!</code></pre>
<p>А вы можете прямо сейчас отсканировать QR код:</p>
<img src="https://static.vladivanov.me/uploads/4fb1fc2d-58da-423a-87f1-326a41c4fa63.webp" alt="Результирующий QR код с игрой внутри" />
<p> или сразу скопировать data-url в свой бразуер:</p>
<pre><code>data:text/html;base64,PGh0bWw+PGhlYWQ+PHRpdGxlPkZsYXBweSBCaXJkPC90aXRsZT48bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSxtYXhpbXVtLXNjYWxlPTEsdXNlci1zY2FsYWJsZT0wIj48L2hlYWQ+PGJvZHkgc3R5bGU9ImJhY2tncm91bmQ6IzAwMCI+PGNhbnZhcyBzdHlsZT0iaGVpZ2h0Ojk2JTtwb3NpdGlvbjpmaXhlZDt0b3A6NTAlO2xlZnQ6NTAlO3RyYW5zZm9ybTp0cmFuc2xhdGUzZCgtNTAlLC01MCUsMCkiIGlkPSJjIiB3aWR0aD0iMzYwIiBoZWlnaHQ9IjY0MCI+PC9jYW52YXM+PHNjcmlwdD5sZXQgaT1jLmdldENvbnRleHQoIjJkIiksdT0oaS50ZXh0QWxpZ249ImNlbnRlciIsYy5oZWlnaHQpLGU9e2E6IjAwMCIsYjoiZmZmIixjOiJmNzEiLGQ6ImYyMCIsZToiZWVjIixmOiIxNzAiLGc6IjFhMCIsaDoiMWQwIixpOiIwYWMifSxnPWkuZmlsbFJlY3QuYmluZChpKSx0PShhLGUsYz0xNix1PTE4MCk9PntpLmZvbnQ9YysicHggY291cmllciIsaS5maWxsVGV4dChhLHUsZSl9LG49YT0+aS5maWxsU3R5bGU9IiMiK2VbYV0sbD0iYTZ1OGEyYzNhYjJhdTZhYzRhYjRhdTRhYzVhYjNhYmF1MmFjNmFiM2FiYXUyYWM3YWI0YXUyYWM4YTZ1YWM3YWQ2YXVhYzVhZGE2dTJhYzZhZDVhdTNhMmM1YTV1NmE1IixmPSJhNHUyYWU0YXVhZTVhMmU1YXVhZTNhdTNhMyIscj0iYTV1YWU1YTJlNWF1YTV1IixvPSJhNXVhZTVhMmU0YXVhZTNhdTNhNCIsYj0iZzZoNGc0aDloM2c0aDRnOWc0ZjlmMyIsZD0iZzMiK2IrImYzIixoLG0scCxzLHYseD0wLHk9MCxrPTQsQT0uNSxhPS0xMixDPTAsRj0oKT0+MjkyKk1hdGgucmFuZG9tKCkrNjR8MCxSPShhLGUsYyx1LGk9MCx0PTIsbD0yKT0+e2Zvcig7YS5sZW5ndGg7KXt2YXIgZj1hWzBdLHI9K2FbMV07aWYobihmKSxhPWEuc2xpY2UociE9cj8xOjIpLCJ1Ij09PWYpaSs9cnx8MTtlbHNlIGZvcih2YXIgbz0ocnx8MSkraTtpPG87aSsrKWcoZStpJXUqdCxjKyhpL3V8MCkqdCx0LGwpfX0sUz0oKT0+e209YSxwPXY9MCxoPTMwOCxzPVtbNTA0LEYoKV0sWzc4OCxGKCldXX0scT0oKT0+e24oImkiKSxnKDAsMCwzNjAsNjQwKSx2JiYobSs9QSxoKz1tLHMubWFwKGE9PnthWzBdLT1rO3ZhclthLGVdPWE7UigiYTIiK2IrImEyIixhLDAsNjQsMCwxLHUpLG4oImEiKSxnKGEtMyxlLTMyLDY5LDI4NCksUihkLGEtMSxlLTMwLDY2LDAsMSwyOCksUihkLGEtMSxlKzIyMiw2NiwwLDEsMjgpLG4oImkiKSxnKGEtMyxlLDY5LDIyMCksYTwtNjQmJihwKyssQz1wPkM/cDpDLHM9Wy4uLnMuc2xpY2UoMSksW3NbMV1bMF0rMjg0LEYoKV1dKX0pLFtbZSxhXV09cyxlPDE5OCYmOTg8ZSYmKGg8YXx8YSsxODg8aCl8fGg8MHx8NjE2PGgpJiZTKCksUihsLDE2NCxoLDE2LDUpO3ZhciBhLGU9KHkvNHwwKSU0O2UlMj9SKHIsMTYyLGgrMTAsNywxKTplP1IobywxNjIsaCsxMiw3LDEpOlIoZiwxNjIsaCs2LDcsMSksbigiYiIpLHQoYFNjb3JlOiAke3B9IHwgQmVzdDogYCtDLDI2KSx2fHwodCh4PyJHYW1lIG92ZXIiOiJGbGFwcHkgYmlyZCIsMjEwLDMyKSx0KCJDbGljayB0byBwbGF5IisoeD8iIGFnYWluIjoiIiksNDMwKSx4KXx8dCgiUVIgY29kZSBlZGl0aW9uIiwyMzAsMTIsMjQ2KSx5KysscmVxdWVzdEFuaW1hdGlvbkZyYW1lKHEpfTtTKCkscSgpLGMub25jbGljaz0oKT0+e209YSx2PXg9MX08L3NjcmlwdD48L2JvZHk+PC9odG1sPg==</code></pre>
<p>И поиграть в&nbsp;<b>Flappy Bird QR code edititon</b>! А самое крутое это то, что для игры не нужно ничего кроме QR кода (ну и устройства с браузером, конечно).</p>
<p>Также, приложу скриншот с игрой. Однако, я рекомендую Вам попробовать поиграть самим!</p>
<img src="https://static.vladivanov.me/uploads/faae9eb7-b531-44cc-9f8e-1b1c3b49c87e.webp" alt="Скриншот с игрой" />
<p>Спасибо за внимание!</p>
<p>⚠️ Увы, но стандартная камера айфона не понимает data-url. Она видит QR код, но говорит “no usable data found” 😟. У меня получилось распознать код первым приложением из App Store, а вообще и с любым другим приложением проблем быть не должно. </p>
<p>🤓 В самом начале мы обсуждали версии QR кодов. Так вот, у нас получился QR код 39 версии (предпоследней).</p>
<h3>Ссылки</h3>
<ol><li>qrcode, просмотрено 12 января 2023 &lt;https://www.npmjs.com/package/qrcode&gt;</li><li>QR Code Specification, просмотрено 12 января 2023, &lt;https://www.labeljoy.com/qr-code/qr-code-specification/&gt;</li><li>QR code, просмотрено 12 января 2023, &lt;https://en.wikipedia.org/wiki/QR_code&gt;</li><li>Data URLS, MDN, просмотрено 12 января 2023, &lt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs&gt;</li><li>Base64 Characters, просмотрено 12 января 2023, &lt;https://base64.guru/learn/base64-characters&gt;&nbsp;</li></ol>]]></content:encoded>
            <enclosure url="https://static.vladivanov.me/uploads/53f9a25c-0d47-40bb-9c32-17cbbc7db3ba.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[CSS: От хаоса к порядку]]></title>
            <link>https://vladivanov.me/css-from-chaos-to-order/</link>
            <guid>a766aa9b-2668-4997-ba27-cb880f8b082a</guid>
            <pubDate>Fri, 23 Dec 2022 21:35:47 GMT</pubDate>
            <description><![CDATA[Я рассказываю о своем опыте наведения порядка в стилях в проекте, а также об инструментах, которые были разработаны для этого.]]></description>
            <content:encoded><![CDATA[<h3>Введение</h3>
<p>Привет! Если Вы веб разработчик или когда либо имели удовольствие писать фронтенд Вы должно быть знаете что такое CSS. Если Вы когда нибудь писали фронтенд для достаточно большого сервиса Вы наверное сталкивались с тем, что открывая файл со стилями годовалой давности непонятно ничего от слова совсем!</p>
<p>Разбираться в большом CSS файле достаточно трудно. Особенно, если этот файл использует препроцессоры, например, SCSS, SASS и другие. К и так не всегда понятным с первого взгляда селекторам добавляются переменные, миксины и самое страшное - вложенности!</p>
<p>Если вы наконец разобрались с тем, что описано в стилях, то нам предстоит разобраться еще в том как эти стили применяются. К сожалению, здесь тоже работает то же соотношение: разобраться в стилях тем сложнее, чем больше файл со стилями и чем больше в этих стилях используется различных наворотов.</p>
<p>Проблем добавляет еще и то, что иногда по названию класса элемента очень трудно сказать о том, что это за элемент вообще, как он выглядит и какое место занимает в интерфейсе. Для решения этой проблемы когда-то была придумана методология БЭМ. Многие люди считают ее устаревшей и я, честно признаться еще полгода назад считал так же. Однако, нужно просто понять суть и вы тут же измените свое мнение! На самом же деле БЭМ не только про нейминг, как могло бы показаться, он про логику разбиения интерфейса на составные части, называемые БЭМ сущностями, а также их переиспользование.</p>
<p>Если следовать БЭМ методологии и некоторому соглашению о разбиении файлов исходного кода, можно прийти в утопический мир будущего, где почти всегда в ОДНОМ файле стили для ОДНОГО элемента, а читая код не возникает примерно такой реакции:</p>
<img src="https://static.vladivanov.me/uploads/d71fb130-a783-4dd1-910a-6d4263443397.webp" alt="Мем с Шелдоном Купером "Почему?"" />
<p>Но эта статья на самом деле не совсем про БЭМ. Что если я скажу вам, что даже с использованием вышеописанных практик есть место для улучшений опыта разработки?Именно об этом я и предлагаю сегодня поговорить.</p>
<h3>Идея</h3>
<p>Вы наверное знаете что такое <a href="https://www.npmjs.com/package/css-loader">css-loader</a> и как он позволяет работать с <a href="https://github.com/css-modules/css-modules">CSS модулями</a>. Если же это не так, то позвольте показать что это такое на примере React компонента:</p>
<pre><code>import styles from &#039;./Button.css&#039;;

export const Button = ({ children }) =&gt; {
  return &lt;button type=&quot;button&quot; className={styles.Button}&gt;{children}&lt;/button&gt;;
}</code></pre>
<p>Удобно, неправда ли? Вы буквально импортируете класснеймы и привязываете их к нужным элементам в интерфейсе.</p>
<p>Если разработка ведется с использованием БЭМ методологии, то это может выглядеть примерно так:</p>
<pre><code>import cls from &#039;classnames&#039;;
import styles from &#039;./Button.css&#039;;

// type: primary | secondary

export const Button = ({ children, type }) =&gt; {
  return (
    &lt;button 
      type=&quot;button&quot; 
      className={cls(styles.Button, {
        [styles.Button_type_primary]: type === &#039;primary&#039;,
        [styles.Button_type_secondary]: type === &#039;secondary&#039;
      })}
    &gt;
      {children}
    &lt;/button&gt;
  );
}</code></pre>
<p>В БЭМ такие сущности называются модификаторами. Обычно, их используют, чтобы задать определенный вид элементу. В примере выше кнопка могла бы менять цвет в зависимости от того, какой у нее <code><code class="inline-code">type</code></code>.</p>
<p>Также, нетрудно догадаться, что для работы с модификаторами нам необходим некий вспомогательный инструмент, чтобы не заниматься конкатенацией классов вручную. В примере выше в качестве такого инструмента выступает npm пакет <a href="https://www.npmjs.com/package/classnames">classnames</a>.</p>
<p>Подождите! Но ведь если у нас есть конкретные правила именования не можем ли мы сами написать функцию по аналогии с <code><code class="inline-code">cls</code></code>, которая могла бы включать определенные модификаторы в зависимости от передаваемых опций. Давайте попробуем:</p>
<pre><code>import styles from &#039;./Button.css&#039;;

const cls = (base, mods) =&gt; {
  let names = Object.entries(mods).map(([modName, modVal]) =&gt; {
    return `${base}_${modName}_${modVal}`;
  });

  return names.concat([base]).join(&#039; &#039;);
}

// type: primary | secondary

export const Button = ({ children, type }) =&gt; {
  return (
    &lt;button 
      type=&quot;button&quot; 
      className={cls(styles.Button, { type })}
    &gt;
      {children}
    &lt;/button&gt;
  );
}</code></pre>
<p>Выглядит круто! Но неудобно, что нужно повсюду таскать за собой <code class="inline-code">cls</code> функцию. К тому же, очень трудно предусмотреть все сценарии применения такой функции. Например, Если БЭМ модификатор булевый, то значение модификатора <code><code class="inline-code">true</code></code> можно опустить и использовать просто имя, например <code><code class="inline-code">Button_disabled</code></code>.</p>
<p>Ну что ж, встречайте!</p>
<h3>Реализация</h3>
<p>Для начала просто посмотрите небольшое демо:</p>
<img src="https://static.vladivanov.me/uploads/703eea95-b648-49af-9251-b052b26ec005.webp" alt="Functional BEM демо" />
<p>По сути все то, о чем я писал выше объединено воедино, а сверху еще и добавлена поддержка TypeScript.</p>
<p>Все это называется functional BEM и вы можете узнать о проекте подробнее здесь <a href="https://github.com/yungvldai/fbem">https://github.com/yungvldai/fbem</a>. Все пакеты <code class="inline-code">@fbem</code> являются open-source проектами и распространяются под лицензией MIT, а это значит, что вы можете использовать их в своих проектах прямо сейчас!</p>
<p>В основе проекта лежит специальный лоадер <a href="https://github.com/yungvldai/fbem/tree/master/packages/css-loader">@fbem/css-loader</a> для webpack, который позволяет импортировать из CSS файлов специальные БЭМ-функции. У БЭМ функции всегда два аргумента: объект модификаторов, где ключ - имя модификатора, а значение - его значение; второй - массив миксов (термин из методологии). Проще говоря, это просто массив строк, которые будут добавлены в конец результирующего класса.</p>
<p>@fbem/css-loader очень схож с css-loader (и на самом деле его форк) и имеет почти тот же набор опций и функционал. Вы также можете использовать его с различными препроцессорами с помощью чейнинга лоадеров в конфиге webpack (но подумайте действительно ли они вам нужны 😏).</p>
<p>Как я писал выше, с CSS гораздо удобнее работать, когда он логично разбит на несколько файлов. В идеале в одном файле должны быть стили только для одного DOM элемента (конечно, есть сложные сценарии, когда по ховеру стили должны применяться к ребенку, но таких случаев сильно меньше, чем случаев, когда вам нужно просто применить какие-нибудь стили без всякой логики).</p>
<p>Для этих целей в <code class="inline-code">@fbem</code>&nbsp;предусмотрена специальная функция <code class="inline-code">compose</code> в пакете <a href="https://github.com/yungvldai/fbem/tree/master/packages/utils">utils</a>. Эта функция позволяет комбинировать несколько БЭМ-функций в одну:</p>
<pre><code>import { compose } from &#039;@fbem/utils&#039;;

import { cnButton as modDisabled } from &#039;./_disabled/button_disabled.css&#039;;
import { cnButton as modStyle } from &#039;./_style/button_style.css&#039;;
import { cnButton as base } from &#039;./button.css&#039;;

const cnButton = compose(base, modDisabled, modStyle);

cnButton({ style: &#039;flat&#039;, disabled: true }, [&#039;mix&#039;]); // &#039;button button_style_flat button_disabled mix&#039;</code></pre>
<h3>Заключение</h3>
<p>В этой статье я постарался показать метод (и инструменты для реализации) организации стилей в проекте. Больше информации о <code class="inline-code">@fbem</code>&nbsp;вы сможете найти на <a href="https://github.com/yungvldai/fbem">странице проекта на Github</a>. Я надеюсь Вам было интересно узнать о моих наработках, а также надеюсь Вы поделитесь этой статьей с кем-нибудь 😉.</p>
<p>Спасибо!</p>
<p>P.S. Этот блок написан с использованием <code class="inline-code">@fbem</code>.</p>]]></content:encoded>
            <enclosure url="https://static.vladivanov.me/uploads/c545e869-3283-4ef7-baf8-9ef8fb5e98c2.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[О блоге]]></title>
            <link>https://vladivanov.me/about/</link>
            <guid>e1bc8417-8064-4321-a9ee-aabe32a1cfe3</guid>
            <pubDate>Fri, 16 Dec 2022 22:28:32 GMT</pubDate>
            <description><![CDATA[Маленькая заметка о том, кто я такой, и почему я создал этот блог]]></description>
            <content:encoded><![CDATA[<p><b>Привет</b>&nbsp;<b>👋, </b>Меня зовут Владислав Иванов, я закончил Факультет Систем Управления и Робототехники в Университете ИТМО, а сейчас я работаю фронтенд-разработчиком в TripleTen (<a href="https://tripleten.com/">https://tripleten.com/</a>).</p>
<p>Я начал вести блог вместо дневника практики в университете. Потом мне просто понравилось писать статьи на разные темы. Чаще технические. <a href="https://yungvldai.ru/">Здесь</a> Вы можете найти мой старый блог. Сейчас я хочу немного сменить формат, поэтому я создал новый блог (этот).</p>
<p>Если у Вас есть какие-либо предложения, жалобы или пожелания, пожалуйста, пишите на&nbsp;<a href="mailto:me@vladivanov.me">me@vladivanov.me</a>.</p>]]></content:encoded>
            <enclosure url="https://static.vladivanov.me/uploads/15ee49b0-fa97-498c-bb15-2815987539f3.webp" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>