diff --git a/.markdownlint.json b/.markdownlint.json index 5e36605..ba8e1ae 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,15 +1,18 @@ { - "MD007": { "indent": 4 }, + "MD007": { + "indent": 4 + }, "MD010": false, "MD013": false, "MD022": false, "MD025": false, "MD026": false, "MD030": { + "ol_single": 2, "ul_single": 3, - "ol_single": 1, + "ol_multi": 2, "ul_multi": 3, - "ol_multi": 3 + "list" }, "MD033": false, "MD036": false, diff --git a/.vscode/settings.json b/.vscode/settings.json index 02312aa..d23f33f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,6 @@ { "editor.tabCompletion": "on", - "diffEditor.codeLens": true + "diffEditor.codeLens": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true } diff --git a/README.md b/README.md index f8fc4f9..76b2582 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ [расширения PyMdown](https://facelessuser.github.io/pymdown-extensions/installation/) и тему [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/): ``` +python -m venv venv +source venv/bin/activate pip3 install -r ./requirements.txt ``` diff --git a/docs/.pages b/docs/.pages index a06ab7b..6d55783 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,5 +1,7 @@ nav: - - index.md + - 🏠: index.md - guide - book - - prisma-orm + - libs + - articles + - Спасибо: https://boosty.to/bndby diff --git a/docs/articles/.pages b/docs/articles/.pages new file mode 100644 index 0000000..78f5f26 --- /dev/null +++ b/docs/articles/.pages @@ -0,0 +1,11 @@ +title: Статьи +nav: + - index.md + - game-of-life + - tsconfig-json.md + - promise-execution + - making-sense-of-senseless-javascript-features + - event-loop.md + - 67debug + - i18n.md + - ... diff --git a/docs/articles/67debug/.pages b/docs/articles/67debug/.pages new file mode 100644 index 0000000..4763e7d --- /dev/null +++ b/docs/articles/67debug/.pages @@ -0,0 +1,3 @@ +title: 67 странных трюков отладки +nav: + - index.md diff --git a/docs/articles/67debug/conditional-breakpoint-DOM-mutation-chrome.avif b/docs/articles/67debug/conditional-breakpoint-DOM-mutation-chrome.avif new file mode 100644 index 0000000..355d923 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-DOM-mutation-chrome.avif differ diff --git a/docs/articles/67debug/conditional-breakpoint-argument-length.gif b/docs/articles/67debug/conditional-breakpoint-argument-length.gif new file mode 100644 index 0000000..c7ee595 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-argument-length.gif differ diff --git a/docs/articles/67debug/conditional-breakpoint-arity-check.gif b/docs/articles/67debug/conditional-breakpoint-arity-check.gif new file mode 100644 index 0000000..0951d39 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-arity-check.gif differ diff --git a/docs/articles/67debug/conditional-breakpoint-console-log.gif b/docs/articles/67debug/conditional-breakpoint-console-log.gif new file mode 100644 index 0000000..41207c5 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-console-log.gif differ diff --git a/docs/articles/67debug/conditional-breakpoint-gated-enable-from-breakpoint.avif b/docs/articles/67debug/conditional-breakpoint-gated-enable-from-breakpoint.avif new file mode 100644 index 0000000..236e581 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-gated-enable-from-breakpoint.avif differ diff --git a/docs/articles/67debug/conditional-breakpoint-gated.avif b/docs/articles/67debug/conditional-breakpoint-gated.avif new file mode 100644 index 0000000..9d071a2 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-gated.avif differ diff --git a/docs/articles/67debug/conditional-breakpoint-parameter-override.gif b/docs/articles/67debug/conditional-breakpoint-parameter-override.gif new file mode 100644 index 0000000..4273cf2 Binary files /dev/null and b/docs/articles/67debug/conditional-breakpoint-parameter-override.gif differ diff --git a/docs/articles/67debug/console-table-in-watch.avif b/docs/articles/67debug/console-table-in-watch.avif new file mode 100644 index 0000000..2b73564 Binary files /dev/null and b/docs/articles/67debug/console-table-in-watch.avif differ diff --git a/docs/articles/67debug/console-time-performance-profile.gif b/docs/articles/67debug/console-time-performance-profile.gif new file mode 100644 index 0000000..73cf05f Binary files /dev/null and b/docs/articles/67debug/console-time-performance-profile.gif differ diff --git a/docs/articles/67debug/console-trace-find-stack.gif b/docs/articles/67debug/console-trace-find-stack.gif new file mode 100644 index 0000000..18fd91f Binary files /dev/null and b/docs/articles/67debug/console-trace-find-stack.gif differ diff --git a/docs/articles/67debug/elusive-element-inspected.gif b/docs/articles/67debug/elusive-element-inspected.gif new file mode 100644 index 0000000..d15409e Binary files /dev/null and b/docs/articles/67debug/elusive-element-inspected.gif differ diff --git a/docs/articles/67debug/elusive-element.gif b/docs/articles/67debug/elusive-element.gif new file mode 100644 index 0000000..8922f8f Binary files /dev/null and b/docs/articles/67debug/elusive-element.gif differ diff --git a/docs/articles/67debug/getEventListeners.avif b/docs/articles/67debug/getEventListeners.avif new file mode 100644 index 0000000..7c3ffa5 Binary files /dev/null and b/docs/articles/67debug/getEventListeners.avif differ diff --git a/docs/articles/67debug/index.md b/docs/articles/67debug/index.md new file mode 100644 index 0000000..ad8cdc9 --- /dev/null +++ b/docs/articles/67debug/index.md @@ -0,0 +1,368 @@ +--- +description: Список полезных и неочевидных хаков, позволяющих получить максимальную отдачу от отладчика вашего браузера. Предполагает понимание инструментов разработчика на среднем или более высоком уровне +--- + +# 67 странных трюков отладки + +Список полезных и неочевидных хаков, позволяющих получить максимальную отдачу от отладчика вашего браузера[^1]. Предполагает понимание инструментов разработчика на среднем или более высоком уровне. + +[^1]: Советы поддерживаются в Chrome, Firefox и Edge, если на логотипах браузеров не указано иное: + +## Расширенные условные точки останова + +Используя выражения, которые имеют побочные эффекты там, где вы не ожидаете, мы можем выжать больше функциональности из базовых функций, таких как условные точки останова. + +### Логпойнты/Трасепойнты + +Например, мы можем использовать `console.log` в точках останова. Логпоинты - это точки останова, которые выводят журнал в консоль без приостановки выполнения. В то время как Microsoft Edge уже давно имеет встроенные точки останова, а Chrome только что добавил их в v73, в Firefox их нет. Но мы можем использовать условные точки останова для их имитации в любом браузере. + +![Условная точка останова - console.log](conditional-breakpoint-console-log.gif) + +Используйте `console.count` вместо `console.log`, если вам также нужен бегущий счетчик того, сколько раз была выполнена строка. + +!!!warning "ОБНОВЛЕНИЕ (май 2020 года)" + + Все основные браузеры теперь напрямую поддерживают логпоинты/трасспоинты ([Chrome Logpoints](https://developers.google.com/web/updates/2019/01/devtools#logpoints), [Edge Tracepoints](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide/debugger#breakpoints), [Firefox Logpoints](https://developer.mozilla.org/docs/Tools/Debugger/Set_a_logpoint)) + +#### Панель наблюдения + +Вы также можете использовать `console.log` в панели наблюдения. Например, чтобы сбрасывать снимок `localStorage` каждый раз, когда ваше приложение приостанавливается в отладчике, вы можете создать часы `console.table(localStorage)`: + +![console.table в панели наблюдения](console-table-in-watch.avif) + +Или, чтобы выполнить выражение после мутации DOM, установите точку останова мутации DOM (в инспекторе элементов): + +![Точка прерывания мутации DOM](conditional-breakpoint-DOM-mutation-chrome.avif) + +А затем добавьте свое выражение watch, например, для записи снимка DOM: `(window.doms = window.doms || []).push(document.documentElement.outerHTML)`. Теперь, после любого изменения поддерева DOM, отладчик приостановит выполнение, и новый снимок DOM будет находиться в конце массива `window.doms`. (Не существует способа создать точку останова для мутации DOM, которая не приостанавливала бы выполнение). + +#### Трассировка стеков вызовов + +Допустим, у вас есть функция, которая показывает загрузочный спиннер, и функция, которая его скрывает, но где-то в вашем коде вы вызываете метод `show` без соответствующего вызова hide. Как найти источник непарного вызова `show`? Используйте `console.trace` в условной точке останова в методе `show`, запустите свой код, найдите последнюю трассировку стека для метода `show` и щелкните по вызывающему методу, чтобы перейти к коду: + +![console.trace в условной точке останова](console-trace-find-stack.gif) + +### Изменение поведения программы + +Используя выражения, которые имеют побочные эффекты на поведение программы, мы можем изменять поведение программы на лету, прямо в браузере. + +Например, вы можете переопределить параметр функции `getPerson`, `id`. Поскольку `id=1` оценивается как true, эта условная точка останова приостановит работу отладчика. Чтобы предотвратить это, добавьте `, false` к выражению. + +![Условная точка останова - переопределение параметров](conditional-breakpoint-parameter-override.gif) + +### Быстрое и грязное профилирование производительности + +Не стоит засорять профилирование производительности такими вещами, как время оценки условных точек останова, но если вам нужно быстро и грязно измерить, сколько времени занимает выполнение какого-либо действия, вы можете использовать консольный API тайминга в условных точках останова. В начальной точке установите точку останова с условием `console.time('label')`, а в конечной точке установите точку останова с условием `console.timeEnd('label')`. Каждый раз, когда будет выполняться измеряемое вами действие, браузер будет выводить в консоль журнал о том, сколько времени оно занимает. + +![Условная точка останова - профиль производительности](console-time-performance-profile.gif) + +### Использование функции Arity + +#### Прерывание по количеству аргументов + +Делайте паузу только в том случае, если текущая функция вызывается с 3 аргументами: `arguments.callee.length === 3`. + +Пригодится, если у вас есть перегруженная функция с необязательными параметрами. + +![Условная точка останова - длина аргумента](conditional-breakpoint-argument-length.gif) + +#### Прерывание при несоответствии арности функции + +Пауза возникает только в том случае, если текущая функция вызывается с неправильным количеством аргументов: `(arguments.callee.length) != arguments.length` + +![Условная точка останова - проверка на целостность](conditional-breakpoint-arity-check.gif) + +Полезно при поиске ошибок в местах вызова функций. + +### Использование времени + +#### Пропустить загрузку страницы + +Не делайте паузу до 5 секунд после загрузки страницы: `performance.now() > 5000`. + +Полезно, когда вы хотите установить точку останова, но вас интересует только приостановка выполнения после начальной загрузки страницы. + +#### Пропустить N секунд + +Не приостанавливайте выполнение, если точка останова будет достигнута в течение следующих 5 секунд, но приостанавливайте выполнение в любое время после этого: `window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000`. + +Сбросьте счетчик из консоли в любое время: `window.baseline = Date.now()`. + +### Использование CSS + +Приостановка на основе вычисленных значений CSS, например, приостанавливать выполнение только тогда, когда тело документа имеет красный цвет фона: `window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)"`. + +### Только четные вызовы + +Делайте паузу только при каждом втором выполнении строки: `window.counter = window.counter || 0, window.counter % 2 === 0` + +### Break on Sample + +Выполняйте прерывание только при случайной выборке выполнения строки, например, прерывайте только 1 из каждых 10 раз выполнения строки: `Math.random() < 0.1` + +### Never Pause Here + + + +Когда вы щелкаете правой кнопкой мыши по желобу и выбираете "Never Pause Here", Chrome создает условную точку останова, которая будет `false` и никогда не пройдет. Таким образом, отладчик никогда не будет останавливаться на этой строке. + +![Never Pause Here](never-pause-here.avif) + +![Never Pause Here Result](never-pause-here-result.avif) + +Пригодится, если вы хотите освободить строку от точек останова XHR, проигнорировать возникающее исключение и т. д. + +### Автоматические идентификаторы экземпляров + +Автоматически присваивайте уникальный ID каждому экземпляру класса, установив эту условную точку останова в конструкторе: `(window.instances = window.instances || []).push(this)`. + +Затем для получения уникального идентификатора: `window.instances.indexOf(instance)` (например, `window.instances.indexOf(this)` в методе класса) + +### Программное переключение + +Используйте глобальное булево значение для установки одной или нескольких условных точек останова: + +![Boolean gate](conditional-breakpoint-gated.avif) + +Затем программно переключите булево значение, например + +- вручную, из консоли + + ```javascript + window.enableBreakpoints = true; + ``` + +- из других точек останова + + ![Булевы ворота - разрешение от другой точки останова](conditional-breakpoint-gated-enable-from-breakpoint.avif) + +- с таймера на консоли + + ```javascript + setTimeout( + () => (window.enableBreakpoints = true), + 5000 + ); + ``` + +- прочее + +## `monitor()` class Calls + + + +Вы можете использовать метод командной строки Chrome `monitor`, чтобы легко отследить все вызовы методов класса. Например, задан класс `Dog` + +```javascript +class Dog { + bark(count) { + /* ... */ + } +} +``` + +Если мы хотим узнать все вызовы, сделанные для всех экземпляров `Dog`, вставьте это в командную строку: + +```javascript +var p = Dog.prototype; +Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k])); +``` + +и вы получите вывод в консоли: + +``` +> function bark called with arguments: 2 +``` + +Вы можете использовать `debug` вместо `monitor`, если хотите приостановить выполнение при любом вызове метода (вместо того, чтобы просто вести журнал в консоли). + +### Из конкретного экземпляра + + + +Если вы не знаете класс, но у вас есть его экземпляр: + +```javascript +var p = instance.constructor.prototype; +Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k])); +``` + +Пригодится, если вы хотите написать функцию, которая делает это для любого экземпляра любого класса (а не только для `Dog`) + +## Вызов и отладка функции + +Перед вызовом функции, которую вы хотите отладить в консоли, вызовите `debugger`. Например: + +```javascript +function fn() { + /* ... */ +} +``` + +Из консоли: + +``` +> debugger; fn(1); +``` + +А затем "Step into next function call" для отладки реализации `fn`. + +Полезно, если вам не хочется искать определение `fn` и добавлять точку останова вручную или если `fn` динамически привязана к функции и вы не знаете, где находится ее исходный текст. + +В Chrome вы также можете опционально вызвать `debug(fn)` в командной строке, и отладчик будет приостанавливать выполнение внутри `fn` при каждом ее вызове. + +## Приостановка выполнения при изменении URL + +Чтобы приостановить выполнение перед тем, как одностраничное приложение изменит URL (т. е. произойдет какое-то событие маршрутизации): + +```javascript +const dbg = () => { + debugger; +}; +history.pushState = dbg; +history.replaceState = dbg; +window.onhashchange = dbg; +window.onpopstate = dbg; +``` + +Создание версии `dbg`, которая приостанавливает выполнение, не нарушая навигацию, - это упражнение, оставленное на усмотрение читателя. + +Также обратите внимание, что это не помогает, когда код вызывает `window.location.replace/assign` напрямую, потому что страница немедленно выгружается после назначения, так что отлаживать нечего. Если вы все же хотите увидеть источник этих перенаправлений (и отладить свое состояние в момент перенаправления), в Chrome вы можете `отладить` соответствующие методы: + +```javascript +debug(window.location.replace); +debug(window.location.assign); +``` + +## Отладка чтения свойств + +Если у вас есть объект и вы хотите знать, когда у него читается какое-либо свойство, используйте геттер объекта с вызовом `debugger`. Например, преобразуйте `{configOption: true}` в `{get configOption() { debugger; return true; }}` (либо в исходном коде, либо с помощью условной точки останова). + +Полезно, когда вы передаете какие-то параметры конфигурации чему-то и хотите посмотреть, как они используются. + +## Используйте `copy()` + + + +С помощью консольного API `copy()` можно скопировать интересную информацию из браузера прямо в буфер обмена без обрезания строк. Некоторые интересные вещи, которые вы можете захотеть скопировать: + +- Снимок текущего DOM: `copy(document.documentElement.outerHTML)`. +- Метаданные о ресурсах (например, изображениях): `copy(performance.getEntriesByType("resource"))`. +- Большой JSON-блоб, отформатированный: `copy(JSON.parse(blob))`. +- Дамп вашего локального хранилища: `copy(localStorage)`. +- И т.д. + +## Отладка HTML/CSS + +Консоль JS может быть полезна при диагностике проблем с HTML/CSS. + +### Осмотр DOM с отключенным JS + +Находясь в инспекторе DOM, нажмите ++ctrl+backslash++ (Chrome/Windows), чтобы приостановить выполнение JS в любой момент. Это позволит вам просмотреть снимок DOM, не беспокоясь о том, что JS изменит DOM или события (например, mouseover) заставят DOM измениться под вами. + +### Осмотр неуловимого элемента + +Допустим, вы хотите проинспектировать элемент DOM, который появляется только условно. Чтобы осмотреть этот элемент, нужно подвести к нему мышь, но при попытке это сделать он исчезает: + +![Elusive element](elusive-element.gif) + +Для проверки элемента вы можете вставить в консоль следующее: `setTimeout(function() { debugger; }, 5000);`. Это даст вам 5 секунд для запуска UI, а затем, как только истечет 5-секундный таймер, выполнение JS приостановится, и ничто не заставит ваш элемент исчезнуть. Вы можете свободно перемещать мышь к инструментам разработчика, не теряя элемент: + +![Elusive element - inspected](elusive-element-inspected.gif) + +Пока выполнение JS приостановлено, вы можете осматривать элемент, редактировать его CSS, выполнять команды в консоли JS и т. д. + +Полезно при осмотре DOM, который зависит от конкретной позиции курсора, фокуса и т. д. + +### Запись моментальных снимков DOM + +Чтобы получить копию DOM в его текущем состоянии: + +```javascript +copy(document.documentElement.outerHTML); +``` + +Для записи снимка DOM каждую секунду: + +```javascript +doms = []; +setInterval(() => { + const domStr = document.documentElement.outerHTML; + doms.push(domStr); +}, 1000); +``` + +Или просто сбросьте его на консоль: + +```javascript +setInterval(() => { + const domStr = document.documentElement.outerHTML; + console.log('snapshotting DOM: ', domStr); +}, 1000); +``` + +### Наблюдение за сфокусированным элементом + +```javascript +(function () { + let last = document.activeElement; + setInterval(() => { + if (document.activeElement !== last) { + last = document.activeElement; + console.log('Focus changed to: ', last); + } + }, 100); +})(); +``` + +![### Наблюдение за сфокусированным элементом](monitor-focus.gif) + +### Поиск жирных элементов + +```javascript +const isBold = (e) => { + let w = window.getComputedStyle(e).fontWeight; + return w === 'bold' || w === '700'; +}; +Array.from(document.querySelectorAll('*')).filter(isBold); +``` + +#### Только потомки + +Или просто потомки элемента, выбранного в данный момент в инспекторе: + +```javascript +Array.from($0.querySelectorAll('*')).filter(isBold); +``` + +### Ссылка на выбранный в данный момент элемент + +`$0` в консоли - это автоматическая ссылка на текущий выбранный элемент в инспекторе элементов. + +#### Предыдущие элементы + + + +В Chrome и Edge вы можете получить доступ к элементу, который вы проверяли последним, с помощью `$1`, к элементу перед этим с помощью `$2` и т. д. + +#### Получение слушателей событий + + + +В Chrome вы можете просмотреть слушателей событий текущего выбранного элемента: `getEventListeners($0)`, например. + +![getEventListeners](getEventListeners.avif) + +### Мониторинг событий для элемента + + + +Отладка всех событий для выбранного элемента: `monitorEvents($0)`. + +Отладка конкретных событий для выбранного элемента: `monitorEvents($0, ["control", "key"])`. + +![monitorEvents](monitorEvents.gif) + +## Источник + +- [67 Weird Debugging Tricks Your Browser Doesn't Want You to Know](https://alan.norbauer.com/articles/browser-debugging-tricks) diff --git a/docs/articles/67debug/monitor-focus.gif b/docs/articles/67debug/monitor-focus.gif new file mode 100644 index 0000000..556dd3a Binary files /dev/null and b/docs/articles/67debug/monitor-focus.gif differ diff --git a/docs/articles/67debug/monitorEvents.gif b/docs/articles/67debug/monitorEvents.gif new file mode 100644 index 0000000..d0e7b05 Binary files /dev/null and b/docs/articles/67debug/monitorEvents.gif differ diff --git a/docs/articles/67debug/never-pause-here-result.avif b/docs/articles/67debug/never-pause-here-result.avif new file mode 100644 index 0000000..8073782 Binary files /dev/null and b/docs/articles/67debug/never-pause-here-result.avif differ diff --git a/docs/articles/67debug/never-pause-here.avif b/docs/articles/67debug/never-pause-here.avif new file mode 100644 index 0000000..ef66daf Binary files /dev/null and b/docs/articles/67debug/never-pause-here.avif differ diff --git a/docs/articles/event-loop.md b/docs/articles/event-loop.md new file mode 100644 index 0000000..a853a7a --- /dev/null +++ b/docs/articles/event-loop.md @@ -0,0 +1,603 @@ +# Event Loop в деталях + +В данной статье поговорим о том, почему Event Loop вообще был создан, как с ним работать и почему про него спрашивают на собесах. + +JS был спроектирован как однопоточный язык программирования. Это значит, что он может выполнять только одну операцию одновременно. Тем не менее у JavaScript есть такой механизм как Event Loop, который как раз и позволяет выполнять "асинхронные" операции. Почему "асинхронные" в кавычках? Да просто потому что JavaScript тоже выполняет их синхронно, асинхронности в самом JavaScript как таковой нет. Вперед под кат, будем разбираться. + +## Синхронный код + +С синхронным кодом все более или менее ясно: интерпретатор проходится по каждой инструкции выполняет ее и все работает. + +```js +let acc = 1; +console.log(acc); // 1 + +acc++; +console.log(acc); // 2 + +let anotherAcc = acc; +console.log(anotherAcc, acc); // 2 2 +``` + +```mermaid +stateDiagram-v2 + VarDecl: Объявление переменной - acc + VarOutput: Вывод переменной - acc + VarInc: Инкрементация переменной - acc + VarOutput2: Вывод переменной - acc + VarDecl2: Объявление переменной - anotherAcc + VarOutput3: Вывод переменной - acc, anotherAcc + + [*] --> VarDecl + VarDecl --> VarOutput + VarOutput --> VarInc + VarInc --> VarOutput2 + VarOutput2 --> VarDecl2 + VarDecl2 --> VarOutput3 + VarOutput3 --> [*] +``` + +## Асинхронный код + +С асинхронным кодом все немножко сложнее. Рассмотрим следующий пример: + +```js +let acc = 1; +console.log('Call 1:', acc); + +acc++; +console.log('Call 2:', acc); + +setTimeout(() => { + acc++; + console.log('Call 3:', acc); +}, 0); + +let anotherAcc = acc; +console.log('Call 4:', anotherAcc, acc); + +/* Output + +Call 1: 1 +Call 2: 2 +Call 4: 2 2 +Call 3: 3 + +*/ +``` + +Как мы можем увидеть `console.log`, который был в `setTimeout` почему-то выполнился позже, но почему так случилось? + +Тут в силу и вступает Event Loop. Так как `setTimeout` это асинхронная операция (таймер высчитывается на стороне браузера, а не в JS). + +### Проходимся по коду как интерпретатор + +Давайте рассмотрим все более подробно. Мы объявляем все наши переменные и проделываем с ними какие-то операции. Все синхронные операции будут выполняться как только интерпретатор дойдет до них: + +```mermaid +stateDiagram-v2 + Program: Скрипт + Program + + state Timer { + VarOutput4: Вывод переменной + VarOutput4 + } + + state EventLoop { + [*] + } + + state Program { + VarDecl: Объявление переменной + VarOutput: Вывод переменной + VarInc: Инкрементация переменной + VarOutput2: Вывод переменной + VarDecl2: Объявление переменной + VarOutput3: Вывод переменной + Timer: Таймер + + + + [*] --> VarDecl + VarDecl --> VarOutput + VarOutput --> VarInc + VarInc --> VarOutput2 + VarOutput2 --> Timer + Timer --> VarDecl2 + VarDecl2 --> VarOutput3 + VarOutput3 --> [*] + -- + EventLoop: Event Loop + + } +``` + +Другое дело обстоит с таймером, время которое ожидает таймаут будет считаться на стороне браузера, поэтому операция как бы "пропадет" из очереди. Таймер попал в Event Loop, где будет ждать покуда браузер не пришлет сигнал, о том что время для таймаута вышло и коллбэк внутри таймера можно выполнять. + +```mermaid +stateDiagram-v2 + Program: Скрипт + Program + + state Timer { + VarOutput4: Вывод переменной + VarOutput4 + } + + state EventLoop { + Timer: Таймер + Timer + } + + state Program { + VarDecl: Объявление переменной + VarOutput: Вывод переменной + VarInc: Инкрементация переменной + VarOutput2: Вывод переменной + VarDecl2: Объявление переменной, anotherAcc + VarOutput3: Вывод переменной + + [*] --> VarDecl + VarDecl --> VarOutput + VarOutput --> VarInc + VarInc --> VarOutput2 + VarOutput2 --> VarDecl2 + VarDecl2 --> VarOutput3 + VarOutput3 --> [*] + -- + EventLoop: Event Loop + } +``` + +Теперь самое неочевидное: даже если таймаут выполнился, а функция в которой мы выполняем все синхронные операции еще не выполнилась, Event Loop будет держать все что в нем содержится, покуда у нас не очистится Call Stack. **Только после того как все синхронные операции в функции выполнились** Event Loop отдаст нам наш таймер, который мы сможем выполнить: + +```mermaid +stateDiagram-v2 + Program: Скрипт + Program + + state EventLoop { + [*] --> VarOutput4 + } + + state Program { + VarDecl: Объявление переменной + VarOutput: Вывод переменной + VarInc: Инкрементация переменной + VarOutput2: Вывод переменной + VarDecl2: Объявление переменной + VarOutput3: Вывод переменной, anotherAcc + VarOutput4: Вывод переменной (из таймера) + + [*] --> VarDecl + VarDecl --> VarOutput + VarOutput --> VarInc + VarInc --> VarOutput2 + VarOutput2 --> VarDecl2 + VarDecl2 --> VarOutput3 + VarOutput3 --> VarOutput4 + VarOutput4 --> [*] + -- + EventLoop: Event Loop + } +``` + +## Лезем в дебри + +Как уже было сказано выше интерпретатор в JavaScript выполняет одну операцию за раз, все что является асинхронным он отправляет в Event Loop. Однако, вы наверное могли слышать о таких вещах как "макротаски" и "микротаски". + +Дело в том, что Event Loop - единственный механизм в JavaScript, который позволяет реализовать асинхронность (хотя по сути все операции выполняются синхронно, просто очень быстро, об этом далее). Event Loop является стеком, где хранятся все задачи, которые не вошли в синхронный поток выполнения. После завершения синхронного потока - задачи начинают выполняться из Event Loop'а. Однако у Event Loop'а тоже есть свои правила. Он делит все задачи на подтипы: + +- Микрозадачи +- Макрозадачи +- Задачи отрисовки + +Макрозадачами являются все асинхронные операции, такие как [`XmlHTTPRequest`](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest), [`setTimeout`](https://developer.mozilla.org/docs/Web/API/setTimeout) и так далее. + +В микрозадачи попадают в основном только две категории: [`then`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) у промисов, а также [Intersection Observer](https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API). + +В задачи отрисовки попадают задачи связанные с отрисовкой и обновлением контента страницы. + +Если бы мы делали свой Event Loop, то он бы выглядел следующим образом: + +```ts +// Задача +interface Task { + execute: () => {}; +} + +// Разные типы макрозадач +interface MacroTasks { + parser: Task[]; + resources: Task[]; + domManipulation: Task[]; + events: Task[]; + callbacks: Task[]; +} + +// Типизация для самого Event Loop'а +interface EventLoop { + macrotasks: MacroTasks; + microtasks: Task[]; + nextMacrotask: () => Task | null; + needRender: () => boolean; + render: () => void; +} + +// Переменная, которая меняется в зависимости от того +// есть ли синхронные операции в callstack +let isCallStackEmpty = false; + +const eventLoop: EventLoop = { + macrotasks: { + parser: [], // Парсеры HTML, CSS, A11y Tree + resources: [], // Подгрузка ресурсов + domManipulation: [], // Манипуляции с DOM + events: [], // Ивенты UI + callbacks: [], // Коллбэки из setTimeout и так далее + }, + + microtasks: [], + + // Отдает нам следующую задачу из списка макрозадач + nextMacrotask() { + for (const macrotaskSection in this.macrotasks) { + const queue = this.macrotasks[ + macrotaskSection as keyof typeof this.macrotasks + ]; + + if (queue.length) { + return queue.shift() ?? null; + } + } + return null; + }, + + needRender: () => false, + render() {}, +}; + +// Event loop начал свою работу +while (true) { + if (!isCallStackEmpty) { + continue; + } + + for (const task of eventLoop.microtasks) { + task.execute(); + } + eventLoop.microtasks = []; + + const task = eventLoop.nextMacrotask(); + if (task) { + task.execute(); + } + + if (eventLoop.needRender()) { + eventLoop.render(); + } +} +``` + +Самое интересное в данном коде находится внизу, внутри цикла `while`: + +```ts +// Event loop начал свою работу +while (true) { + if (!isCallStackEmpty) { + continue; + } + + for (const task of eventLoop.microtasks) { + task.execute(); + } + eventLoop.microtasks = []; + + const task = eventLoop.nextMacrotask(); + if (task) { + task.execute(); + } + + if (eventLoop.needRender()) { + eventLoop.render(); + } +} +``` + +Мы можем наглядно рассмотреть как Event Loop делает решения о том какую задачу брать и выполнять первой. + +1. Сначала Event Loop проверяет выполнились ли все синхронные задачи +2. Потом выполняются все задачи из микротасков +3. После выполнения всех микротасков - очередь очищается +4. Затем мы берем одну макрозадачу из списка и выполняем ее +5. После выполнения мы смотрим нужно ли нам сделать перерисовку страницы +6. Если перерисовать страницу нужно - делаем это +7. Все снова начинается с первого пункта + +## Эксперименты ✨ + +Весь этот флоу достаточно легко проверить. Давайте возьмем несколько микро и макро операций и попробуем запустить их: + +```js +console.log('Step 1: In global scope'); + +setTimeout(() => console.log('Step 2: In setTimeout')); + +new Promise((resolve) => { + console.log('Step 3: In promise constructor'); +}).then(() => console.log('Step 4: In then')); + +setTimeout(() => + console.log('Step 5: In another setTimeout') +); + +// Step 1: In global scope +// Step 3: In promise constructor +// Step 4: In then +// Step 2: In setTimeout +// Step 5: In another setTimeout +``` + +!!!tip "Внимание" + + На всякий случай напомню, что коллбэк внутри конструктора промиса является **синхронным**. + +Как мы можем увидеть тут все выводится согласно коду, который мы рассматривали выше. + +- Сначала выполнились все синхронные операции: `Step 1`, `Step 3`; +- Потом выполнились все микротаски: `Step 4`; +- Затем выполнились все макротаски: `Step 2`, `Step 5`; + +Следует заметить что задачи из каждой очереди реализованы по принципу FIFO (First In, First Out) (Первый вошел, первый вышел), именно поэтому в списке макрозадач вывелось `Step 2`, а затем `Step 5`. + +```mermaid +stateDiagram-v2 + Program: Скрипт + SyncTasks: Синхронные задачи + MicroTasks: Микро-задачи + MacroTasks: Макро-задачи + + Program + + state Program { + direction LR + SyncTasks --> MicroTasks + MicroTasks --> MacroTasks + } + + state SyncTasks { + Step1: Step 1 - In global scope + Step3: Step 3 - In Promise constructor + + Step1 --> Step3 + } + + state MicroTasks { + Step4: Step 4 - In then + } + + state MacroTasks { + Step2: Step 2 - In setTimeout + Step5: Step 5 - In another setTimeout + + Step2 --> Step5 + } +``` + +### Два then + +Теперь давайте добавим еще один `then` к нашему промису и посмотрим что случится: + +```js +console.log('Step 1: In global scope'); + +setTimeout(() => console.log('Step 2: In setTimeout')); + +new Promise((resolve) => { + console.log('Step 3: In promise constructor'); +}) + .then(() => console.log('Step 4: In then')) + .then(() => console.log('Step 5: In another then')); + +setTimeout(() => + console.log('Step 6: In another setTimeout') +); + +// Step 1: In global scope +// Step 3: In promise constructor +// Step 4: In then +// Step 5: In another then +// Step 2: In setTimeout +// Step 6: In another setTimeout +``` + +Как мы можем увидеть, особо ничего не поменялось. Просто добавилась еще одна микрозадача, в целом список выводимых строк никак не поменялся, просто после `Step 4` добавился `Step 5`. + +```mermaid +stateDiagram-v2 + Program: Скрипт + SyncTasks: Синхронные задачи + MicroTasks: Микро-задачи + MacroTasks: Макро-задачи + + Program + + state Program { + direction LR + SyncTasks --> MicroTasks + MicroTasks --> MacroTasks + } + + state SyncTasks { + Step1: Step 1 - In global scope + Step3: Step 3 - In Promise constructor + + Step1 --> Step3 + } + + state MicroTasks { + Step4: Step 4 - In then + Step5: Step 5 - In another then + + Step4 --> Step5 + } + + state MacroTasks { + Step2: Step 2 - In setTimeout + Step6: Step 6 - In another setTimeout + + Step2 --> Step6 + } +``` + +### Два `Promise` с двумя `then` + +Теперь давайте попробуем провернуть то же, но уже с двумя промисами: + +```js +console.log('Step 1: In global scope'); + +setTimeout(() => console.log('Step 2: In setTimeout')); + +new Promise((resolve) => { + console.log('Step 3: In promise constructor'); + resolve(); +}) + .then(() => console.log('Step 4: In then')) + .then(() => console.log('Step 5: In another then')); + +setTimeout(() => + console.log('Step 6: In another setTimeout') +); + +new Promise((resolve) => { + console.log('Step 7: In promise constructor'); + resolve(); +}) + .then(() => console.log('Step 8: In then')) + .then(() => console.log('Step 9: In another then')); + +// Step 1: In global scope +// Step 3: In promise constructor +// Step 7: In promise constructor +// Step 4: In then +// Step 8: In then +// Step 5: In another then +// Step 9: In another then +// Step 2: In setTimeout +// Step 6: In another setTimeout +``` + +Тут уже что-то по интереснее, давайте разбираться. Все синхронные задачи выполнились первыми. Затем подтянулись микротаски, но не в обычном порядке: вместо того чтобы выполнить `console.log` в последовательности `Step 4`, `Step 5`, `Step 8`, `Step 9`, он выполнился в последовательности `Step 4`, `Step 8`, `Step 5`, `Step 9`. Сейчас разберемся почему так случилось. + +```mermaid +stateDiagram-v2 + Program: Скрипт + Wrong: ❌ + Right: ✅ + Step4: Step 4 - In then + Step5: Step 5 - In another then + Step8: Step 8 - In then + Step9: Step 9 - In another then + Step44: Step 4 - In then + Step55: Step 5 - In another then + Step88: Step 8 - In then + Step99: Step 9 - In another then + + Program + + state Program { + Wrong + -- + Right + } + + state Wrong { + Step4 --> Step5 + Step5 --> Step8 + Step8 --> Step9 + } + + state Right { + Step44 --> Step88 + Step88 --> Step55 + Step55 --> Step99 + } +``` + +Дело в том, что интерпретатор JavaScript выполняет код шаг за шагом. Он увидел что у нас есть `then` и поместил их в очередь микрозадач, когда микрозадачи отработали - интерпретатор увидел еще один `then` и опять поместил его в очередь микрозадач. Таким образом все `then`, которые были после первых `then` выполнялись в ряд, после того как первичные `then` отработали. + +![Event loop](event-loop1.png) + +### Микрозадача, внутри которой другая микрозадача + +Давайте создадим промис, внутри которого будет объявляться макрозадача. Сделаем это опять же с помощью промисов: + +```js +setTimeout(() => console.log('Step 1: In setTimeout')); + +new Promise((resolve) => { + console.log('Step 2: In promise constructor'); + resolve(); +}).then(() => { + console.log('Step 3: In then'); + setTimeout(() => + console.log( + 'Step 4: In setTimeout (inside of "then")' + ) + ); +}); + +setTimeout(() => + console.log('Step 5: In another setTimeout') +); + +// Step 2: In promise constructor +// Step 3: In then +// Step 1: In setTimeout +// Step 5: In another setTimeout +// Step 4: In setTimeout (inside of "then") +``` + +Тут все достаточно тривиально. Во время выполнения одной из микрозадач была найдена макрозадача. Эта макрозадача была помещена в конец очереди макрозадач. Поэтому `Step 4` вывелся в конце (потому что данная микрозадача была добавлена позже всех). + +# Макрозадача, внутри которой микрозадача + +```js +setTimeout(() => console.log('Step 1: In setTimeout')); +setTimeout(() => { + new Promise((resolve) => { + console.log( + 'Step 2: In promise constructor (inside setTimeout)' + ); + resolve(); + }).then(() => + console.log('Step 3: In then (inside setTimeout)') + ); +}); + +new Promise((resolve) => { + console.log('Step 4: In promise constructor'); + resolve(); +}).then(() => { + console.log('Step 5: In then'); +}); + +setTimeout(() => + console.log('Step 6: In another setTimeout') +); + +// Step 4: In promise constructor +// Step 5: In then +// Step 1: In setTimeout +// Step 2: In promise constructor (inside setTimeout) +// Step 3: In then (inside setTimeout) +// Step 6: In another setTimeout +``` + +Давайте вспомним что макрозадачи выполняются по одной. Перед тем как выполнять каждую следующую задачу Event Loop проверяет нет ли у нас активных задач из Call Stack, а также нет ли микрозадач. + +Как только новая задача находится и Call Stack пустой - она сразу же выполняется. Вот почему `Step 3` выполнился сразу же после `Step 2`. + +![Event Loop](event-loop2.png) diff --git a/docs/articles/event-loop1.png b/docs/articles/event-loop1.png new file mode 100644 index 0000000..94a64c9 Binary files /dev/null and b/docs/articles/event-loop1.png differ diff --git a/docs/articles/event-loop2.png b/docs/articles/event-loop2.png new file mode 100644 index 0000000..1a1c090 Binary files /dev/null and b/docs/articles/event-loop2.png differ diff --git a/docs/articles/game-of-life/.pages b/docs/articles/game-of-life/.pages new file mode 100644 index 0000000..13913f2 --- /dev/null +++ b/docs/articles/game-of-life/.pages @@ -0,0 +1,13 @@ +title: Rust и WebAssembly - Game of Life +nav: + - index.md + - setup.md + - hello-world.md + - rules.md + - implementing.md + - testing.md + - debugging.md + - interactivity.md + - time-profiling.md + - code-size.md + - publishing-to-npm.md diff --git a/docs/articles/game-of-life/bench-perf-annotate.png b/docs/articles/game-of-life/bench-perf-annotate.png new file mode 100644 index 0000000..1a812fa Binary files /dev/null and b/docs/articles/game-of-life/bench-perf-annotate.png differ diff --git a/docs/articles/game-of-life/bench-perf-report.png b/docs/articles/game-of-life/bench-perf-report.png new file mode 100644 index 0000000..8884575 Binary files /dev/null and b/docs/articles/game-of-life/bench-perf-report.png differ diff --git a/docs/articles/game-of-life/code-size.md b/docs/articles/game-of-life/code-size.md new file mode 100644 index 0000000..3a401cf --- /dev/null +++ b/docs/articles/game-of-life/code-size.md @@ -0,0 +1,43 @@ +# Уменьшение размера `.wasm` + +Для `.wasm`-бинарников, которые мы отдаём клиентам по сети — как в нашем веб-приложении «Игра жизни» — важно следить за размером кода. Чем меньше `.wasm`, тем быстрее загружается страница и тем лучше для пользователей. + +## Насколько можно уменьшить `.wasm` «Игры жизни» за счёт настроек сборки? + +[Немного времени на обзор опций сборки, которые можно подкрутить для меньшего размера `.wasm`.](https://rustwasm.github.io/docs/book/reference/code-size.html#optimizing-builds-for-code-size) + +При стандартной конфигурации release-сборки (без отладочных символов) наш WebAssembly-бинарник занимает 29 410 байт: + +``` +$ wc -c pkg/wasm_game_of_life_bg.wasm +29410 pkg/wasm_game_of_life_bg.wasm +``` + +После включения LTO, установки `opt-level = "z"` и запуска `wasm-opt -Oz` итоговый `.wasm` сжимается всего до 17 317 байт: + +``` +$ wc -c pkg/wasm_game_of_life_bg.wasm +17317 pkg/wasm_game_of_life_bg.wasm +``` + +А если сжать его `gzip` (как делает почти каждый HTTP-сервер), получается скромные 9 045 байт! + +``` +$ gzip -9 < pkg/wasm_game_of_life_bg.wasm | wc -c +9045 +``` + +## Упражнения + +- С помощью [утилиты `wasm-snip`](https://rustwasm.github.io/docs/book/reference/code-size.html#use-the-wasm-snip-tool) уберите из `.wasm` «Игры жизни» инфраструктурные функции для паники. Сколько байт это экономит? + +- Соберите крейт «Игра жизни» с [`wee_alloc` в качестве глобального аллокатора](https://github.com/rustwasm/wee_alloc) и без него. В шаблоне `rustwasm/wasm-pack-template`, с которого мы начинали проект, есть cargo-фича `wee_alloc`: её можно включить, добавив её в ключ `default` в секции `[features]` файла `wasm-game-of-life/Cargo.toml`: + + ```toml + [features] + default = ["wee_alloc"] + ``` + + Насколько уменьшается размер `.wasm` при использовании `wee_alloc`? + +- Мы создаём только один экземпляр `Universe`, поэтому вместо конструктора можно экспортировать операции, которые работают с одним глобальным экземпляром `static mut`. Если этот глобальный экземпляр использует двойную буферизацию из предыдущих глав, буферы тоже можно сделать глобальными `static mut`. Тогда в реализации «Игры жизни» не остаётся динамического выделения памяти, и крейт можно сделать `#![no_std]` без аллокатора. Насколько уменьшился `.wasm` после полного отказа от зависимости на аллокатор? diff --git a/docs/articles/game-of-life/console-time-in-profiler.png b/docs/articles/game-of-life/console-time-in-profiler.png new file mode 100644 index 0000000..6c47840 Binary files /dev/null and b/docs/articles/game-of-life/console-time-in-profiler.png differ diff --git a/docs/articles/game-of-life/console-time-in-universe-tick.png b/docs/articles/game-of-life/console-time-in-universe-tick.png new file mode 100644 index 0000000..97bdf06 Binary files /dev/null and b/docs/articles/game-of-life/console-time-in-universe-tick.png differ diff --git a/docs/articles/game-of-life/console-time.png b/docs/articles/game-of-life/console-time.png new file mode 100644 index 0000000..b858093 Binary files /dev/null and b/docs/articles/game-of-life/console-time.png differ diff --git a/docs/articles/game-of-life/debugging.md b/docs/articles/game-of-life/debugging.md new file mode 100644 index 0000000..c99d9c3 --- /dev/null +++ b/docs/articles/game-of-life/debugging.md @@ -0,0 +1,109 @@ +# Отладка + +Прежде чем писать ещё много кода, стоит иметь под рукой средства отладки на случай ошибок. Ознакомьтесь со [справочной страницей: инструменты и подходы к отладке WebAssembly, сгенерированного из Rust](https://rustwasm.github.io/docs/book/reference/debugging.html). + +## Логирование паник + +[При панике в коде в консоли разработчика должны появляться понятные сообщения об ошибке.](https://rustwasm.github.io/docs/book/reference/debugging.html#logging-panics) + +В `wasm-pack-template` по умолчанию подключена опциональная зависимость [крейт `console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook), настройка — в `wasm-game-of-life/src/utils.rs`. Достаточно установить хук в функции инициализации или на общем пути выполнения. Например, вызвать его в конструкторе `Universe::new` в `wasm-game-of-life/src/lib.rs`: + +```rust +pub fn new() -> Universe { + utils::set_panic_hook(); + + // ... +} +``` + +## Добавляем логирование в «Игру жизни» + +[Через `console.log` из крейта `web-sys` можно добавить логирование](https://rustwasm.github.io/docs/book/reference/debugging.html#logging-with-the-console-apis) по каждой ячейке в функции `Universe::tick`. + +Сначала добавьте `web-sys` в зависимости и включите фичу `"console"` в `wasm-game-of-life/Cargo.toml`: + +```toml +[dependencies] + +# ... + +[dependencies.web-sys] +version = "0.3" +features = [ + "console", +] +``` + +Для удобства обернём `console.log` в макрос в духе `println!`: + +```rust +extern crate web_sys; + +// Макрос с синтаксисом в стиле `println!(..)` для логирования через `console.log`. +macro_rules! log { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()); + } +} +``` + +Дальше в коде на Rust можно вставлять вызовы `log!` и писать в консоль. Например, чтобы логировать состояние ячейки, число живых соседей и следующее состояние, измените `wasm-game-of-life/src/lib.rs` так: + +```diff +diff --git a/src/lib.rs b/src/lib.rs +index f757641..a30e107 100755 +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -123,6 +122,14 @@ impl Universe { + let cell = self.cells[idx]; + let live_neighbors = self.live_neighbor_count(row, col); + ++ log!( ++ "ячейка[{}, {}] изначально {:?}, живых соседей: {}", ++ row, ++ col, ++ cell, ++ live_neighbors ++ ); ++ + let next_cell = match (cell, live_neighbors) { + // Правило 1: любая живая клетка с менее чем двумя живыми соседями + // умирает от «недонаселения». +@@ -140,6 +147,8 @@ impl Universe { + (otherwise, _) => otherwise, + }; + ++ log!(" следующее состояние: {:?}", next_cell); ++ + next[idx] = next_cell; + } + } +``` + +## Отладчик с паузой между каждым тиком + +[Пошаговый отладчик в браузере удобен, чтобы смотреть JavaScript, с которым взаимодействует наш WebAssembly из Rust.](https://rustwasm.github.io/docs/book/reference/debugging.html#using-a-debugger) + +Например, можно останавливаться на каждой итерации `renderLoop`, поставив [инструкцию JavaScript `debugger;`](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Statements/debugger) перед вызовом `universe.tick()`. + +```js +const renderLoop = () => { + debugger; + universe.tick(); + + drawGrid(); + drawCells(); + + requestAnimationFrame(renderLoop); +}; +``` + +Так удобно проверять записи в лог и сравнивать текущий кадр с предыдущим. + +![Скриншот отладки «Игры жизни»](./debugging.png) + +## Упражнения + +- Добавьте в `tick` логирование строки и столбца каждой ячейки, у которой состояние перешло из «жива» в «мёртва» или наоборот. + +- Вставьте `panic!()` в метод `Universe::new`. Посмотрите трейс паники в JavaScript-отладчике браузера. Отключите отладочные символы, пересоберите без опциональной зависимости `console_error_panic_hook` и снова посмотрите стек. Без хука картина заметно хуже, не так ли? diff --git a/docs/articles/game-of-life/debugging.png b/docs/articles/game-of-life/debugging.png new file mode 100644 index 0000000..87c4e3d Binary files /dev/null and b/docs/articles/game-of-life/debugging.png differ diff --git a/docs/articles/game-of-life/deploying-to-production.md b/docs/articles/game-of-life/deploying-to-production.md new file mode 100644 index 0000000..305e466 --- /dev/null +++ b/docs/articles/game-of-life/deploying-to-production.md @@ -0,0 +1,35 @@ +# Публикация веб-приложения «Игра жизни» в продакшен + +Когда проект готов, следующий шаг — выложить его на продакшен-сервер вместо локального dev-сервера. С Rust и WebAssembly всё то же, что и в общем случае: файлы должны быть доступны из сети через HTTP-сервер! + +Убедитесь, что wasm-модуль собран актуально: из каталога `wasm-game-of-life` выполните `wasm-pack`: + +``` +wasm-pack build +``` + +Затем соберите JavaScript и HTML через `webpack` в каталоге `wasm-game-of-life/www`: + +``` +./node_modules/.bin/webpack +``` + +В результате собирается всё веб-приложение — бинарник `.wasm`, JavaScript и HTML — и попадает в `wasm-game-of-life/www/dist`. Содержимое может выглядеть примерно так: + +``` +wasm-game-of-life/www/dist/ +├── 0.bootstrap.js +├── 357c6c6c57e15cecdc07.module.wasm +├── bootstrap.js +└── index.html +``` + +Чтобы приложение открывалось с сервера, тот должен отдавать `.wasm` с правильным [MIME-типом](https://developer.mozilla.org/ru/docs/Web/HTTP/Basics_of_HTTP/MIME_types) — `application/wasm`. + +> **Примечание:** настройка сервера зависит от ОС; лучше найти руководство под вашу систему и веб-сервер. Ниже предполагается что-то вроде Debian/Ubuntu или CentOS/Red Hat/Fedora. + +Для nginx [добавьте `application/wasm wasm;` в `/etc/nginx/mime.types`](https://nginx.org/en/docs/http/ngx_http_core_module.html#types). Затем перезагрузите nginx: `sudo nginx -s reload`, чтобы подхватить изменения. + +Для Apache [добавьте `AddType application/wasm .wasm` в корень конфигурации](https://httpd.apache.org/docs/2.4/mod/mod_mime.html#addtype) — часто это `/etc/apache2/apache2.conf` или `/etc/httpd.d/conf/httpd.conf`. Перезагрузите Apache: `sudo apachectl -k graceful`. + +Наконец, можно залить содержимое `dist` на продакшен, например через SCP или SFTP-клиент. Если скопировать файлы в корень сайта (часто `/var/www/html` или `/var/www`), приложение станет доступно извне. diff --git a/docs/articles/game-of-life/drawCells-after-flamegraph.png b/docs/articles/game-of-life/drawCells-after-flamegraph.png new file mode 100644 index 0000000..55c5bba Binary files /dev/null and b/docs/articles/game-of-life/drawCells-after-flamegraph.png differ diff --git a/docs/articles/game-of-life/drawCells-after-waterfall.png b/docs/articles/game-of-life/drawCells-after-waterfall.png new file mode 100644 index 0000000..68436c9 Binary files /dev/null and b/docs/articles/game-of-life/drawCells-after-waterfall.png differ diff --git a/docs/articles/game-of-life/drawCells-before-calltree.png b/docs/articles/game-of-life/drawCells-before-calltree.png new file mode 100644 index 0000000..cad0e87 Binary files /dev/null and b/docs/articles/game-of-life/drawCells-before-calltree.png differ diff --git a/docs/articles/game-of-life/drawCells-before-flamegraph.png b/docs/articles/game-of-life/drawCells-before-flamegraph.png new file mode 100644 index 0000000..b62e018 Binary files /dev/null and b/docs/articles/game-of-life/drawCells-before-flamegraph.png differ diff --git a/docs/articles/game-of-life/drawCells-before-waterfall.png b/docs/articles/game-of-life/drawCells-before-waterfall.png new file mode 100644 index 0000000..9ae91c9 Binary files /dev/null and b/docs/articles/game-of-life/drawCells-before-waterfall.png differ diff --git a/docs/articles/game-of-life/hello-world.md b/docs/articles/game-of-life/hello-world.md new file mode 100644 index 0000000..b894987 --- /dev/null +++ b/docs/articles/game-of-life/hello-world.md @@ -0,0 +1,300 @@ +--- +description: В этом разделе вы узнаете, как создать и запустить свою первую программу на Rust и WebAssembly - веб-страницу, оповещающую Hello, World! +--- + +# Hello, World! + +В этом разделе вы узнаете, как создать и запустить свою первую программу на Rust и WebAssembly: веб-страницу, оповещающую "Hello, World!". + +Перед началом работы убедитесь, что вы выполнили инструкции [setup instructions](./setup.md). + +## Клонирование шаблона проекта + +Шаблон проекта поставляется предварительно сконфигурированным с разумными настройками по умолчанию, чтобы вы могли быстро собрать, интегрировать и упаковать свой код для Web. + +Клонируйте шаблон проекта с помощью этой команды: + +```text +cargo generate --git https://github.com/rustwasm/wasm-pack-template +``` + +Вам будет предложено ввести название нового проекта. Мы будем использовать **"wasm-game-of-life"**. + +```text +wasm-game-of-life +``` + +## Что внутри + +Заходите в новый проект `wasm-game-of-life` + +``` +cd wasm-game-of-life +``` + +и давайте посмотрим на его содержимое: + +```text +wasm-game-of-life/ +├── Cargo.toml +├── LICENSE_APACHE +├── LICENSE_MIT +├── README.md +└── src + ├── lib.rs + └── utils.rs +``` + +Давайте рассмотрим несколько таких файлов подробнее. + +**`wasm-game-of-life/Cargo.toml`** + +Файл `Cargo.toml` определяет зависимости и метаданные для `cargo`, менеджера пакетов и инструмента сборки Rust. Этот файл поставляется с предустановленной зависимостью `wasm-bindgen`, несколькими дополнительными зависимостями, которые мы рассмотрим позже, и правильно инициализированным `crate-type` для генерации библиотек `.wasm`. + +**`wasm-game-of-life/src/lib.rs`** + +Файл `src/lib.rs` является корнем Rust crate, который мы компилируем в WebAssembly. Он использует `wasm-bindgen` для взаимодействия с JavaScript. Он импортирует JavaScript-функцию `window.alert` и экспортирует Rust-функцию `greet`, которая выдает приветствие. + +```rust +mod utils; + +use wasm_bindgen::prelude::*; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern { + fn alert(s: &str); +} + +#[wasm_bindgen] +pub fn greet() { + alert("Hello, wasm-game-of-life!"); +} +``` + +**`wasm-game-of-life/src/utils.rs`** + +Модуль `src/utils.rs` предоставляет общие утилиты для облегчения работы с Rust, скомпилированным в WebAssembly. Мы рассмотрим некоторые из этих утилит более подробно позже в учебнике, например, когда будем рассматривать [отладку нашего wasm-кода](./debugging.md), но пока мы можем игнорировать этот файл. + +## Сборка проекта + +Мы используем `wasm-pack` для организации следующих шагов сборки: + +- Убедитесь, что у нас установлен Rust 1.30 или новее и цель `wasm32-unknown-unknown` через `rustup`, +- Скомпилируйте наши исходные тексты Rust в бинарник WebAssembly `.wasm` с помощью `cargo`, +- Используйте `wasm-bindgen` для генерации JavaScript API для использования нашей Rust-генерированной WebAssembly. + +Чтобы сделать все это, запустите эту команду в каталоге проекта: + +``` +wasm-pack build +``` + +После завершения сборки мы можем найти ее артефакты в директории `pkg`, и она должна иметь следующее содержимое: + +``` +pkg/ +├── package.json +├── README.md +├── wasm_game_of_life_bg.wasm +├── wasm_game_of_life.d.ts +└── wasm_game_of_life.js +``` + +Файл `README.md` скопирован из основного проекта, но все остальные - совершенно новые. + +**`wasm-game-of-life/pkg/wasm_game_of_life_bg.wasm`** + +Файл `.wasm` - это двоичный файл WebAssembly, который генерируется компилятором Rust из наших исходных текстов Rust. Он содержит скомпилированные в wasm версии всех наших функций и данных Rust. Например, в нем есть экспортируемая функция "greet". + +**`wasm-game-of-life/pkg/wasm_game_of_life.js`** + +Файл `.js` генерируется `wasm-bindgen` и содержит JavaScript-клеи для импорта DOM и JavaScript-функций в Rust и раскрытия красивого API для функций WebAssembly в JavaScript. Например, есть функция JavaScript `greet`, которая оборачивает функцию `greet`, экспортированную из модуля WebAssembly. Сейчас этот клей не делает многого, но когда мы начнем передавать более интересные значения туда и обратно между wasm и JavaScript, он поможет переправить эти значения через границу. + +```js +import * as wasm from './wasm_game_of_life_bg'; + +// ... + +export function greet() { + return wasm.greet(); +} +``` + +**`wasm-game-of-life/pkg/wasm_game_of_life.d.ts`** + +Файл `.d.ts` содержит объявления типов [TypeScript](http://www.typescriptlang.org/) для JavaScript-клея. Если вы используете TypeScript, вы можете проверять типы ваших вызовов функций WebAssembly, а ваша IDE может предоставить автозавершение и предложения! Если вы не используете TypeScript, можете смело игнорировать этот файл. + +```typescript +export function greet(): void; +``` + +**`wasm-game-of-life/pkg/package.json`** + +[Файл `package.json` содержит метаданные о сгенерированном пакете JavaScript и WebAssembly.](https://docs.npmjs.com/files/package.json) Он используется npm и JavaScript bundlers для определения зависимостей между пакетами, имен пакетов, версий и множества других вещей. Он помогает нам интегрироваться с инструментами JavaScript и позволяет опубликовать наш пакет в npm. + +```json +{ + "name": "wasm-game-of-life", + "collaborators": ["Your Name "], + "description": null, + "version": "0.1.0", + "license": null, + "repository": null, + "files": [ + "wasm_game_of_life_bg.wasm", + "wasm_game_of_life.d.ts" + ], + "main": "wasm_game_of_life.js", + "types": "wasm_game_of_life.d.ts" +} +``` + +## Вставка в веб-страницу + +Чтобы взять наш пакет `wasm-game-of-life` и использовать его в веб-странице, мы используем [шаблон проекта JavaScript `create-wasm-app`](https://github.com/rustwasm/create-wasm-app). + +Выполните эту команду в директории `wasm-game-of-life`: + +``` +npm init wasm-app www +``` + +Вот что содержит наш новый подкаталог `wasm-game-of-life/www`: + +``` +wasm-game-of-life/www/ +├── bootstrap.js +├── index.html +├── index.js +├── LICENSE-APACHE +├── LICENSE-MIT +├── package.json +├── README.md +└── webpack.config.js +``` + +Давайте еще раз внимательно рассмотрим некоторые из этих файлов. + +**`wasm-game-of-life/www/package.json`** + +Этот `package.json` поставляется с предустановленными зависимостями `webpack` и `webpack-dev-server`, а также зависимостью от `hello-wasm-pack`, которая представляет собой версию начального пакета `wasm-pack-template`, опубликованного на npm. + +**`wasm-game-of-life/www/webpack.config.js`** + +Этот файл настраивает webpack и его локальный сервер разработки. Он поставляется уже настроенным, и вам не придется вносить в него какие-либо изменения, чтобы заставить webpack и его локальный сервер разработки работать. + +**`wasm-game-of-life/www/index.html`** + +Это корневой HTML-файл для веб-страницы. Он не делает ничего особенного, кроме загрузки `bootstrap.js`, который является очень тонкой оберткой вокруг `index.js`. + +```html + + + + + Hello wasm-pack! + + + + + +``` + +**`wasm-game-of-life/www/index.js`** + +Сайт `index.js` - это основная точка входа для JavaScript нашей веб-страницы. Он импортирует пакет npm `hello-wasm-pack`, который содержит скомпилированный WebAssembly и JavaScript-клея по умолчанию `wasm-pack-template`, а затем вызывает функцию `hello-wasm-pack` `greet`. + +```js +import * as wasm from 'hello-wasm-pack'; + +wasm.greet(); +``` + +### Установите зависимости + +Сначала убедитесь, что локальный сервер разработки и его зависимости установлены, запустив `npm install` в подкаталоге `wasm-game-of-life/www`: + +```text +npm install +``` + +Эту команду нужно выполнить только один раз, и она установит пакет JavaScript `webpack` и его сервер разработки. + +> Обратите внимание, что `webpack` не является обязательным для работы с Rust и WebAssembly, это просто бандлер и сервер разработки, который мы выбрали для удобства. Parcel и Rollup также должны поддерживать импорт WebAssembly как модулей ECMAScript. Вы также можете использовать Rust и WebAssembly [без бандлера](https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html), если хотите! + +### Использование нашего локального пакета `wasm-game-of-life` в `www` + +Вместо того чтобы использовать пакет `hello-wasm-pack` из npm, мы хотим использовать наш локальный пакет `wasm-game-of-life`. Это позволит нам постепенно развивать нашу программу Game of Life. + +Откройте `wasm-game-of-life/www/package.json` и рядом с `"devDependencies"` добавьте поле `"dependencies"`, включая `"wasm-game-of-life": "file:../pkg"`: + +```js +{ + // ... + "dependencies": { // Add this three lines block! + "wasm-game-of-life": "file:../pkg" + }, + "devDependencies": { + //... + } +} +``` + +Затем измените `wasm-game-of-life/www/index.js`, чтобы импортировать `wasm-game-of-life` вместо пакета `hello-wasm-pack`: + +```js +import * as wasm from 'wasm-game-of-life'; + +wasm.greet(); +``` + +Поскольку мы объявили новую зависимость, нам нужно ее установить: + +```text +npm install +``` + +Наша веб-страница теперь готова к местному обслуживанию! + +## Обслуживание локально + +Далее откройте новый терминал для сервера разработки. Запуск сервера в новом терминале позволяет нам оставить его работать в фоновом режиме и не мешает нам выполнять другие команды в это время. В новом терминале выполните эту команду из директории `wasm-game-of-life/www`: + +``` +npm run start +``` + +Перейдите в веб-браузере по адресу [http://localhost:8080/](http://localhost:8080/), и вас должно встретить сообщение с предупреждением: + +![Скриншот оповещения веб-страницы "Привет, wasm-game-of-life!"](hello-world.png) + +Каждый раз, когда вы вносите изменения и хотите, чтобы они отразились на [http://localhost:8080/](http://localhost:8080/), просто повторно выполните команду `wasm-pack build` в директории `wasm-game-of-life`. + +## Упражнения + +Измените функцию `greet` в `wasm-game-of-life/src/lib.rs`, чтобы она принимала параметр `name: &str`, который настраивает сообщение с предупреждением, и передайте свое имя в функцию `greet` из каталога `wasm-game-of-life/www/index.js`. Пересоберите бинарный файл `.wasm` с помощью `wasm-pack build`, затем обновите [http://localhost:8080/](http://localhost:8080/) в веб-браузере, и вы увидите настроенное приветствие! + +!!!note "Answer" + + New version of the `greet` function in `wasm-game-of-life/src/lib.rs`: + + ```rust + #[wasm_bindgen] + pub fn greet(name: &str) { + alert(&format!("Hello, {}!", name)); + } + ``` + + New invocation of `greet` in `wasm-game-of-life/www/index.js`: + + ```js + wasm.greet('Your Name'); + ``` + +:material-information-outline: Источник — diff --git a/docs/articles/game-of-life/hello-world.png b/docs/articles/game-of-life/hello-world.png new file mode 100644 index 0000000..1d5f6d9 Binary files /dev/null and b/docs/articles/game-of-life/hello-world.png differ diff --git a/docs/articles/game-of-life/implementing.md b/docs/articles/game-of-life/implementing.md new file mode 100644 index 0000000..d62e9a6 --- /dev/null +++ b/docs/articles/game-of-life/implementing.md @@ -0,0 +1,622 @@ +# Реализация «Игры жизни» Конвея + +## Проектирование + +Прежде чем углубляться в код, нужно рассмотреть несколько проектных решений. + +### Бесконечная вселенная + +«Игра жизни» работает в бесконечной вселенной, но у нас нет бесконечной памяти и вычислительных ресурсов. Обход этого довольно неприятного ограничения обычно сводится к одному из трех вариантов: + +1. Отслеживать, в каком подмножестве вселенной происходит что-то интересное, и расширять эту область по мере необходимости. В худшем случае расширение становится неограниченным, реализация работает все медленнее и в итоге расходует всю память. +2. Создать вселенную фиксированного размера, где клетки на границах имеют меньше соседей, чем клетки в середине. Минус такого подхода в том, что бесконечные паттерны (например, «планеры»), достигнув края, затухают. +3. Создать периодическую вселенную фиксированного размера, где клетки на границах имеют соседей, «заворачивающихся» на противоположную сторону. Поскольку соседи переходят через края, планеры могут двигаться бесконечно. + +Мы реализуем третий вариант. + +### Взаимодействие Rust и JavaScript + +> ⚡ Это одна из самых важных идей, которую стоит понять и вынести из этого руководства! + +Куча JavaScript с автоматической сборкой мусора — где выделяются `Object`, `Array` и узлы DOM — отделена от линейной памяти WebAssembly, где живут наши значения Rust. Сейчас у WebAssembly нет прямого доступа к куче со сборкой мусора (по состоянию на апрель 2018 года ожидается, что это изменится благодаря предложению [«Interface Types»](https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md)). JavaScript, в свою очередь, может читать и писать в линейную память WebAssembly, но только как [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) скалярных значений (`u8`, `i32`, `f64` и т.д.). Функции WebAssembly также принимают и возвращают скалярные значения. Именно это — базовые строительные блоки всего обмена данными между WebAssembly и JavaScript. + +`wasm_bindgen` задает общее понимание того, как работать со сложными структурами по обе стороны этой границы. Это включает размещение структур Rust в куче с последующей оберткой указателя в JavaScript-класс для удобства, либо индексацию в таблице объектов JavaScript из Rust. `wasm_bindgen` очень удобен, но он не снимает необходимость продумывать представление данных, а также какие значения и структуры передаются через границу. Считайте его инструментом для реализации выбранного вами дизайна интерфейса. + +При проектировании интерфейса между WebAssembly и JavaScript мы хотим оптимизировать следующие свойства: + +1. **Минимизировать копирование в линейную память WebAssembly и обратно.** Лишние копии дают лишние накладные расходы. +2. **Минимизировать сериализацию и десериализацию.** Как и копирование, они создают накладные расходы и часто тоже требуют копий. Если можно передавать непрозрачные дескрипторы структуры данных — вместо сериализации на одной стороне, копирования в известное место линейной памяти WebAssembly и десериализации на другой стороне — это часто значительно снижает издержки. `wasm_bindgen` помогает определять и использовать непрозрачные дескрипторы JavaScript-`Object` или упакованных структур Rust. + +Эмпирическое правило: хороший дизайн интерфейса JavaScript↔WebAssembly часто выглядит так — большие и долгоживущие структуры данных реализованы как типы Rust, живущие в линейной памяти WebAssembly, и предоставляются JavaScript как непрозрачные дескрипторы. JavaScript вызывает экспортированные функции WebAssembly, которые принимают эти дескрипторы, преобразуют данные, выполняют тяжелые вычисления, запрашивают данные и в итоге возвращают небольшой результат, который легко копировать. Возвращая только небольшой итог вычисления, мы избегаем копирования и/или сериализации больших структур туда-обратно между кучей JavaScript и линейной памятью WebAssembly. + +### Взаимодействие Rust и JavaScript в нашей «Игре жизни» + +Начнем с перечисления рисков, которых нужно избегать. Мы не хотим на каждом тике копировать всю вселенную в линейную память WebAssembly и обратно. Мы не хотим выделять объекты для каждой клетки вселенной и не хотим вызывать межъязыковой вызов для чтения и записи каждой клетки. + +К чему это нас приводит? Мы можем представить вселенную как плоский массив в линейной памяти WebAssembly, где на каждую клетку приходится один байт. `0` — мертвая клетка, `1` — живая клетка. + +Вот как выглядит в памяти вселенная размером 4 на 4: + +![Скриншот вселенной 4 на 4](./universe.png) + +Чтобы найти индекс массива для клетки по заданным строке и столбцу, можно использовать такую формулу: + +```text +index(row, column, universe) = row * width(universe) + column +``` + +Есть несколько способов предоставить клетки вселенной в JavaScript. Для начала реализуем [`std::fmt::Display`](https://doc.rust-lang.org/1.25.0/std/fmt/trait.Display.html) для `Universe`, чтобы получать Rust-строку `String`, где клетки отрисованы текстовыми символами. Затем эта Rust-строка копируется из линейной памяти WebAssembly в строку JavaScript в куче JavaScript и выводится через установку HTML-свойства `textContent`. Позже в этой главе мы улучшим реализацию, чтобы не копировать клетки между кучами и рисовать прямо в ``. + +_Еще один рабочий вариант дизайна — чтобы Rust после каждого тика возвращал список только тех клеток, которые сменили состояние, вместо передачи всей вселенной в JavaScript. Тогда JavaScript при рендеринге проходил бы не по всей вселенной, а только по релевантному подмножеству. Компромисс в том, что такой дельта-подход немного сложнее в реализации._ + +## Реализация на Rust + +В прошлой главе мы клонировали стартовый шаблон проекта. Сейчас мы его изменим. + +Начнем с удаления импорта `alert` и функции `greet` из `wasm-game-of-life/src/lib.rs`, заменив их определением типа для клеток: + +```rust +#[wasm_bindgen] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Cell { + Dead = 0, + Alive = 1, +} +``` + +Важно иметь `#[repr(u8)]`, чтобы каждая клетка представлялась одним байтом. Также важно, чтобы вариант `Dead` был равен `0`, а вариант `Alive` — `1`, чтобы можно было просто считать число живых соседей сложением. + +Далее определим вселенную. У вселенной есть ширина, высота и вектор клеток длиной `width * height`. + +```rust +#[wasm_bindgen] +pub struct Universe { + width: u32, + height: u32, + cells: Vec, +} +``` + +Чтобы получить клетку по строке и столбцу, переводим их в индекс вектора `cells`, как описано ранее: + +```rust +impl Universe { + fn get_index(&self, row: u32, column: u32) -> usize { + (row * self.width + column) as usize + } + + // ... +} +``` + +Чтобы вычислить следующее состояние клетки, нужно посчитать, сколько ее соседей живы. Напишем для этого метод `live_neighbor_count`! + +```rust +impl Universe { + // ... + + fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { + let mut count = 0; + for delta_row in [self.height - 1, 0, 1].iter().cloned() { + for delta_col in [self.width - 1, 0, 1].iter().cloned() { + if delta_row == 0 && delta_col == 0 { + continue; + } + + let neighbor_row = (row + delta_row) % self.height; + let neighbor_col = (column + delta_col) % self.width; + let idx = self.get_index(neighbor_row, neighbor_col); + count += self.cells[idx] as u8; + } + } + count + } +} +``` + +Метод `live_neighbor_count` использует смещения и операцию modulo, чтобы не писать специальные `if` для краев вселенной. При смещении `-1` мы _прибавляем_ `self.height - 1` и позволяем modulo сделать свою работу вместо вычитания `1`. `row` и `column` могут быть равны `0`, и если бы мы попытались вычесть `1`, получили бы underflow беззнакового целого. + +Теперь у нас есть все, чтобы вычислить следующее поколение из текущего! Каждое правило игры прямо переводится в условие внутри выражения `match`. Дополнительно, поскольку мы хотим, чтобы момент тика контролировался из JavaScript, поместим этот метод внутрь блока `#[wasm_bindgen]`, чтобы он экспортировался в JavaScript. + +```rust +/// Public methods, exported to JavaScript. +#[wasm_bindgen] +impl Universe { + pub fn tick(&mut self) { + let mut next = self.cells.clone(); + + for row in 0..self.height { + for col in 0..self.width { + let idx = self.get_index(row, col); + let cell = self.cells[idx]; + let live_neighbors = self.live_neighbor_count(row, col); + + let next_cell = match (cell, live_neighbors) { + // Rule 1: Any live cell with fewer than two live neighbours + // dies, as if caused by underpopulation. + (Cell::Alive, x) if x < 2 => Cell::Dead, + // Rule 2: Any live cell with two or three live neighbours + // lives on to the next generation. + (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive, + // Rule 3: Any live cell with more than three live + // neighbours dies, as if by overpopulation. + (Cell::Alive, x) if x > 3 => Cell::Dead, + // Rule 4: Any dead cell with exactly three live neighbours + // becomes a live cell, as if by reproduction. + (Cell::Dead, 3) => Cell::Alive, + // All other cells remain in the same state. + (otherwise, _) => otherwise, + }; + + next[idx] = next_cell; + } + } + + self.cells = next; + } + + // ... +} +``` + +Пока состояние вселенной представлено вектором клеток. Чтобы сделать его удобным для чтения человеком, реализуем простейший текстовый рендерер. Идея в том, чтобы построчно выводить вселенную как текст: для живой клетки печатать Unicode-символ `◼` («черный средний квадрат»), для мертвой — `◻` («белый средний квадрат»). + +Реализовав трейт [`Display`](https://doc.rust-lang.org/1.25.0/std/fmt/trait.Display.html) из стандартной библиотеки Rust, мы добавим способ форматировать структуру в пользовательском виде. Это также автоматически даст нам метод [`to_string`](https://doc.rust-lang.org/1.25.0/std/string/trait.ToString.html). + +```rust +use std::fmt; + +impl fmt::Display for Universe { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for line in self.cells.as_slice().chunks(self.width as usize) { + for &cell in line { + let symbol = if cell == Cell::Dead { '◻' } else { '◼' }; + write!(f, "{}", symbol)?; + } + write!(f, "\n")?; + } + + Ok(()) + } +} +``` + +Наконец, определим конструктор, который инициализирует вселенную интересным паттерном живых и мертвых клеток, а также метод `render`: + +```rust +/// Public methods, exported to JavaScript. +#[wasm_bindgen] +impl Universe { + // ... + + pub fn new() -> Universe { + let width = 64; + let height = 64; + + let cells = (0..width * height) + .map(|i| { + if i % 2 == 0 || i % 7 == 0 { + Cell::Alive + } else { + Cell::Dead + } + }) + .collect(); + + Universe { + width, + height, + cells, + } + } + + pub fn render(&self) -> String { + self.to_string() + } +} +``` + +На этом Rust-часть реализации нашей «Игры жизни» готова! + +Пересоберите ее в WebAssembly, выполнив `wasm-pack build` в директории `wasm-game-of-life`. + +## Рендеринг с JavaScript + +Сначала добавим элемент `
` в `wasm-game-of-life/www/index.html`, в который будем рисовать вселенную, прямо над тегом `
+
+```
+
+Кроме того, нам нужно центрировать `
` посередине веб-страницы. Для этого можно использовать CSS flexbox. Добавьте следующий тег `
+```
+
+В начале `wasm-game-of-life/www/index.js` поправим импорт: вместо старой функции `greet` нужно импортировать `Universe`:
+
+```js
+import { Universe } from 'wasm-game-of-life';
+```
+
+Также получим добавленный элемент `
` и создадим новую вселенную:
+
+```js
+const pre = document.getElementById('game-of-life-canvas');
+const universe = Universe.new();
+```
+
+JavaScript выполняется [в цикле `requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). На каждой итерации он рисует текущее состояние вселенной в `
`, а затем вызывает `Universe::tick`.
+
+```js
+const renderLoop = () => {
+    pre.textContent = universe.render();
+    universe.tick();
+
+    requestAnimationFrame(renderLoop);
+};
+```
+
+Чтобы запустить процесс рендеринга, достаточно сделать начальный вызов для первой итерации цикла:
+
+```js
+requestAnimationFrame(renderLoop);
+```
+
+Убедитесь, что сервер разработки все еще запущен (выполните `npm run start` в `wasm-game-of-life/www`), и вот так должна выглядеть страница [http://localhost:8080/](http://localhost:8080/):
+
+[Скриншот реализации «Игры жизни» с текстовым рендерингом](./initial-game-of-life-pre.png)
+
+## Рендеринг в Canvas напрямую из памяти
+
+Генерация (и выделение памяти под) `String` в Rust, а затем преобразование ее в валидную строку JavaScript через `wasm-bindgen` приводит к лишним копиям клеток вселенной. Поскольку код JavaScript уже знает ширину и высоту вселенной и может напрямую читать линейную память WebAssembly, где лежат клетки, мы изменим метод `render`, чтобы он возвращал указатель на начало массива клеток.
+
+Также вместо рендеринга Unicode-текста перейдем к использованию [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API). Эту схему мы будем использовать в оставшейся части руководства.
+
+В `wasm-game-of-life/www/index.html` заменим добавленный ранее `
` на ``, в который будем рисовать (он тоже должен быть внутри ``, перед `
+
+```
+
+Чтобы получить нужные данные из реализации на Rust, добавим несколько геттеров: ширина, высота вселенной и указатель на массив ее клеток. Все они также экспортируются в JavaScript. Внесите эти изменения в `wasm-game-of-life/src/lib.rs`:
+
+```rust
+/// Public methods, exported to JavaScript.
+#[wasm_bindgen]
+impl Universe {
+    // ...
+
+    pub fn width(&self) -> u32 {
+        self.width
+    }
+
+    pub fn height(&self) -> u32 {
+        self.height
+    }
+
+    pub fn cells(&self) -> *const Cell {
+        self.cells.as_ptr()
+    }
+}
+```
+
+Далее в `wasm-game-of-life/www/index.js` также импортируем `Cell` из `wasm-game-of-life` и определим несколько констант, которые будем использовать при рендеринге в canvas:
+
+```js
+import { Universe, Cell } from 'wasm-game-of-life';
+
+const CELL_SIZE = 5; // px
+const GRID_COLOR = '#CCCCCC';
+const DEAD_COLOR = '#FFFFFF';
+const ALIVE_COLOR = '#000000';
+```
+
+Теперь перепишем остальную часть JavaScript-кода: вместо записи в `textContent` у `
` будем рисовать в ``:
+
+```js
+// Construct the universe, and get its width and height.
+const universe = Universe.new();
+const width = universe.width();
+const height = universe.height();
+
+// Give the canvas room for all of our cells and a 1px border
+// around each of them.
+const canvas = document.getElementById(
+    'game-of-life-canvas'
+);
+canvas.height = (CELL_SIZE + 1) * height + 1;
+canvas.width = (CELL_SIZE + 1) * width + 1;
+
+const ctx = canvas.getContext('2d');
+
+const renderLoop = () => {
+    universe.tick();
+
+    drawGrid();
+    drawCells();
+
+    requestAnimationFrame(renderLoop);
+};
+```
+
+Чтобы нарисовать сетку между клетками, рисуем набор горизонтальных линий с равным шагом и набор вертикальных линий с равным шагом. Пересекаясь, они образуют сетку.
+
+```js
+const drawGrid = () => {
+    ctx.beginPath();
+    ctx.strokeStyle = GRID_COLOR;
+
+    // Vertical lines.
+    for (let i = 0; i <= width; i++) {
+        ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
+        ctx.lineTo(
+            i * (CELL_SIZE + 1) + 1,
+            (CELL_SIZE + 1) * height + 1
+        );
+    }
+
+    // Horizontal lines.
+    for (let j = 0; j <= height; j++) {
+        ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
+        ctx.lineTo(
+            (CELL_SIZE + 1) * width + 1,
+            j * (CELL_SIZE + 1) + 1
+        );
+    }
+
+    ctx.stroke();
+};
+```
+
+Мы можем напрямую обращаться к линейной памяти WebAssembly через `memory`, которая определена в сыром wasm-модуле `wasm_game_of_life_bg`. Чтобы рисовать клетки, получаем указатель на массив клеток вселенной, создаем `Uint8Array`, накладывающийся на буфер клеток, проходим по каждой клетке и рисуем белый или черный прямоугольник в зависимости от того, мертва клетка или жива. За счет работы с указателями и overlay-массивом мы избегаем копирования клеток через границу на каждом тике.
+
+```js
+// Import the WebAssembly memory at the top of the file.
+import { memory } from 'wasm-game-of-life/wasm_game_of_life_bg';
+
+// ...
+
+const getIndex = (row, column) => {
+    return row * width + column;
+};
+
+const drawCells = () => {
+    const cellsPtr = universe.cells();
+    const cells = new Uint8Array(
+        memory.buffer,
+        cellsPtr,
+        width * height
+    );
+
+    ctx.beginPath();
+
+    for (let row = 0; row < height; row++) {
+        for (let col = 0; col < width; col++) {
+            const idx = getIndex(row, col);
+
+            ctx.fillStyle =
+                cells[idx] === Cell.Dead
+                    ? DEAD_COLOR
+                    : ALIVE_COLOR;
+
+            ctx.fillRect(
+                col * (CELL_SIZE + 1) + 1,
+                row * (CELL_SIZE + 1) + 1,
+                CELL_SIZE,
+                CELL_SIZE
+            );
+        }
+    }
+
+    ctx.stroke();
+};
+```
+
+Чтобы запустить рендеринг, используем тот же код, что и выше, для старта первой итерации цикла:
+
+```js
+drawGrid();
+drawCells();
+requestAnimationFrame(renderLoop);
+```
+
+Обратите внимание: здесь мы вызываем `drawGrid()` и `drawCells()` _до_ `requestAnimationFrame()`. Это сделано для того, чтобы _начальное_ состояние вселенной было нарисовано до внесения изменений. Если бы мы просто вызвали `requestAnimationFrame(renderLoop)`, первый отрисованный кадр оказался бы уже _после_ первого вызова `universe.tick()`, то есть фактически соответствовал бы второму «тику» жизни этих клеток.
+
+## Работает!
+
+Пересоберите WebAssembly и «клей» биндингов, выполнив эту команду из корневой директории `wasm-game-of-life`:
+
+```
+wasm-pack build
+```
+
+Убедитесь, что сервер разработки все еще запущен. Если нет — запустите его снова из директории `wasm-game-of-life/www`:
+
+```
+npm run start
+```
+
+Если обновить [http://localhost:8080/](http://localhost:8080/), вы увидите впечатляющую демонстрацию жизни!
+
+[Скриншот реализации «Игры жизни»](./initial-game-of-life.png)
+
+К слову, существует еще очень интересный алгоритм реализации «Игры жизни» под названием [hashlife](https://en.wikipedia.org/wiki/Hashlife). Он активно использует мемоизацию и со временем может вычислять будущие поколения _экспоненциально быстрее_! Возможно, вы задаетесь вопросом, почему мы не реализовали hashlife в этом руководстве. Это выходит за рамки текущего текста, где мы сосредоточены на интеграции Rust и WebAssembly, но мы настоятельно рекомендуем изучить hashlife самостоятельно.
+
+## Упражнения
+
+-   Инициализируйте вселенную одним космическим кораблем.
+
+-   Вместо жестко заданной начальной вселенной сгенерируйте случайную, где каждая клетка с вероятностью 50/50 будет живой или мертвой.
+
+    _Подсказка: используйте [crate `js-sys`](https://crates.io/crates/js-sys), чтобы импортировать [JavaScript-функцию `Math.random`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random)._
+
+    ???question "Ответ"
+
+        _Сначала добавьте `js-sys` как зависимость в `wasm-game-of-life/Cargo.toml`:_
+
+        ```toml
+        # ...
+        [dependencies]
+        js-sys = "0.3"
+        # ...
+        ```
+
+        _Затем используйте функцию `js_sys::Math::random`, чтобы «подбросить монетку»:_
+
+        ```rust
+        extern crate js_sys;
+
+        // ...
+
+        if js_sys::Math::random() < 0.5 {
+        	// Alive...
+        } else {
+        	// Dead...
+        }
+        ```
+
+-   Представление каждой клетки одним байтом упрощает обход клеток, но расходует лишнюю память. В каждом байте восемь бит, а нам нужен только один бит, чтобы хранить состояние клетки (жива или мертва). Переделайте представление данных так, чтобы каждая клетка занимала только один бит.
+
+    ???question "Ответ"
+
+        В Rust можно использовать [crate `fixedbitset` и тип `FixedBitSet`](https://crates.io/crates/fixedbitset), чтобы представлять клетки вместо `Vec`:
+
+        ```rust
+        // Make sure you also added the dependency to Cargo.toml!
+        extern crate fixedbitset;
+        use fixedbitset::FixedBitSet;
+
+        // ...
+
+        #[wasm_bindgen]
+        pub struct Universe {
+        	width: u32,
+        	height: u32,
+        	cells: FixedBitSet,
+        }
+        ```
+
+        Конструктор `Universe` можно изменить следующим образом:
+
+        ```rust
+        pub fn new() -> Universe {
+        	let width = 64;
+        	let height = 64;
+
+        	let size = (width * height) as usize;
+        	let mut cells = FixedBitSet::with_capacity(size);
+
+        	for i in 0..size {
+        		cells.set(i, i % 2 == 0 || i % 7 == 0);
+        	}
+
+        	Universe {
+        		width,
+        		height,
+        		cells,
+        	}
+        }
+        ```
+
+        Чтобы обновить клетку на следующем тике вселенной, используем метод `set` у `FixedBitSet`:
+
+        ```rust
+        next.set(idx, match (cell, live_neighbors) {
+        	(true, x) if x < 2 => false,
+        	(true, 2) | (true, 3) => true,
+        	(true, x) if x > 3 => false,
+        	(false, 3) => true,
+        	(otherwise, _) => otherwise
+        });
+        ```
+
+        Чтобы передать указатель на начало битов в JavaScript, можно преобразовать `FixedBitSet` в slice, а затем преобразовать slice в указатель:
+
+        ```rust
+        #[wasm_bindgen]
+        impl Universe {
+        	// ...
+
+        	pub fn cells(&self) -> *const u32 {
+        		self.cells.as_slice().as_ptr()
+        	}
+        }
+        ```
+
+        В JavaScript создание `Uint8Array` из памяти Wasm выглядит так же, как и раньше, кроме того, что длина массива теперь не `width * height`, а `width * height / 8`, так как теперь у нас одна клетка на бит, а не на байт:
+
+        ```js
+        const cells = new Uint8Array(
+        	memory.buffer,
+        	cellsPtr,
+        	(width * height) / 8
+        );
+        ```
+
+        Имея индекс и `Uint8Array`, можно определить, установлен ли _nth_ бит, с помощью такой функции:
+
+        ```js
+        const bitIsSet = (n, arr) => {
+        	const byte = Math.floor(n / 8);
+        	const mask = 1 << n % 8;
+        	return (arr[byte] & mask) === mask;
+        };
+        ```
+
+        С учетом всего вышесказанного новая версия `drawCells` выглядит так:
+
+        ```js
+        const drawCells = () => {
+        	const cellsPtr = universe.cells();
+
+        	// This is updated!
+        	const cells = new Uint8Array(
+        		memory.buffer,
+        		cellsPtr,
+        		(width * height) / 8
+        	);
+
+        	ctx.beginPath();
+
+        	for (let row = 0; row < height; row++) {
+        		for (let col = 0; col < width; col++) {
+        			const idx = getIndex(row, col);
+
+        			// This is updated!
+        			ctx.fillStyle = bitIsSet(idx, cells)
+        				? ALIVE_COLOR
+        				: DEAD_COLOR;
+
+        			ctx.fillRect(
+        				col * (CELL_SIZE + 1) + 1,
+        				row * (CELL_SIZE + 1) + 1,
+        				CELL_SIZE,
+        				CELL_SIZE
+        			);
+        		}
+        	}
+
+        	ctx.stroke();
+        };
+        ```
diff --git a/docs/articles/game-of-life/index.md b/docs/articles/game-of-life/index.md
new file mode 100644
index 0000000..ffa5930
--- /dev/null
+++ b/docs/articles/game-of-life/index.md
@@ -0,0 +1,29 @@
+---
+description: Это учебное пособие по реализации Conway's Game of Life на Rust и WebAssembly
+---
+
+# Учебник: Игра жизни Конвея
+
+Это учебное пособие по реализации [Conway's Game of Life](https://ru.wikipedia.org/wiki/Игра_«Жизнь») на Rust и WebAssembly.
+
+## Для кого этот учебник?
+
+Этот учебник предназначен для тех, кто уже имеет базовый опыт работы с Rust и JavaScript и хочет научиться использовать Rust, WebAssembly и JavaScript вместе.
+
+Вам должно быть удобно читать и писать на Rust, JavaScript и HTML. Вам определенно не нужно быть экспертом.
+
+## Чему я научусь?
+
+-   Как настроить инструментарий Rust для компиляции в WebAssembly.
+
+-   Рабочий процесс разработки полиглот-программ из Rust, WebAssembly, JavaScript, HTML и CSS.
+
+-   Как разрабатывать API, чтобы максимально использовать сильные стороны Rust и WebAssembly, а также сильные стороны JavaScript.
+
+-   Как отлаживать модули WebAssembly, скомпилированные из Rust.
+
+-   Как составить временной профиль программ на Rust и WebAssembly, чтобы сделать их быстрее.
+
+-   Как определять размер программ на Rust и WebAssembly, чтобы сделать двоичные файлы `.wasm` меньше и быстрее загружались по сети.
+
+:material-information-outline: Источник — 
diff --git a/docs/articles/game-of-life/initial-game-of-life-pre.png b/docs/articles/game-of-life/initial-game-of-life-pre.png
new file mode 100644
index 0000000..df50cf8
Binary files /dev/null and b/docs/articles/game-of-life/initial-game-of-life-pre.png differ
diff --git a/docs/articles/game-of-life/initial-game-of-life.png b/docs/articles/game-of-life/initial-game-of-life.png
new file mode 100644
index 0000000..9adf799
Binary files /dev/null and b/docs/articles/game-of-life/initial-game-of-life.png differ
diff --git a/docs/articles/game-of-life/initial-universe.png b/docs/articles/game-of-life/initial-universe.png
new file mode 100644
index 0000000..e5a349b
Binary files /dev/null and b/docs/articles/game-of-life/initial-universe.png differ
diff --git a/docs/articles/game-of-life/interactivity.md b/docs/articles/game-of-life/interactivity.md
new file mode 100644
index 0000000..8fdb356
--- /dev/null
+++ b/docs/articles/game-of-life/interactivity.md
@@ -0,0 +1,155 @@
+---
+description: Интерактивность в игре жизни — пауза, клик по клеткам и связь JavaScript с WebAssembly
+---
+
+# Добавление интерактивности
+
+Продолжим разбирать связку JavaScript и WebAssembly: добавим в реализацию игры жизни интерактив. Пользователь сможет переключать клетку «жива/мёртва» по клику и ставить игру на паузу — так удобнее рисовать паттерны.
+
+## Пауза и возобновление игры
+
+Добавим кнопку, переключающую режим «играет / на паузе». В файле `wasm-game-of-life/www/index.html` вставьте кнопку сразу над ``:
+
+```html
+
+```
+
+В JavaScript `wasm-game-of-life/www/index.js` сделайте следующее:
+
+-   Храните идентификатор, который вернул последний вызов `requestAnimationFrame`, чтобы отменить анимацию через `cancelAnimationFrame` с этим идентификатором.
+
+-   По клику на «play/pause» проверяйте, есть ли идентификатор запланированного кадра анимации. Если есть — игра идёт, нужно отменить кадр, чтобы `renderLoop` больше не вызывался (пауза). Если идентификатора нет — мы на паузе, вызываем `requestAnimationFrame`, чтобы возобновить игру.
+
+Так как цикл ведёт JavaScript, менять исходники Rust не нужно.
+
+Введём переменную `animationId` для идентификатора из `requestAnimationFrame`. Когда кадр не запланирован, в ней хранится `null`.
+
+```js
+let animationId = null;
+
+// Как раньше, но результат `requestAnimationFrame` сохраняем в `animationId`.
+const renderLoop = () => {
+    drawGrid();
+    drawCells();
+
+    universe.tick();
+
+    animationId = requestAnimationFrame(renderLoop);
+};
+```
+
+В любой момент по значению `animationId` видно, на паузе игра или нет:
+
+```js
+const isPaused = () => {
+    return animationId === null;
+};
+```
+
+По клику на кнопку проверяем паузу или воспроизведение и либо снова запускаем `renderLoop`, либо отменяем следующий кадр. Заодно обновляем подпись на кнопке так, чтобы она показывала действие при следующем клике.
+
+```js
+const playPauseButton = document.getElementById(
+    'play-pause'
+);
+
+const play = () => {
+    playPauseButton.textContent = '⏸';
+    renderLoop();
+};
+
+const pause = () => {
+    playPauseButton.textContent = '▶';
+    cancelAnimationFrame(animationId);
+    animationId = null;
+};
+
+playPauseButton.addEventListener('click', (event) => {
+    if (isPaused()) {
+        play();
+    } else {
+        pause();
+    }
+});
+```
+
+Раньше мы запускали анимацию прямым вызовом `requestAnimationFrame(renderLoop)`; замените это на вызов `play`, чтобы с самого начала на кнопке была правильная подпись.
+
+```diff
+// Раньше здесь было `requestAnimationFrame(renderLoop)`.
+play();
+```
+
+Обновите [http://localhost:8080/](http://localhost:8080/) — по кнопке можно ставить игру на паузу и снимать с паузы.
+
+## Переключение состояния клетки по событию `"click"`
+
+Раз пауза есть, добавим изменение клеток по клику.
+
+Переключить клетку — значит сменить состояние «жива» ↔ «мёртва». Добавьте метод `toggle` для `Cell` в `wasm-game-of-life/src/lib.rs`:
+
+```rust
+impl Cell {
+    fn toggle(&mut self) {
+        *self = match *self {
+            Cell::Dead => Cell::Alive,
+            Cell::Alive => Cell::Dead,
+        };
+    }
+}
+```
+
+Чтобы переключить клетку в заданной строке и столбце, переводим пару (строка, столбец) в индекс в векторе клеток и вызываем `toggle` у этой клетки:
+
+```rust
+/// Публичные методы, экспортируемые в JavaScript.
+#[wasm_bindgen]
+impl Universe {
+    // ...
+
+    pub fn toggle_cell(&mut self, row: u32, column: u32) {
+        let idx = self.get_index(row, column);
+        self.cells[idx].toggle();
+    }
+}
+```
+
+Метод объявлен в блоке `impl` с атрибутом `#[wasm_bindgen]`, чтобы его можно было вызывать из JavaScript.
+
+В `wasm-game-of-life/www/index.js` подписываемся на клики по ``: переводим координаты клика из системы страницы в координаты холста, затем в строку и столбец, вызываем `toggle_cell` и перерисовываем сцену.
+
+```js
+canvas.addEventListener('click', (event) => {
+    const boundingRect = canvas.getBoundingClientRect();
+
+    const scaleX = canvas.width / boundingRect.width;
+    const scaleY = canvas.height / boundingRect.height;
+
+    const canvasLeft =
+        (event.clientX - boundingRect.left) * scaleX;
+    const canvasTop =
+        (event.clientY - boundingRect.top) * scaleY;
+
+    const row = Math.min(
+        Math.floor(canvasTop / (CELL_SIZE + 1)),
+        height - 1
+    );
+    const col = Math.min(
+        Math.floor(canvasLeft / (CELL_SIZE + 1)),
+        width - 1
+    );
+
+    universe.toggle_cell(row, col);
+
+    drawGrid();
+    drawCells();
+});
+```
+
+Соберите проект командой `wasm-pack build` в каталоге `wasm-game-of-life`, снова откройте [http://localhost:8080/](http://localhost:8080/) — можно рисовать свои паттерны кликами по клеткам.
+
+## Упражнения
+
+-   Добавьте виджет [``](https://developer.mozilla.org/ru/docs/Web/HTML/Element/input/range), чтобы задавать, сколько тиков выполняется за один кадр анимации.
+-   Кнопка сброса вселенной в случайное начальное состояние по клику. Ещё одна кнопка — сброс во «все клетки мёртвы».
+-   По `Ctrl + клик` вставляйте [планер (глайдер)](), центрированный на целевой клетке. По `Shift + клик` — [пульсар]() (классический осциллятор в игре жизни).
diff --git a/docs/articles/game-of-life/next-universe.png b/docs/articles/game-of-life/next-universe.png
new file mode 100644
index 0000000..ca353d0
Binary files /dev/null and b/docs/articles/game-of-life/next-universe.png differ
diff --git a/docs/articles/game-of-life/perf-report.png b/docs/articles/game-of-life/perf-report.png
new file mode 100644
index 0000000..6eb734b
Binary files /dev/null and b/docs/articles/game-of-life/perf-report.png differ
diff --git a/docs/articles/game-of-life/profiler-firefox-show-gecko-platform.png b/docs/articles/game-of-life/profiler-firefox-show-gecko-platform.png
new file mode 100644
index 0000000..0aa7c53
Binary files /dev/null and b/docs/articles/game-of-life/profiler-firefox-show-gecko-platform.png differ
diff --git a/docs/articles/game-of-life/profiler-with-rust-names.png b/docs/articles/game-of-life/profiler-with-rust-names.png
new file mode 100644
index 0000000..8164b35
Binary files /dev/null and b/docs/articles/game-of-life/profiler-with-rust-names.png differ
diff --git a/docs/articles/game-of-life/publishing-to-npm.md b/docs/articles/game-of-life/publishing-to-npm.md
new file mode 100644
index 0000000..2565512
--- /dev/null
+++ b/docs/articles/game-of-life/publishing-to-npm.md
@@ -0,0 +1,49 @@
+# Публикация в npm
+
+Теперь у нас есть рабочий, быстрый *и* компактный пакет `wasm-game-of-life` — можно опубликовать его в npm, чтобы другие разработчики на JavaScript могли им пользоваться, если им понадобится готовая реализация «Игры жизни».
+
+## Предварительные требования
+
+Во-первых, [зарегистрируйте аккаунт на npm](https://www.npmjs.com/signup), если его ещё нет.
+
+Во-вторых, войдите в аккаунт локально, выполнив команду:
+
+```
+wasm-pack login
+```
+
+## Публикация
+
+Убедитесь, что сборка в `wasm-game-of-life/pkg` актуальна: из каталога `wasm-game-of-life` запустите `wasm-pack`:
+
+```
+wasm-pack build
+```
+
+Загляните в содержимое `wasm-game-of-life/pkg` — именно это мы и будем выкладывать в npm на следующем шаге!
+
+Когда будете готовы, выполните `wasm-pack publish`, чтобы загрузить пакет в npm:
+
+```
+wasm-pack publish
+```
+
+На этом публикация в npm заканчивается!
+
+...кроме того, что этот туториал уже проходили другие люди, поэтому имя `wasm-game-of-life` на npm занято, и последняя команда, скорее всего, не сработала.
+
+Откройте `wasm-game-of-life/Cargo.toml` и допишите своё имя пользователя в конец поля `name`, чтобы пакет однозначно отличался от остальных:
+
+```toml
+[package]
+name = "wasm-game-of-life-my-username"
+```
+
+Затем снова соберите и опубликуйте:
+
+```
+wasm-pack build
+wasm-pack publish
+```
+
+На этот раз должно получиться!
diff --git a/docs/articles/game-of-life/rules.md b/docs/articles/game-of-life/rules.md
new file mode 100644
index 0000000..5890636
--- /dev/null
+++ b/docs/articles/game-of-life/rules.md
@@ -0,0 +1,59 @@
+# Правила "Игры жизни" Конвея
+
+Примечание: если вы уже знакомы с "Игрой жизни Конвея" и ее правилами, переходите к следующему разделу!
+
+[Википедия дает отличное описание правил "Игры жизни Конвея":](https://ru.wikipedia.org/wiki/Игра_«Жизнь»)
+
+> Вселенная игры Game of Life - это бесконечная двумерная ортогональная сетка квадратных клеток, каждая из которых находится в одном из двух возможных состояний: живая или мертвая, "заселенная" или "незаселенная". Каждая клетка взаимодействует со своими восемью соседями - клетками, расположенными рядом по горизонтали, вертикали или диагонали. На каждом шаге во времени происходят следующие переходы:
+>
+> 1. Любая живая клетка, имеющая менее двух живых соседей, погибает, как если бы это было вызвано перенаселением.
+>
+> 2. Любая живая клетка с двумя или тремя живыми соседями переходит в следующее поколение.
+>
+> 3. Любая живая клетка с более чем тремя живыми соседями погибает, как при перенаселении.
+>
+> 4. Любая мертвая клетка с ровно тремя живыми соседями становится живой клеткой, как при размножении.
+>
+> Начальный шаблон представляет собой семя системы. Первое поколение создается путем одновременного применения вышеуказанных правил к каждой клетке в семени - рождение и смерть происходят одновременно, а дискретный момент, когда это происходит, иногда называют тиком (другими словами, каждое поколение является чистой функцией предыдущего). Эти правила продолжают применяться многократно для создания последующих поколений.
+
+Рассмотрим следующую начальную вселенную:
+
+![Initial Universe](initial-universe.png)
+
+Мы можем вычислить следующее поколение, рассмотрев каждую клетку. Левая верхняя клетка мертва. Правило (4) - единственное правило перехода, которое применяется к мертвым клеткам. Однако, поскольку у левой верхней клетки нет ровно трех живых соседей, правило перехода не действует, и она остается мертвой в следующем поколении. То же самое происходит и со всеми остальными клетками в первом ряду.
+
+Ситуация становится интересной, когда мы рассматриваем верхнюю живую клетку во втором ряду, третьем столбце. Для живых клеток потенциально применимо любое из первых трех правил. В случае с этой клеткой у нее есть только один живой сосед, и поэтому действует правило (1): эта клетка умрет в следующем поколении. Та же участь ожидает нижнюю живую клетку.
+
+У средней живой клетки есть два живых соседа: верхняя и нижняя живые клетки. Это означает, что к ней применимо правило (2), и она останется живой в следующем поколении.
+
+Последний интересный случай - это мертвые клетки слева и справа от средней живой клетки. Все три живые клетки являются соседями обеих этих клеток, что означает, что правило (4) применимо, и эти клетки станут живыми в следующем поколении.
+
+Сложите все это вместе, и мы получим следующую вселенную после следующего тика:
+
+![Следующая вселенная](next-universe.png)
+
+Из этих простых, детерминированных правил вытекает странное и захватывающее поведение:
+
+| Gosper's glider gun                                                                                | Pulsar                                                                                 | Space ship                                                                                                   |
+| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
+| ![Gosper's glider gun](https://upload.wikimedia.org/wikipedia/commons/e/e5/Gospers_glider_gun.gif) | ![Pulsar](https://upload.wikimedia.org/wikipedia/commons/0/07/Game_of_life_pulsar.gif) | ![Lighweight space ship](https://upload.wikimedia.org/wikipedia/commons/3/37/Game_of_life_animated_LWSS.gif) |
+
+
+
+## Упражнения
+
+Вычислите вручную следующий тик нашего примера вселенной. Заметили что-то знакомое?
+
+!!!note "Answer"
+
+    Это должно быть начальное состояние примера вселенной:
+
+    ![Initial Universe](initial-universe.png)
+
+    Этот паттерн является _периодическим_: он возвращается в исходное состояние через каждые два тика.
+
+Можете ли вы найти начальную вселенную, которая была бы стабильной? То есть такую вселенную, в которой каждое поколение всегда одно и то же.
+
+!!!note "Answer"
+
+    Существует бесконечное число стабильных вселенных! Тривиально стабильная вселенная - это пустая вселенная. Квадрат два на два из живых клеток - тоже стабильная вселенная.
diff --git a/docs/articles/game-of-life/setup.md b/docs/articles/game-of-life/setup.md
new file mode 100644
index 0000000..3fc6442
--- /dev/null
+++ b/docs/articles/game-of-life/setup.md
@@ -0,0 +1,45 @@
+---
+description: В этом разделе описывается настройка цепочки инструментов для компиляции программ на языке Rust в WebAssembly и их интеграции в JavaScript
+---
+
+# Настройка
+
+В этом разделе описывается настройка цепочки инструментов для компиляции программ на языке Rust в WebAssembly и их интеграции в JavaScript.
+
+## Инструментарий Rust
+
+Вам понадобится стандартный набор инструментов Rust, включая `rustup`, `rustc` и `cargo`.
+
+[Следуйте этим инструкциям, чтобы установить инструментарий Rust](https://www.rust-lang.org/tools/install).
+
+Опыт работы с Rust и WebAssembly оседлал поезда с релизом Rust и перешел на стабильный уровень! Это означает, что нам не требуются флаги экспериментальных функций. Тем не менее, нам требуется Rust 1.30 или новее.
+
+## `wasm-pack`
+
+`wasm-pack` - это ваш универсальный магазин для создания, тестирования и публикации WebAssembly, сгенерированного на Rust.
+
+[Получить `wasm-pack` можно здесь!](https://rustwasm.github.io/wasm-pack/installer/)
+
+## `cargo-generate`
+
+[`cargo-generate` поможет вам быстро начать работу над новым проектом Rust, используя уже существующий git-репозиторий в качестве шаблона](https://github.com/ashleygwilliams/cargo-generate).
+
+Установите `cargo-generate` с помощью этой команды:
+
+```
+cargo install cargo-generate
+```
+
+## `npm`
+
+`npm` - это менеджер пакетов для JavaScript. Мы будем использовать его для установки и запуска JavaScript-бандлера и сервера разработки. В конце урока мы опубликуем наш скомпилированный `.wasm` в реестре `npm`.
+
+[Следуйте этим инструкциям для установки `npm`](https://www.npmjs.com/get-npm).
+
+Если у вас уже установлен `npm`, убедитесь в его актуальности с помощью этой команды:
+
+```
+npm install npm@latest -g
+```
+
+:material-information-outline: Источник — 
diff --git a/docs/articles/game-of-life/testing.md b/docs/articles/game-of-life/testing.md
new file mode 100644
index 0000000..38beb07
--- /dev/null
+++ b/docs/articles/game-of-life/testing.md
@@ -0,0 +1,142 @@
+# Testing Conway's Game of Life
+
+Now that we have our Rust implementation of the Game of Life rendering in the 
+browser with JavaScript, let's talk about testing our Rust-generated 
+WebAssembly functions.
+
+We are going to test our `tick` function to make sure that it gives us the 
+output that we expect.
+
+Next, we'll want to create some setter and getter 
+functions inside our existing `impl Universe` block in the
+`wasm_game_of_life/src/lib.rs` file. We are going to create a `set_width`
+and a `set_height` function so we can create `Universe`s of different sizes.
+
+```rust
+#[wasm_bindgen]
+impl Universe { 
+    // ...
+
+    /// Set the width of the universe.
+    ///
+    /// Resets all cells to the dead state.
+    pub fn set_width(&mut self, width: u32) {
+        self.width = width;
+        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
+    }
+
+    /// Set the height of the universe.
+    ///
+    /// Resets all cells to the dead state.
+    pub fn set_height(&mut self, height: u32) {
+        self.height = height;
+        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
+    }
+
+}
+```
+
+We are going to create another `impl Universe` block inside our
+`wasm_game_of_life/src/lib.rs` file without the `#[wasm_bindgen]` attribute.
+There are a few functions we need for testing that we don't want to expose to
+our JavaScript. Rust-generated WebAssembly functions cannot return
+borrowed references. Try compiling the Rust-generated WebAssembly with the
+attribute and take a look at the errors you get.
+
+We are going to write the implementation of `get_cells` to get the contents of
+the `cells` of a `Universe`. We'll also write a `set_cells` function so we can
+set `cells` in a specific row and column of a `Universe` to be `Alive.`
+
+```rust
+impl Universe {
+    /// Get the dead and alive values of the entire universe.
+    pub fn get_cells(&self) -> &[Cell] {
+        &self.cells
+    }
+
+    /// Set cells to be alive in a universe by passing the row and column
+    /// of each cell as an array.
+    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
+        for (row, col) in cells.iter().cloned() {
+            let idx = self.get_index(row, col);
+            self.cells[idx] = Cell::Alive;
+        }
+    }
+
+}
+```
+
+Now we're going to create our test in the `wasm_game_of_life/tests/web.rs` file.
+
+Before we do that, there is already one working test in the file. You can
+confirm that the Rust-generated WebAssembly test is working by running
+`wasm-pack test --chrome --headless` in the `wasm-game-of-life` directory.
+You can also use the `--firefox`, `--safari`, and `--node` options to
+test your code in those browsers.
+
+In the `wasm_game_of_life/tests/web.rs` file, we need to export our
+`wasm_game_of_life` crate and the `Universe` type.
+
+```rust
+extern crate wasm_game_of_life;
+use wasm_game_of_life::Universe;
+```
+
+In the `wasm_game_of_life/tests/web.rs` file we'll want to create some
+spaceship builder functions.
+
+We'll want one for our input spaceship that we'll call the `tick` function on
+and we'll want the expected spaceship we will get after one tick. We picked the
+cells that we want to initialize as `Alive` to create our spaceship in the
+`input_spaceship` function. The position of the spaceship in the
+`expected_spaceship` function after the tick of the `input_spaceship` was
+calculated manually. You can confirm for yourself that the cells of the input
+spaceship after one tick is the same as the expected spaceship.
+
+```rust
+#[cfg(test)]
+pub fn input_spaceship() -> Universe {
+    let mut universe = Universe::new();
+    universe.set_width(6);
+    universe.set_height(6);
+    universe.set_cells(&[(1,2), (2,3), (3,1), (3,2), (3,3)]);
+    universe
+}
+
+#[cfg(test)]
+pub fn expected_spaceship() -> Universe {
+    let mut universe = Universe::new();
+    universe.set_width(6);
+    universe.set_height(6);
+    universe.set_cells(&[(2,1), (2,3), (3,2), (3,3), (4,2)]);
+    universe
+}
+```
+
+Now we will write the implementation for our `test_tick` function. First, we
+create an instance of our `input_spaceship()` and our `expected_spaceship()`.
+Then, we call `tick` on the `input_universe`. Finally, we use the `assert_eq!`
+macro to call `get_cells()` to ensure that `input_universe` and
+`expected_universe` have the same `Cell` array values. We add the
+`#[wasm_bindgen_test]` attribute to our code block so we can test our
+Rust-generated WebAssembly code and use `wasm-pack test` to test the
+WebAssembly code.
+
+```rust
+#[wasm_bindgen_test]
+pub fn test_tick() {
+    // Let's create a smaller Universe with a small spaceship to test!
+    let mut input_universe = input_spaceship();
+
+    // This is what our spaceship should look like
+    // after one tick in our universe.
+    let expected_universe = expected_spaceship();
+
+    // Call `tick` and then see if the cells in the `Universe`s are the same.
+    input_universe.tick();
+    assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
+}
+```
+
+Run the tests within the `wasm-game-of-life` directory by running
+`wasm-pack test --firefox --headless`.
diff --git a/docs/articles/game-of-life/time-profiling.md b/docs/articles/game-of-life/time-profiling.md
new file mode 100644
index 0000000..611ccd9
--- /dev/null
+++ b/docs/articles/game-of-life/time-profiling.md
@@ -0,0 +1,481 @@
+# Профилирование по времени
+
+В этой главе мы улучшим производительность нашей реализации «Игры жизни». Будем опираться на замеры времени (time profiling), чтобы не действовать вслепую.
+
+Перед продолжением ознакомьтесь с [доступными инструментами для профилирования по времени кода на Rust и WebAssembly](https://rustwasm.github.io/docs/book/reference/time-profiling.html#available-tools).
+
+## Счётчик FPS на основе `window.performance.now`
+
+Такой счётчик кадров в секунду пригодится, когда будем разбираться, как ускорить отрисовку «Игры жизни».
+
+Начнём с добавления объекта `fps` в `wasm-game-of-life/www/index.js`:
+
+```js
+const fps = new (class {
+    constructor() {
+        this.fps = document.getElementById('fps');
+        this.frames = [];
+        this.lastFrameTimeStamp = performance.now();
+    }
+
+    render() {
+        // Переводим интервал с предыдущего кадра в оценку кадров в секунду.
+        const now = performance.now();
+        const delta = now - this.lastFrameTimeStamp;
+        this.lastFrameTimeStamp = now;
+        const fps = (1 / delta) * 1000;
+
+        // Храним только последние 100 измерений.
+        this.frames.push(fps);
+        if (this.frames.length > 100) {
+            this.frames.shift();
+        }
+
+        // Ищем минимум, максимум и среднее по последним 100 значениям.
+        let min = Infinity;
+        let max = -Infinity;
+        let sum = 0;
+        for (let i = 0; i < this.frames.length; i++) {
+            sum += this.frames[i];
+            min = Math.min(this.frames[i], min);
+            max = Math.max(this.frames[i], max);
+        }
+        let mean = sum / this.frames.length;
+
+        // Выводим статистику.
+        this.fps.textContent = `
+Кадров в секунду:
+         последний = ${Math.round(fps)}
+ среднее за 100 = ${Math.round(mean)}
+    мин за 100 = ${Math.round(min)}
+    макс за 100 = ${Math.round(max)}
+`.trim();
+    }
+})();
+```
+
+Далее вызываем `fps.render()` на каждой итерации `renderLoop`:
+
+```js
+const renderLoop = () => {
+    fps.render(); // новое
+
+    universe.tick();
+    drawGrid();
+    drawCells();
+
+    animationId = requestAnimationFrame(renderLoop);
+};
+```
+
+Не забудьте добавить элемент `fps` в `wasm-game-of-life/www/index.html` чуть выше ``:
+
+```html
+
+``` + +И CSS, чтобы вывод выглядел аккуратно: + +```css +#fps { + white-space: pre; + font-family: monospace; +} +``` + +Готово! Обновите [http://localhost:8080](http://localhost:8080) — на странице появится счётчик FPS. + +### Замер каждого `Universe::tick` через `console.time` и `console.timeEnd` + +Чтобы измерить длительность каждого вызова `Universe::tick`, можно использовать `console.time` и `console.timeEnd` из крейта `web-sys`. + +Сначала добавьте `web-sys` в зависимости `wasm-game-of-life/Cargo.toml`: + +```toml +[dependencies.web-sys] +version = "0.3" +features = [ + "console", +] +``` + +На каждый вызов `console.time` должен приходиться соответствующий `console.timeEnd`, поэтому удобно обернуть оба вызова в тип с паттерном [RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization): + +```rust +extern crate web_sys; +use web_sys::console; + +pub struct Timer<'a> { + name: &'a str, +} + +impl<'a> Timer<'a> { + pub fn new(name: &'a str) -> Timer<'a> { + console::time_with_label(name); + Timer { name } + } +} + +impl<'a> Drop for Timer<'a> { + fn drop(&mut self) { + console::time_end_with_label(self.name); + } +} +``` + +Затем можно замерить `Universe::tick`, добавив в начало метода: + +```rust +let _timer = Timer::new("Universe::tick"); +``` + +Длительность каждого вызова `Universe::tick` теперь попадает в консоль: + +[Снимок экрана: логи console.time](./console-time.png) + +Кроме того, пары `console.time` / `console.timeEnd` отображаются во «временной шкале» или «водопаде» в профилировщике браузера: + +[Снимок экрана: console.time в профилировщике](./console-time-in-profiler.png) + +## Увеличиваем вселенную «Игры жизни» + +> ⚠️ В этом разделе для примеров используются скриншоты из Firefox. В других браузерах инструменты похожи, но названия и вид панелей могут чуть отличаться. Суть профиля будет той же, а вот удобство отдельных представлений может различаться. + +Что будет, если сделать вселенную больше? Если заменить сетку 64×64 на 128×128 (изменив `Universe::new` в `wasm-game-of-life/src/lib.rs`), на моей машине FPS падает с ровных 60 до неровных ~40. + +Если записать профиль и открыть вид «водопада», видно, что кадр анимации занимает больше 20 миллисекунд. При 60 FPS на весь кадр остаётся около шестнадцати миллисекунд — и это не только наш JavaScript и WebAssembly, но и остальная работа браузера, в том числе отрисовка. + +[Снимок экрана: водопад отрисовки кадра](./drawCells-before-waterfall.png) + +Если разобрать один кадр анимации, оказывается, что сеттер `CanvasRenderingContext2D.fillStyle` очень дорогой! + +> ⚠️ В Firefox, если вместо `CanvasRenderingContext2D.fillStyle` вы видите строку просто «DOM», включите в настройках инструментов производительности опцию «Show Gecko Platform Data»: [Включение Show Gecko Platform Data](./profiler-firefox-show-gecko-platform.png) + +[Снимок экрана: flame graph отрисовки кадра](./drawCells-before-flamegraph.png) + +На агрегированном дереве вызовов по многим кадрам видно, что это не случайность: + +[Снимок экрана: call tree отрисовки кадра](./drawCells-before-calltree.png) + +Почти 40% времени уходит на этот сеттер! + +> ⚡ Можно было ожидать, что узким местом окажется что-то в методе `tick`, но это не так. Всегда опирайтесь на профилирование: время часто тратится не там, где вы его ждёте. + +В функции `drawCells` в `wasm-game-of-life/www/index.js` свойство `fillStyle` задаётся для каждой клетки вселенной на каждом кадре анимации: + +```js +for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col); + + ctx.fillStyle = + cells[idx] === DEAD ? DEAD_COLOR : ALIVE_COLOR; + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } +} +``` + +Раз смена `fillStyle` так дорога, как реже её вызывать? Значение должно зависеть от того, жива клетка или мёртва. Если выставить `fillStyle = ALIVE_COLOR` и в одном проходе нарисовать все живые клетки, затем `fillStyle = DEAD_COLOR` и во втором проходе — все мёртвые, `fillStyle` меняется только дважды, а не на каждую клетку. + +```js +// Живые клетки. +ctx.fillStyle = ALIVE_COLOR; +for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col); + if (cells[idx] !== Cell.Alive) { + continue; + } + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } +} + +// Мёртвые клетки. +ctx.fillStyle = DEAD_COLOR; +for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + const idx = getIndex(row, col); + if (cells[idx] !== Cell.Dead) { + continue; + } + + ctx.fillRect( + col * (CELL_SIZE + 1) + 1, + row * (CELL_SIZE + 1) + 1, + CELL_SIZE, + CELL_SIZE + ); + } +} +``` + +После сохранения и обновления [http://localhost:8080/](http://localhost:8080/) отрисовка снова держит ровные 60 FPS. + +По новому профилю видно, что кадр теперь занимает около десяти миллисекунд. + +[Снимок экрана: водопад после изменений в drawCells](./drawCells-after-waterfall.png) + +В разборе одного кадра стоимость `fillStyle` исчезла, большая часть времени уходит на `fillRect` — прямоугольники клеток. + +[Снимок экрана: flame graph после изменений в drawCells](./drawCells-after-flamegraph.png) + +## Больше тиков за кадр + +Кому-то не хочется ждать: хочется не один тик вселенной за кадр анимации, а девять. Функцию `renderLoop` в `wasm-game-of-life/www/index.js` можно изменить так: + +```js +for (let i = 0; i < 9; i++) { + universe.tick(); +} +``` + +На моей машине FPS снова падает примерно до 35 — плохо, хочется плавные 60. + +Теперь ясно, что время уходит в `Universe::tick`: добавим несколько `Timer` с вызовами `console.time` / `console.timeEnd` в разных местах и посмотрим. Гипотеза: выделение нового вектора клеток и освобождение старого на каждом тике дороги и съедают заметную долю бюджета. + +```rust +pub fn tick(&mut self) { + let _timer = Timer::new("Universe::tick"); + + let mut next = { + let _timer = Timer::new("allocate next cells"); + self.cells.clone() + }; + + { + let _timer = Timer::new("new generation"); + for row in 0..self.height { + for col in 0..self.width { + let idx = self.get_index(row, col); + let cell = self.cells[idx]; + let live_neighbors = self.live_neighbor_count(row, col); + + let next_cell = match (cell, live_neighbors) { + // Правило 1: живая клетка с менее чем двумя соседями умирает (недонаселение). + (Cell::Alive, x) if x < 2 => Cell::Dead, + // Правило 2: живая клетка с двумя или тремя соседями выживает. + (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive, + // Правило 3: живая клетка с более чем тремя соседями умирает (перенаселение). + (Cell::Alive, x) if x > 3 => Cell::Dead, + // Правило 4: мёртвая клетка с ровно тремя соседями оживает. + (Cell::Dead, 3) => Cell::Alive, + // Остальные клетки не меняются. + (otherwise, _) => otherwise, + }; + + next[idx] = next_cell; + } + } + } + + let _timer = Timer::new("free old cells"); + self.cells = next; +} +``` + +По замерам видно, что гипотеза неверна: подавляющее время уходит на вычисление следующего поколения клеток. Выделение и освобождение вектора на каждом тике, к удивлению, почти ничего не стоят. Ещё один повод всегда сверяться с профилем. + +[Снимок экрана: таймеры внутри Universe::tick](./console-time-in-universe-tick.png) + +Для следующего фрагмента нужен компилятор `nightly` из‑за [встроенного бенчмарка](https://doc.rust-lang.org/unstable-book/library-features/test.html). Ещё понадобится утилита [cargo benchcmp](https://github.com/BurntSushi/cargo-benchcmp) — небольшой инструмент для сравнения микробенчмарков из `cargo bench`. + +Напишем нативный `#[bench]`, делающий то же, что и WebAssembly, но уже со зрелыми средствами профилирования. Новый файл `wasm-game-of-life/benches/bench.rs`: + +```rust +#![feature(test)] + +extern crate test; +extern crate wasm_game_of_life; + +#[bench] +fn universe_ticks(b: &mut test::Bencher) { + let mut universe = wasm_game_of_life::Universe::new(); + + b.iter(|| { + universe.tick(); + }); +} +``` + +Также придётся закомментировать все аннотации `#[wasm_bindgen]` и секцию `"cdylib"` в `Cargo.toml`, иначе сборка нативного кода завершится ошибками линковки. + +После этого выполните `cargo bench | tee before.txt`, чтобы скомпилировать и прогнать бенчмарк. Часть `| tee before.txt` перенаправит вывод `cargo bench` в файл `before.txt`. + +``` +$ cargo bench | tee before.txt + Finished release [optimized + debuginfo] target(s) in 0.0 secs + Running target/release/deps/wasm_game_of_life-91574dfbe2b5a124 + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out + + Running target/release/deps/bench-8474091a05cfa2d9 + +running 1 test +test universe_ticks ... bench: 664,421 ns/iter (+/- 51,926) + +test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out +``` + +Из вывода видно, где лежит бинарник; тот же бенчмарк можно запустить под профилировщиком ОС. У меня Linux, поэтому использую [`perf`](https://perf.wiki.kernel.org/index.php/Main_Page): + +``` +$ perf record -g target/release/deps/bench-8474091a05cfa2d9 --bench +running 1 test +test universe_ticks ... bench: 635,061 ns/iter (+/- 38,764) + +test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out + +[ perf record: Woken up 1 times to write data ] +[ perf record: Captured and wrote 0.178 MB perf.data (2349 samples) ] +``` + +`perf report` показывает, что время, как и ожидалось, уходит в `Universe::tick`: + +[Снимок экрана: отчёт perf](./bench-perf-report.png) + +В `perf` можно разметить инструкции по доле времени, нажав `a`: + +[Снимок экрана: аннотация инструкций в perf](./bench-perf-annotate.png) + +Так видно, что 26,67% времени уходит на суммирование значений соседних клеток, 23,41% — на получение индекса столбца соседа и ещё 15,42% — на индекс строки. Среди трёх самых дорогих инструкций две последние — это `div`. Эти деления реализуют логику индексации по модулю в `Universe::live_neighbor_count`. + +Напомним определение `live_neighbor_count` в `wasm-game-of-life/src/lib.rs`: + +```rust +fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { + let mut count = 0; + for delta_row in [self.height - 1, 0, 1].iter().cloned() { + for delta_col in [self.width - 1, 0, 1].iter().cloned() { + if delta_row == 0 && delta_col == 0 { + continue; + } + + let neighbor_row = (row + delta_row) % self.height; + let neighbor_col = (column + delta_col) % self.width; + let idx = self.get_index(neighbor_row, neighbor_col); + count += self.cells[idx] as u8; + } + } + count +} +``` + +Модulo выбрали, чтобы не засорять код ветвлениями для первой/последней строки и столбца. Но цена инструкции `div` платится и в самом частом случае, когда ни строка, ни столбец не на границе и «заворот» по модулю не нужен. Если для краёв использовать `if` и развернуть цикл, ветки _должны_ хорошо предсказываться блоком предсказания ветлений в CPU. + +Перепишем `live_neighbor_count` так: + +```rust +fn live_neighbor_count(&self, row: u32, column: u32) -> u8 { + let mut count = 0; + + let north = if row == 0 { + self.height - 1 + } else { + row - 1 + }; + + let south = if row == self.height - 1 { + 0 + } else { + row + 1 + }; + + let west = if column == 0 { + self.width - 1 + } else { + column - 1 + }; + + let east = if column == self.width - 1 { + 0 + } else { + column + 1 + }; + + let nw = self.get_index(north, west); + count += self.cells[nw] as u8; + + let n = self.get_index(north, column); + count += self.cells[n] as u8; + + let ne = self.get_index(north, east); + count += self.cells[ne] as u8; + + let w = self.get_index(row, west); + count += self.cells[w] as u8; + + let e = self.get_index(row, east); + count += self.cells[e] as u8; + + let sw = self.get_index(south, west); + count += self.cells[sw] as u8; + + let s = self.get_index(south, column); + count += self.cells[s] as u8; + + let se = self.get_index(south, east); + count += self.cells[se] as u8; + + count +} +``` + +Снова запустите бенчмарк, на этот раз сохранив вывод в `after.txt`. + +``` +$ cargo bench | tee after.txt + Compiling wasm_game_of_life v0.1.0 (file:///home/fitzgen/wasm_game_of_life) + Finished release [optimized + debuginfo] target(s) in 0.82 secs + Running target/release/deps/wasm_game_of_life-91574dfbe2b5a124 + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out + + Running target/release/deps/bench-8474091a05cfa2d9 + +running 1 test +test universe_ticks ... bench: 87,258 ns/iter (+/- 14,632) + +test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out +``` + +Намного лучше! Насколько именно — покажет `benchcmp` и два сохранённых файла: + +``` +$ cargo benchcmp before.txt after.txt + name before.txt ns/iter after.txt ns/iter diff ns/iter diff % speedup + universe_ticks 664,421 87,258 -577,163 -86.87% x 7.61 +``` + +Ускорение примерно в 7,61 раза. + +WebAssembly сознательно близок к распространённым архитектурам, но стоит убедиться, что выигрыш нативного кода переносится и на `.wasm`. + +Пересоберите модуль командой `wasm-pack build` и обновите [http://localhost:8080/](http://localhost:8080/). У меня снова стабильные 60 FPS, а профиль браузера показывает около десяти миллисекунд на кадр. + +Готово! + +[Снимок экрана: водопад после замены modulo на ветвления](./waterfall-after-branches-and-unrolling.png) + +## Упражнения + +- Следующий очевидный шаг для ускорения `Universe::tick` — убрать выделение и освобождение памяти на каждом тике. Реализуйте двойную буферизацию: у `Universe` два вектора, ни один не освобождается, в `tick` новые буферы не выделяются. + +- Реализуйте дельта-подход из главы «Реализация Life», когда Rust возвращает в JavaScript список изменившихся клеток. Ускоряет ли это отрисовку на ``? Получится ли обойтись без нового списка дельт на каждом тике? + +- Профилирование показало, что отрисовка на 2D `` не самая быстрая. Замените рендер на [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API). Насколько быстрее вариант на WebGL? До какого размера вселенной узким местом остаётся именно WebGL? diff --git a/docs/articles/game-of-life/universe.png b/docs/articles/game-of-life/universe.png new file mode 100644 index 0000000..4998de6 Binary files /dev/null and b/docs/articles/game-of-life/universe.png differ diff --git a/docs/articles/game-of-life/waterfall-after-branches-and-unrolling.png b/docs/articles/game-of-life/waterfall-after-branches-and-unrolling.png new file mode 100644 index 0000000..b08cdb1 Binary files /dev/null and b/docs/articles/game-of-life/waterfall-after-branches-and-unrolling.png differ diff --git a/docs/articles/i18n.md b/docs/articles/i18n.md new file mode 100644 index 0000000..af9003a --- /dev/null +++ b/docs/articles/i18n.md @@ -0,0 +1,322 @@ +--- +description: Если вы используете i18next с TypeScript, вы, вероятно, чувствовали эту боль. Несмотря на улучшения производительности, реальность отрезвляет +--- + +# Создание альтернативы i18n + +## Кризис производительности в современной i18n + +Если вы используете [i18next с TypeScript](https://www.i18next.com/overview/typescript), вы, вероятно, чувствовали эту боль. Несмотря на [улучшения производительности](https://www.i18next.com/misc/migration-guide#redesigned-typescript-types), реальность отрезвляет: + +> протестировано на Apple M1 + +- **Компиляция TypeScript**: Каждые 1,000 ключей переводов добавляют ~1 секунду к времени сборки `tsc` +- **Отзывчивость IDE**: Подсказки типов замедляются на 0.3+ секунды с большими словарями +- **Размер бандла**: i18next весит 41.6 КБ (13.2 КБ gzip) еще до добавления переводов +- **Производительность во время выполнения**: Парсинг пользовательского DSL становится узким местом в масштабе + +Реальные разработчики чувствуют эту боль: + +> "Нам пришлось полностью убрать типизацию i18n из-за переполнения памяти CI с ~3к переводами" - _Продакшн разработчик_ +> +> "Удаление i18next улучшило производительность нашего SSR в 3 раза без потери функциональности" - _Инженер по производительности_ + +Но вот в чем дело: **современный JavaScript имеет все необходимое встроенное**. + +## Почему использовать нативные решения? + +[API интернационализации](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Internationalization) значительно созрел. У нас есть: + +- [`Intl.NumberFormat`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) для чисел, валют, единиц измерения +- [`Intl.DateTimeFormat`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) для дат и времени +- [`Intl.PluralRules`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) для логики множественного числа +- [`Intl.RelativeTimeFormat`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) для форматирования "2 дня назад" + +Эти API имеют **нулевую стоимость**, **поддерживают tree-shaking** и **молниеносно быстры**. + +## Решение: Система i18n из 5 файлов + +Вот полная система интернационализации, которая проще, быстрее и более удобна в сопровождении, чем традиционные библиотеки: + +### 1. Определение и управление языками + +```ts +// translations/lang.ts +import { cookie } from '../cookie'; + +const LANGS = { + en: 'en', + ru: 'ru', +} as const; +export type LANGS = keyof typeof LANGS; + +const userLang = + cookie.get('lang') ?? window.navigator.language; +export const LANG = + userLang in LANGS ? (userLang as LANGS) : 'en'; + +export const changeLang = (lang: string) => { + cookie.set('lang', lang); + window.location.reload(); +}; + +// Нативные форматеры - нулевые накладные расходы +export const degree = new Intl.NumberFormat(LANG, { + style: 'unit', + unit: 'degree', + unitDisplay: 'long', +}); +``` + +### 2. Динамическая загрузка переводов + +```ts +// translations/index.ts +import { LANG } from './lang'; + +export * from './lang'; + +const vocabModule = { + en: () => import('./en'), + ru: () => import('./ru'), +} as const; + +// Загружаем только нужный нам язык +export const { vocab: t } = await vocabModule[LANG](); +``` + +### 3. Типобезопасные файлы переводов + +```ts +// translations/en.ts +import { degree } from './lang'; + +export const vocab = { + hi: 'Hello', + temperature: (n: number) => + `Temperature is ${degree.format(n)}`, +}; + +export type Vocab = typeof vocab; +``` + +### 4. Простая утилита для работы с cookie + +```ts +// cookie.ts +const getCookieRec = () => + Object.fromEntries( + document.cookie + .split('; ') + .map((rec) => rec.split('=')) + ); + +export const cookie = { + get(name: string): string | undefined { + return getCookieRec()[name]; + }, + set(name: string, value: string) { + document.cookie = `${name}=${value}`; + }, +}; +``` + +### 5. Использование в компонентах + +```ts +// App.tsx +import { useState } from 'react'; +import { LANG, changeLang, t } from './translations'; + +export function App() { + const [count, setCount] = useState(0); + + return ( +
+

{t.hi}

+

+ +

+

{t.temperature(count)}

+

+ +

+
+ ); +} +``` + +## Преимущества + +✅ **Молниеносно быстрые типы**: Прямой доступ к объекту, без сложного сопоставления. + +✅ **Нулевые накладные расходы времени выполнения**: Без парсинга DSL, без дополнительного размера библиотеки. + +✅ **Автоматическое разделение кода**: Загружайте только те переводы, которые вам нужны + +✅ **Полная типобезопасность**: TypeScript автоматически выводит всё + +✅ **Нативное форматирование**: Используйте браузерные API для чисел, дат, множественного числа + +✅ **Простой API**: `t.key` вместо `t('key')` + +✅ **SSR из коробки**: никаких дополнительных настроек для SSR + +✅ **Независимость от фреймворка**: используйте со Svelte, React, Vue или jQuery 😁 + +## Продвинутые паттерны + +### Поддержка пространств имён + +Создавайте подкаталоги для различных функциональных областей: + +```ts +const vocabModule = { + en: () => import('./en'), + ru: () => import('./ru'), +} as const; + +const authModule = { + en: () => import('./auth/en'), + ru: () => import('./auth/ru'), +} as const; +``` + +### Множественные формы с Intl.PluralRules + +Для сложных форм множественного числа интегрируйте нативный API [`Intl.PluralRules`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) непосредственно в ваш словарь: + +```ts +// translations/en.ts +import { degree } from './lang'; + +const pluralRules = new Intl.PluralRules('en-US'); + +const createPlural = ( + forms: Record +) => (count: number) => { + const rule = pluralRules.select(count); + return forms[rule].replace('{count}', count.toString()); +}; + +export const vocab = { + hi: 'Hello', + temperature: (n: number) => + `Temperature is ${degree.format(n)}`, + items: createPlural({ + zero: 'No items', + one: '1 item', + two: '{count} элементов', // для языков, которые различают "два" + few: '{count} элементов', // для языков с категорией "несколько" + many: '{count} элементов', // для языков с категорией "много" + other: '{count} items', + }), + // Более сложный пример с вариациями по роду/падежу + notifications: createPlural({ + zero: 'No new notifications', + one: 'You have 1 new notification', + other: 'You have {count} new notifications', + }), +}; + +export type Vocab = typeof vocab; +``` + +Использование остается красиво простым: + +```jsx +

{t.items(0)}

// "No items" +

{t.items(1)}

// "1 item" +

{t.items(5)}

// "5 items" +``` + +Прекрасно то, что каждый язык может определить свои собственные правила множественного числа - русский имеет другие категории по сравнению с английским, и API `Intl.PluralRules` обрабатывает всю сложность за вас. + +## Серверный рендеринг + +Одно из самых больших преимуществ этого подхода становится очевидным при серверном рендеринге. **Это просто работает из коробки** - никакой сложной настройки сервера, никаких несоответствий гидратации, никаких головных болей с определением локали. + +Для **серверных сред без состояния** (Lambda, Vercel Functions и т.д.) это решение идеально подходит как есть. Каждый запрос получает свой собственный контекст выполнения, поэтому статические импорты работают прекрасно. + +Для **серверов с состоянием** (Express, Fastify и т.д.) у вас есть простой путь миграции. Преобразуйте точечную нотацию `t.key` в вызовы функций `t().key`, затем реализуйте функцию `t` используя [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) из Node.js: + +```ts +// translations/variable.ts +import { AsyncLocalStorage } from 'async_hooks'; + +export const tVariable = new AsyncLocalStorage(); +``` + +--- + +```ts +// translations/index.ts + +// ... + +const { vocab } = await vocabModule[LANG](); + +export let t = () => vocab; + +if (SSR) { + const vocabs = { + en: import('./en'), + ru: import('./ru'), + } as const; + + const { tVariable } = await import('./variable'); + + t = () => { + const locale = tVariable.getStore() ?? 'en'; + return vocabs[locale]; + }; +} +``` + +--- + +```ts +import { tVariable } from 'translations/variable'; + +app.use((req, res, next) => { + const locale = detectLocale(req); + tVariable.run(locale, next); +}); +``` + +Это обеспечивает изоляцию локали для каждого запроса без загрязнения глобального состояния - именно то, что нужно для обработки одновременных запросов. + +## Компромисс + +**Основной недостаток**: Переводы живут в коде, что затрудняет их редактирование для нетехнических членов команды. Это не всегда проблема - многие команды предпочитают контролируемые разработчиками переводы для лучшего контроля версий и процессов проверки. + +Для команд, которым нужно нетехническое редактирование, рассмотрите: + +- Генерацию во время сборки из внешних источников +- Git-воркфлоу с инструментами управления переводами +- Гибридные подходы для разных типов контента + +## Попробуйте + +[Демо на StackBlitz](https://stackblitz.com/edit/dumb-translations-system-example?file=src/App.tsx,src/translations/index.ts,src/translations/lang.ts&terminal=dev) + +Этот подход полностью изменил то, как я думаю об интернационализации. Иногда лучшее решение - не самое популярное, а то, которое использует то, что уже встроено в платформу. + +Каков ваш опыт работы с производительностью i18n? Нашли ли вы другие легковесные альтернативы? Поделитесь своими мыслями в комментариях! + +:material-information-outline: Источник — diff --git a/docs/articles/index.md b/docs/articles/index.md new file mode 100644 index 0000000..3cd18d5 --- /dev/null +++ b/docs/articles/index.md @@ -0,0 +1,29 @@ +--- +description: Интересные статьи про JAvascript и Typescript +--- + +# Статьи + +[Создание молниеносно быстрой альтернативы i18n](./i18n.md) + +: Если вы используете i18next с TypeScript, вы, вероятно, чувствовали эту боль. Несмотря на улучшения производительности, реальность отрезвляет + +[Контрольный список для tsconfig.json](./tsconfig-json.md) + +: Чтобы чувствовать себя более уверенно в своем `tsconfig.json`, я решил просмотреть документацию по `tsconfig.json`, собрать все часто используемые опции и описать их + +[Визуализация JavaScript: выполнение промисов](./promise-execution/index.md) + +: В этой статье мы погрузимся во внутреннюю работу промисов и узнаем, как они позволяют выполнять неблокирующие асинхронные задачи в JavaScript + +[Осмысление "бессмысленных" свойств JavaScript](./making-sense-of-senseless-javascript-features/index.md) + +: Рассматривается несколько "абсурдных" эксцентричностей JavaScript и объясняется, как они попали в язык, а также как избежать их в собственном коде + +[Event Loop](event-loop.md) + +: В данной статье поговорим о том, почему Event Loop вообще был создан, как с ним работать и почему про него спрашивают на собесах. + +[67 странных трюков отладки, о которых браузер не хочет, чтобы вы знали](./67debug/index.md) + +: Список полезных и неочевидных хаков, позволяющих получить максимальную отдачу от отладчика вашего браузера. Предполагает понимание инструментов разработчика на среднем или более высоком уровне diff --git a/docs/articles/making-sense-of-senseless-javascript-features/.pages b/docs/articles/making-sense-of-senseless-javascript-features/.pages new file mode 100644 index 0000000..442f8af --- /dev/null +++ b/docs/articles/making-sense-of-senseless-javascript-features/.pages @@ -0,0 +1,3 @@ +title: Осмысление "бессмысленных" свойств JavaScript +nav: + - index.md diff --git a/docs/articles/making-sense-of-senseless-javascript-features/1-8-bit-integers-showing-0-255.jpg b/docs/articles/making-sense-of-senseless-javascript-features/1-8-bit-integers-showing-0-255.jpg new file mode 100644 index 0000000..51cd92d Binary files /dev/null and b/docs/articles/making-sense-of-senseless-javascript-features/1-8-bit-integers-showing-0-255.jpg differ diff --git a/docs/articles/making-sense-of-senseless-javascript-features/2-decimals-fixed-point.jpg b/docs/articles/making-sense-of-senseless-javascript-features/2-decimals-fixed-point.jpg new file mode 100644 index 0000000..4721997 Binary files /dev/null and b/docs/articles/making-sense-of-senseless-javascript-features/2-decimals-fixed-point.jpg differ diff --git a/docs/articles/making-sense-of-senseless-javascript-features/3-decimals-floating-point.jpg b/docs/articles/making-sense-of-senseless-javascript-features/3-decimals-floating-point.jpg new file mode 100644 index 0000000..48fa24b Binary files /dev/null and b/docs/articles/making-sense-of-senseless-javascript-features/3-decimals-floating-point.jpg differ diff --git a/docs/articles/making-sense-of-senseless-javascript-features/index.md b/docs/articles/making-sense-of-senseless-javascript-features/index.md new file mode 100644 index 0000000..ec3e422 --- /dev/null +++ b/docs/articles/making-sense-of-senseless-javascript-features/index.md @@ -0,0 +1,263 @@ +--- +description: Рассматривается несколько "абсурдных" эксцентричностей JavaScript и объясняется, как они попали в язык, а также как избежать их в собственном коде +--- + +# Осмысление "бессмысленных" свойств JavaScript + +JavaScript может быть самым популярным в мире языком клиентской части, но он далек от совершенства и не лишен причуд. Хуан Диего Родригес рассматривает несколько "абсурдных" эксцентричностей JavaScript и объясняет, как они попали в язык, а также как избежать их в собственном коде. + +Почему JavaScript имеет так много эксцентриситетов! Например, почему `0.2 + 0.1` равно `0.30000000000000004`? Или почему `"" == false` оценивается как `true`? + +В JavaScript есть множество умопомрачительных решений, которые кажутся бессмысленными; некоторые из них неправильно поняты, а другие являются прямыми ошибками при проектировании. Как бы то ни было, стоит знать, что это за странные вещи и почему они присутствуют в языке. Я расскажу о самых странных, на мой взгляд, вещах в JavaScript и объясню их смысл. + +## `0.1 + 0.2` и формат с плавающей точкой + +Многие из нас издевались над JavaScript, написав в консоли `0.1 + 0.2` и наблюдая, как он с треском проваливается, не получая `0.3`, а скорее забавно выглядящее значение `0.30000000000000004`. + +Многие разработчики могут не знать, что в этом странном результате JavaScript не виноват! JavaScript просто придерживается [стандарта IEEE для арифметики с плавающей точкой](https://ieeexplore.ieee.org/document/8766229), который используют почти все остальные компьютеры и языки программирования для представления чисел. + +Но что именно представляет собой арифметика с плавающей точкой? + +Компьютерам приходится представлять числа всех размеров, от расстояния между планетами и даже между атомами. На бумаге легко написать огромное число или мизерную величину, не задумываясь о том, какого размера оно будет. Компьютеры не могут похвастаться такой роскошью, поскольку им приходится сохранять все виды чисел в двоичном виде и занимать небольшое пространство в памяти. + +Возьмем, к примеру, 8-битное целое число. В двоичном виде оно может хранить целые числа от `0` до `255`. + +![8-битные целые числа, отображающие 0 и 255.](1-8-bit-integers-showing-0-255.jpg) + +Ключевое слово здесь - целые числа. Между ними не может быть десятичных дробей. Чтобы исправить это, мы можем добавить мнимую десятичную точку где-то в 8-битной системе с тем, чтобы биты перед точкой использовались для представления целой части, а остальные - для десятичной. Поскольку точка всегда находится в одном и том же мнимом месте, это называется **десятичной дробью с фиксированной точкой**. Но за это приходится платить, так как диапазон сокращается от `0` до `255` до значений от `0` до `15.9375`. + +![Десятичные числа с фиксированной точкой.](2-decimals-fixed-point.jpg) + +Повышение точности означает потерю дальности, и наоборот. Также необходимо учитывать, что компьютеры должны удовлетворять большое количество пользователей с разными требованиями. Инженер, строящий мост, не слишком беспокоится, если измерения отклоняются на небольшую величину, скажем, на сотые доли сантиметра. Но, с другой стороны, та же сотая доля сантиметра может обойтись гораздо дороже для того, кто создает микрочип. Необходима разная точность, и последствия ошибки могут быть разными. + +Еще одно соображение - размер памяти, в которой хранятся числа, поскольку хранить длинные числа в мегабайтах не представляется возможным. + +Формат с _плавающей точкой_ родился из этой потребности представлять большие и малые величины с точностью и эффективностью. Он состоит из трех частей: + +1. Один бит, обозначающий, является ли число положительным или отрицательным (`0` - положительным, `1` - отрицательным). +2. [Знак](https://mathworld.wolfram.com/Significand.html) или [мантисса](https://mathworld.wolfram.com/Mantissa.html), содержащая цифры числа. +3. **Экспонента** определяет, где находится десятичная (или двоичная) точка относительно начала мантиссы, подобно тому, как работает научная система счисления. Следовательно, точка может перемещаться в любую позицию, отсюда и _плавающая_ точка. + +![Десятичные числа с плавающей точкой](3-decimals-floating-point.jpg) + +8-битный формат с плавающей точкой может представлять числа от `0.0078` до `480` (и их отрицательные значения), но заметьте, что представление с плавающей точкой не может представлять все числа в этом диапазоне. Это невозможно, поскольку 8 бит могут представлять только 256 различных значений. Неизбежно, что многие числа не могут быть точно представлены. В этом диапазоне есть пробелы. Компьютеры, конечно, работают с большим количеством бит, чтобы увеличить точность и диапазон, обычно с 32 и 64 битами, но невозможно точно представить все числа, и это небольшая цена, если учесть диапазон, который мы получаем, и память, которую мы экономим. + +Точная динамика гораздо сложнее, но пока нам достаточно понимать, что хотя этот формат позволяет выражать числа в большом диапазоне, он теряет точность (увеличиваются промежутки между представляемыми значениями), когда они становятся слишком большими. Например, числа в JavaScript представлены в формате двойной точности с плавающей точкой, то есть каждое число представлено в памяти в 64 битах, оставляя 53 бита для представления мантиссы. Это означает, что JavaScript может безопасно представлять целые числа только в диапазоне от -(2^53^ - 1) до 2^53^ - 1 без потери точности. После этого арифметика перестает иметь смысл. Именно поэтому у нас есть статическое свойство данных `Number.MAX_SAFE_INTEGER` для представления максимально безопасного целого числа в JavaScript, которое составляет (2^53^ - 1) или `9007199254740991`. + +Но `0.3` явно ниже порога `MAX_SAFE_INTEGER`, так почему же мы не можем получить его при сложении `0.1` и `0.2`? Формат с плавающей точкой испытывает трудности с некоторыми дробными числами. Это не проблема формата с плавающей запятой, но она точно есть в любой системе счисления. + +Чтобы убедиться в этом, давайте представим одну треть (1⁄3) в системе счисления base-10. + +``` +0.3 +``` + +--- + +``` +0.33 +``` + +--- + +``` +0.3333333 [...] +``` + +Сколько бы цифр мы ни пытались записать, результат никогда не будет равен одной трети. Точно так же мы не можем точно представить некоторые дробные числа в формате base-2 или двоичном. Возьмем, к примеру, `0.2`. Мы можем без проблем записать его в base-10, но если попытаемся записать его в двоичном виде, то получим в конце повторяющееся бесконечно число `1001`. + +``` +0.001 1001 1001 1001 1001 1001 10 [...] +``` + +Очевидно, что мы не можем иметь бесконечно большое число, поэтому в какой-то момент мантисса должна быть усечена, что не позволяет не потерять точность в процессе. Если мы попытаемся преобразовать `0.2` из числа с плавающей точкой двойной точности обратно в base-10, то увидим, что в памяти сохранилось фактическое значение: + +``` +0.200000000000000011102230246251565404236316680908203125 +``` + +Это не `0.2`! Мы не можем представить огромное количество дробных значений - не только в JavaScript, но и почти во всех компьютерах. Так почему же выполнение `0.2 + 0.2` правильно вычисляет `0.4`? В данном случае неточность настолько мала, что округляется Javascript (до 16-го знака после запятой), но иногда неточности достаточно, чтобы избежать механизма округления, как в случае с `0.2 + 0.1`. Мы можем увидеть, что происходит под капотом, если попробуем сложить фактические значения `0.1` и `0.2`. + +Это фактическое значение, сохраненное при записи `0.1`: + +``` +0.1000000000000000055511151231257827021181583404541015625 +``` + +Если мы вручную просуммируем фактические значения `0.1` и `0.2`, то увидим виновника: + +``` +0.3000000000000000444089209850062616169452667236328125 +``` + +Это значение округляется до `0.30000000000000004`. Вы можете проверить реальные значения, сохраненные на сайте [float.exposed](https://float.exposed/0x3fb999999999999a). + +Плавающая точка имеет свои известные недостатки, но ее положительные стороны перевешивают их, и она является стандартом во всем мире. В этом смысле радует, что все современные системы выдают нам один и тот же результат `0.30000000000000004` на разных архитектурах. Это может быть не тот результат, который вы ожидаете, но это результат, который вы можете предсказать. + +## Принудительное определение типа + +JavaScript - динамически типизированный язык, то есть нам не нужно объявлять тип переменной, и он может быть изменен позже в коде. + +!!!quote "" + + Я считаю динамически типизированные языки более свободными, поскольку мы можем больше сосредоточиться на сути кода. + +Проблема возникает из-за слабой типизации, так как во многих случаях язык пытается выполнить неявное преобразование между различными типами, например, из строк в числа или из _ложных_ и _истинных_ значений. Это особенно актуально при использовании операторов равенства (`==`) и знака плюс (`+`). Правила согласования типов сложны, трудно запоминаемы и в некоторых ситуациях даже неверны. Лучше избегать использования `==` и всегда отдавать предпочтение оператору строгого равенства (`===`). + +Например, JavaScript преобразует строку в число, если сравнить ее с другим числом: + +```js +console.log('2' == 2); // true +``` + +Обратное применимо к оператору со знаком плюс (`+`). Он будет пытаться превратить число в строку, когда это возможно: + +```js +console.log(2 + '2'); // "22" +``` + +Поэтому оператор со знаком плюс (`+`) следует использовать только в том случае, если мы уверены, что значения являются числами. При конкатенации строк лучше использовать метод [`concat()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/concat) или [шаблонные литералы](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Template_literals). + +Причина появления таких принуждений в языке на самом деле абсурдна. Когда создателя JavaScript Брендана Эйха спросили, что бы [он сделал по-другому](https://thenewstack.io/brendan-eich-on-creating-javascript-in-10-days-and-what-hed-do-differently-today#:~:text=notorious) при разработке JavaScript, он ответил, что был бы более щепетилен в реализации, которую хотели получить ранние пользователи языка: + +!!!quote "Brendan Eich" + + "Я бы избежал некоторых компромиссов, на которые я пошел, когда у меня появились первые последователи, и они сказали: "Вы можете это изменить?". + +Самый яркий пример - причина, по которой у нас есть два оператора равенства, `==` и `===`. Когда один из первых пользователей JavaScript объяснил, что ему нужно сравнить число со строкой без необходимости менять код для преобразования, Брендан добавил оператор свободного равенства, чтобы удовлетворить эти потребности. + +Существует множество других правил, управляющих оператором свободного равенства (и другими операторами, проверяющими условие), которые заставляют разработчиков JavaScript ломать голову. Они сложны, утомительны и бессмысленны, поэтому нам следует любой ценой избегать оператора свободного равенства (`==`) и заменить его строгим омонимом (`===`). + +Зачем вообще нужны два оператора равенства? Много факторов, но мы можем указать пальцем на Гая Л. Стила, соавтора языка программирования Scheme. Он заверил Эйха, что мы всегда сможем добавить еще один оператор равенства, поскольку в языке Lisp есть диалекты с пятью различными операторами равенства! Такой менталитет опасен, и в наши дни все возможности должны подвергаться тщательному анализу, потому что мы всегда можем добавить новые возможности, но как только они появляются в языке, их уже нельзя удалить. + +## Автоматическая вставка точки с запятой + +При написании кода на JavaScript точка с запятой (`;`) необходима в конце некоторых операторов, в том числе: + +- `var`, `let`, `const`; +- выражения операторов; +- `do...while`; +- `continue`, `break`, `return`, `throw`; +- `debugger`; +- Объявление полей класса (public или private); +- `import`, `export`. + +При этом нам не обязательно каждый раз вставлять точку с запятой, поскольку JavaScript может **автоматически вставлять точки с запятой** в процессе, неудивительно известном как автоматическая вставка точек с запятой (ASI). Он был задуман для того, чтобы облегчить кодирование для новичков, которые не знают, где нужна точка с запятой, но это ненадежная функция, и нам следует придерживаться явного ввода места, где ставится точка с запятой. Линтеры и форматтеры добавляют точку с запятой там, где это сделал бы ASI, но они тоже не совсем надежны. + +ASI может заставить работать некоторый код, но в большинстве случаев это не так. Возьмем следующий код: + +``` +const a = 1 +(1).toString() + +const b = 1 +[1, 2, 3].forEach(console.log) +``` + +Вы, вероятно, видите, куда делись точки с запятой, и если бы мы отформатировали это правильно, то получилось бы так: + +```js +const a = 1; + +(1).toString(); + +const b = 1; + +[(1, 2, 3)].forEach(console.log); +``` + +Но если мы передадим предыдущий код непосредственно в JavaScript, то возникнут всевозможные исключения, поскольку это будет то же самое, что написать вот это: + +```js +const a = 1(1).toString(); + +const b = (1)[(1, 2, 3)].forEach(console.log); +``` + +В заключение хочу сказать, что знайте свои точки с запятой. + +## Почему так много "нижних" значений? + +Термин "нижний" часто используется для обозначения значения, которое не существует или не определено. Но почему в JavaScript есть два вида нижних значений? + +Все в JavaScript можно считать объектом, кроме двух нижних значений `null` и `undefined` (несмотря на то, что `typeof null` возвращает `object`). Попытка получить от них значение свойства вызывает исключение. + +Обратите внимание, что, строго говоря, **все примитивные значения не являются объектами**. Но только `null` и `undefined` не подвергаются [боксированию](https://stackoverflow.com/questions/34067261/is-boxing-coercion-in-javascript). + +Можно даже считать `NaN` третьим нижним значением, которое представляет собой отсутствие числа. Обилие нижних значений следует рассматривать как ошибку проектирования. Нет прямой причины, объясняющей существование двух нижних значений, но мы видим разницу в том, как JavaScript использует их. + +`undefined` - это нижнее значение, которое JavaScript использует по умолчанию, поэтому считается хорошей практикой использовать его исключительно в коде. Когда мы определяем переменную без начального значения, попытка извлечь ее приводит к присвоению `undefined` значения. То же самое происходит, когда мы пытаемся получить доступ к несуществующему свойству объекта. Чтобы максимально соответствовать поведению JavaScript, используйте `undefined` для обозначения существующего свойства или переменной, не имеющей значения. + +С другой стороны, `null` используется для обозначения отсутствия объекта (следовательно, его `typeof` возвращает `object`, хотя он им не является). Однако это считается ошибкой, потому что `undefined` могло бы выполнять свои функции так же эффективно. В JavaScript он используется для обозначения конца рекурсивной структуры данных. Точнее, он используется в цепочке прототипов для обозначения ее конца. В большинстве случаев вы можете использовать `undefined` вместо `null`, но есть случаи, когда можно использовать только `null`, как в случае с [`Object.create`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create), когда мы можем создать объект без прототипа только передав `null`; использование `undefined` возвращает ошибку типа `TypeError`. + +И `null`, и `undefined` страдают от проблемы пути. При попытке получить доступ к свойству из нижнего значения - как если бы они были объектами - возникают исключения. + +```js +let user; + +let userName = user.name; // Uncaught TypeError + +let userNick = user.name.nick; // Uncaught TypeError +``` + +Обойти это невозможно, если только не проверять каждое значение свойства перед попыткой доступа к следующему, используя логическое AND (`&&`) или необязательную цепочку (`?`). + +```js +let user; + +let userName = user?.name; + +let userNick = user && user.name && user.name.nick; + +console.log(userName); // undefined + +console.log(userNick); // undefined +``` + +Я уже говорил, что `NaN` можно считать нижним значением, но у него есть свое запутанное место в JavaScript, поскольку он представляет числа, которые на самом деле не являются числами, обычно из-за неудачного преобразования строки в число (что является еще одной причиной избегать его). `NaN` имеет свои собственные махинации, потому что оно не равно самому себе! Чтобы проверить, является ли значение `NaN` или нет, используйте `Number.isNaN()`. + +Мы можем проверить все три нижних значения с помощью следующего теста: + +```js +function stringifyBottom(bottomValue) { + if (bottomValue === undefined) { + return 'undefined'; + } + + if (bottomValue === null) { + return 'null'; + } + + if (Number.isNaN(bottomValue)) { + return 'NaN'; + } +} +``` + +## Инкремент (`++`) и декремент (`--`) + +Как разработчики, мы склонны тратить больше времени на чтение кода, чем на его написание. Читаем ли мы документацию, просматриваем чужую работу или проверяем свою, **читабельность кода увеличит нашу производительность по сравнению с краткостью**. Другими словами, читабельность экономит время в долгосрочной перспективе. + +Вот почему я предпочитаю использовать `+ 1` или `- 1`, а не операторы инкремента (`++`) и декремента (`--`). + +Нелогично иметь отдельный синтаксис только для увеличения значения на единицу, а также иметь форму до увеличения и форму после увеличения в зависимости от места расположения оператора. Их очень легко перепутать, и это может быть трудно отладить. Им не должно быть места в вашем коде или даже в языке в целом, если мы рассмотрим, откуда берутся операторы инкремента. + +Как мы видели [в предыдущей статье](https://www.smashingmagazine.com/2023/12/marketing-changed-oop-javascript/), синтаксис JavaScript в значительной степени вдохновлен языком C, в котором используются переменные-указатели. Переменные-указатели были разработаны для хранения в памяти адресов других переменных, что позволяет динамически распределять память и манипулировать ею. Операторы `++` и `--` изначально были созданы для того, чтобы продвигаться вперед или возвращаться назад по ячейкам памяти. + +В настоящее время доказано, что арифметика указателей вредна и может привести к случайному доступу к участкам памяти за пределами предполагаемых границ массивов или буферов, что приводит к ошибкам памяти - печально известному источнику багов и уязвимостей. Тем не менее, синтаксис попал в JavaScript и остается там по сей день. + +Хотя использование `++` и `--` остается стандартом среди разработчиков, можно привести аргумент в пользу удобства чтения. Выбор `+ 1` или `- 1` вместо `++` и `--` не только соответствует принципам ясности и четкости, но и избавляет от необходимости иметь дело с формой до и после увеличения. + +В целом, это не жизненно важная ситуация, но хороший способ сделать ваш код более читабельным. + +## Заключение + +Кажущиеся бессмысленными особенности JavaScript часто возникают в результате исторических решений, компромиссов и попыток удовлетворить все потребности. К сожалению, невозможно сделать всех счастливыми, и JavaScript не является исключением. + +!!!quote "" + + JavaScript не обязан подстраиваться под всех разработчиков, но каждый разработчик обязан понимать язык и использовать его сильные стороны, не забывая при этом о его причудах. + +Я надеюсь, что вам стоит продолжать узнавать все больше и больше о JavaScript и его истории, чтобы разобраться в его непонятных особенностях и сомнительных решениях. Возьмем, к примеру, его удивительную прототипическую природу. Это было скрыто во время разработки или таких ошибок, как ключевое слово `this` и его многоцелевое поведение. + +В любом случае, я рекомендую каждому разработчику изучить язык и узнать о нем больше. И если вам интересно, я немного углублюсь в сомнительные области дизайна JavaScript в [другой статье](https://www.smashingmagazine.com/2023/12/marketing-changed-oop-javascript/), опубликованной здесь, на Smashing Magazine! diff --git a/docs/articles/promise-execution/.pages b/docs/articles/promise-execution/.pages new file mode 100644 index 0000000..a38855b --- /dev/null +++ b/docs/articles/promise-execution/.pages @@ -0,0 +1,3 @@ +title: Выполнение промисов +nav: + - index.md diff --git a/docs/articles/promise-execution/1.mp4 b/docs/articles/promise-execution/1.mp4 new file mode 100644 index 0000000..45db8f7 Binary files /dev/null and b/docs/articles/promise-execution/1.mp4 differ diff --git a/docs/articles/promise-execution/10.mp4 b/docs/articles/promise-execution/10.mp4 new file mode 100644 index 0000000..3175aad Binary files /dev/null and b/docs/articles/promise-execution/10.mp4 differ diff --git a/docs/articles/promise-execution/11.mp4 b/docs/articles/promise-execution/11.mp4 new file mode 100644 index 0000000..9da5bf4 Binary files /dev/null and b/docs/articles/promise-execution/11.mp4 differ diff --git a/docs/articles/promise-execution/12.mp4 b/docs/articles/promise-execution/12.mp4 new file mode 100644 index 0000000..dc708de Binary files /dev/null and b/docs/articles/promise-execution/12.mp4 differ diff --git a/docs/articles/promise-execution/13.mp4 b/docs/articles/promise-execution/13.mp4 new file mode 100644 index 0000000..4859f7e Binary files /dev/null and b/docs/articles/promise-execution/13.mp4 differ diff --git a/docs/articles/promise-execution/2.mp4 b/docs/articles/promise-execution/2.mp4 new file mode 100644 index 0000000..45db8f7 Binary files /dev/null and b/docs/articles/promise-execution/2.mp4 differ diff --git a/docs/articles/promise-execution/3.mp4 b/docs/articles/promise-execution/3.mp4 new file mode 100644 index 0000000..aeb0249 Binary files /dev/null and b/docs/articles/promise-execution/3.mp4 differ diff --git a/docs/articles/promise-execution/4.mp4 b/docs/articles/promise-execution/4.mp4 new file mode 100644 index 0000000..9534eb2 Binary files /dev/null and b/docs/articles/promise-execution/4.mp4 differ diff --git a/docs/articles/promise-execution/5.mp4 b/docs/articles/promise-execution/5.mp4 new file mode 100644 index 0000000..5a8e689 Binary files /dev/null and b/docs/articles/promise-execution/5.mp4 differ diff --git a/docs/articles/promise-execution/6.mp4 b/docs/articles/promise-execution/6.mp4 new file mode 100644 index 0000000..a7bf3a1 Binary files /dev/null and b/docs/articles/promise-execution/6.mp4 differ diff --git a/docs/articles/promise-execution/7.webp b/docs/articles/promise-execution/7.webp new file mode 100644 index 0000000..994a398 Binary files /dev/null and b/docs/articles/promise-execution/7.webp differ diff --git a/docs/articles/promise-execution/8.mp4 b/docs/articles/promise-execution/8.mp4 new file mode 100644 index 0000000..6ae39eb Binary files /dev/null and b/docs/articles/promise-execution/8.mp4 differ diff --git a/docs/articles/promise-execution/9.mp4 b/docs/articles/promise-execution/9.mp4 new file mode 100644 index 0000000..e8878db Binary files /dev/null and b/docs/articles/promise-execution/9.mp4 differ diff --git a/docs/articles/promise-execution/index.md b/docs/articles/promise-execution/index.md new file mode 100644 index 0000000..dbf436f --- /dev/null +++ b/docs/articles/promise-execution/index.md @@ -0,0 +1,195 @@ +--- +description: В этой статье мы погрузимся во внутреннюю работу промисов и узнаем, как они позволяют выполнять неблокирующие асинхронные задачи в JavaScript +--- + +# Визуализация JavaScript: выполнение промисов + +Промисы в JavaScript поначалу могут показаться немного пугающими, но понимание того, что происходит под капотом, может сделать их гораздо более доступными. + +В этой статье мы погрузимся во внутреннюю работу промисов и узнаем, как они позволяют выполнять неблокирующие асинхронные задачи в JavaScript. + + + +Один из способов создания промиса - использование конструктора `new Promise`, который получает *функцию-исполнителя* с аргументами `resolve` и `reject`. + +```js +new Promise((resolve, reject) => { + // TODO(Lydia): Некоторые асинхронные вещи здесь +}); +``` + +Когда вызывается конструктор промисов, происходит несколько вещей: + +- Создается [объект промиса](https://tc39.es/ecma262/#sec-properties-of-promise-instances). + + Этот объект промиса содержит несколько внутренних слотов, включая `[[PromiseState]]`, `[[PromiseResult]]`, `[[PromiseIsHandled]]`, `[[PromiseFulfillReactions]]` и `[[PromiseRejectReactions]]`. + +- Создается [запись возможности промиса](https://tc39.es/ecma262/#sec-promisecapability-records) + + Она "инкапсулирует" промис и добавляет некоторые дополнительные функции для разрешения или отклонения промиса. Это функции, которые управляют конечным состоянием `[[PromiseState]]` и `[[PromiseResult]]` промиса и запускают асинхронные задачи. + + + +Мы можем *разрешить* этот промис, вызвав `resolve`, который доступен нам через функцию `executor`. Когда мы вызываем `resolve`: + +- `[[PromiseState]]` устанавливается в значение `"fulfilled"` +- `[[PromiseResult]]` устанавливается в значение, которое мы передали `resolve` и которое в данном случае равно `"Done!"`. + + + +Процесс аналогичен вызову `reject`, теперь `[[PromiseState]]` устанавливается в `"rejected"`, а `[[PromiseResult]]` устанавливается в значение, которое мы передали `reject`, то есть `"Fail!"`. + + + +Конечно, здорово... но что такого особенного в использовании функций для изменения внутренних свойств объекта? + +Ответ кроется в поведении, связанном с двумя внутренними слотами, которые мы до сих пор пропускали: `[[PromiseFulfillReactions]]` и `[[PromiseRejectReactions]]`. + +--- + +Поле `[[PromiseFulfillReactions]]` содержит **реакции промиса**. Это объект, созданный путем присоединения обработчика `then` к промису. + +Эта **реакция промиса** содержит, помимо прочих полей, свойство `[[Handler]]`, содержащее функцию обратного вызова, которую мы передали в `then`. Когда промис разрешается, этот обработчик добавляется в *очередь микрозадач* и получает доступ к значению, с которым промис разрешился. + + + +Когда промис разрешается, этот обработчик получает в качестве аргумента значение `[[PromiseResult]]`, после чего оно пересылается в *очередь микрозадач*. + +Именно здесь вступает в силу асинхронная часть промисов! + + + +!!!note "Очередь микрозадач" + + **Очередь микрозадач** - это специализированная очередь в цикле событий. Когда *стек вызовов* пуст, цикл событий сначала обрабатывает задачи, ожидающие в *очереди микрозадач*, прежде чем обрабатывать задачи из обычной *очереди задач* (также называемой "очередью обратных вызовов" или "очередью макрозадач"). + + Я нахожусь в процессе обновления своей [статьи в блоге от 2019 года](https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif), но здесь вы можете прочитать немного больше о цикле событий. Я также рассказываю о нем в своем [курсе FrontendMasters](https://frontendmasters.com/courses/javascript-quiz/). + + + +Аналогичным образом мы можем создать запись *Promise Reaction* для обработки отказа от выполнения промиса с помощью цепочки `catch`. Этот обратный вызов будет добавлен в очередь микрозадач, когда промис будет отклонен. + + + +--- + +До сих пор мы вызывали `resolve` или `reject` только непосредственно в функции-исполнителе. Хотя это и *возможно*, но не позволяет использовать всю мощь (и основное назначение) промисов! + +В большинстве случаев мы хотим вызвать `resolve` или `reject` в какой-то более поздний момент времени, обычно после завершения асинхронной задачи. + +Асинхронные задачи выполняются вне основного потока, например чтение файла (например, `fs.readFile`), выполнение сетевых запросов (например, `https.get` или `XMLHttpRequest`) или что-то простое, как таймеры (`setTimeout`). + +![Примеры](7.webp) + +Когда эти задачи завершатся в какое-то неизвестное время в будущем, мы можем использовать обратный вызов, который обычно предоставляют такие async-операции, чтобы либо разрешить (`resolve`) их с помощью данных, которые мы получили от async-задачи, либо отклонить (`reject`), если произошла ошибка. + +--- + +Чтобы наглядно представить это, давайте пройдемся по шагам. Чтобы сделать демонстрацию простой, но реалистичной, мы используем `setTimeout`, чтобы добавить асинхронное поведение. + +```js +new Promise((resolve) => { + setTimeout(() => resolve("Done!"), 100); +}).then(result => console.log(result)) +``` + +Сначала в *стек вызовов* добавляется конструктор нового промиса `new Promise`, который создает *объект промиса*. + + + +Затем выполняется функция-исполнитель. В первой строке тела функции мы вызываем `setTimeout`, который добавляется в стек вызовов. + +`setTimeout` отвечает за планирование таймера в Timers Web API с задержкой в 100 мс, после чего обратный вызов, который мы передали в `setTimeout`, будет помещен в *очередь задач*. + + + +!!!note "" + + Это асинхронное поведение связано с `setTimeout`, а не с промисами. Я просто показываю это здесь, чтобы продемонстрировать распространенный способ использования промисов - разрешение промиса после некоторой задержки. + + Однако сама задержка не вызвана промисами. Промисы предназначены для работы с асинхронными операциями, но эти асинхронные операции могут происходить из разных источников, например таймеров или сетевых запросов. + +После того как таймер и конструктор выведены из *стека вызовов*, движок встречается с `then`. + +`then` добавляется в стек вызовов и создает запись *Promise Reaction*, обработчиком которой является код, переданный нами в качестве обратного вызова обработчику `then`. + +Поскольку состояние `[[PromiseState]]` все еще `"pending"`, эта запись *Promise Reaction* добавляется в список `[[PromiseFulfillReactions]]`. + + + +По истечении 100 мс обратный вызов `setTimeout` будет помещен в *очередь задач*. + +Сценарий уже завершил выполнение, поэтому *стек вызовов* пуст. В *очереди микрозадач* также ничего нет, а это значит, что теперь эта задача взята из *очереди задач* в *стек вызовов*. + +Обратный вызов выполняется и вызывает `resolve`. + + + +Вызов `resolve` устанавливает `[[PromiseState]]` в `"fulfilled"`, `[[PromiseResult]]` в `"Done!"`, а код, связанный с обработчиком реакции промиса, добавляется в *очередь микрозадач*. + +`resolve` и обратный вызов выгружаются из *стека вызовов*. + + + +Поскольку *стек вызовов* пуст, цикл событий сначала проверяет *очередь микрозадач*, где `then` ожидает обратный вызов обработчика. + +Теперь обратный вызов добавляется в *стек вызовов* и регистрирует значение `result`, которое является значением `[[PromiseResult]]` - строка `"Done!"`. + + + +Как только обратный вызов завершит выполнение и будет удален из стека вызовов, программа будет завершена! + +--- + +Помимо создания промиса Reaction, `then` также возвращает промис. Это означает, что мы можем соединить в цепочку несколько `then`, например: + +```js +new Promise((resolve) => { + resolve(1) +}) + .then(result => result * 2) + .then(result => result * 2) + .then(result => console.log(result)); +``` + +Когда этот код выполняется, при вызове конструктора промиса создается *объект промиса*. После этого всякий раз, когда движок встречает `then`, создаются как запись реакции промиса, так и объект промиса. + +В обоих случаях обратные вызовы `then` умножают полученное значение `[[PromiseResult]]` на два. Значение `[[PromiseResult]]` в `then` устанавливается на результат этого вычисления, который, в свою очередь, используется следующим обработчиком `then`. + + + +В конечном итоге результат записывается в журнал. Результат `[[PromiseResult]]` последнего промиса `then` не определен, поскольку мы явно не вернули значение, а значит, он неявно вернул `undefined`. + +Конечно, использование чисел - не самый *реалистичный* сценарий. Вместо этого вы можете захотеть изменять результат выполнения промиса шаг за шагом, как при постепенном изменении внешнего вида изображения. + +Например, вы можете предпринять ряд поэтапных шагов, изменяющих внешний вид изображения с помощью таких операций, как изменение размера, применение фильтров, добавление водяных знаков и т. д. + +```js +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }) +} + +loadImage(src) + .then(image => resizeImage(image)) + .then(image => applyGrayscaleFilter(image)) + .then(image => addWatermark(image)) +``` + +Задачи такого типа часто включают асинхронные операции, поэтому промисы являются хорошим выбором для неблокирующего управления. + +--- + +**Заключение** + +Короче говоря, промисы - это просто объекты с некоторой дополнительной функциональностью для изменения их внутреннего состояния. + +Самое замечательное в промисах то, что они могут запускать асинхронные действия, если к ним подключен обработчик с помощью `then` или `catch`. Так как обработчики передаются в *очередь микрозадач*, вы можете обрабатывать конечный результат неблокирующим способом. Это упрощает обработку ошибок, создание цепочек из нескольких операций, а также делает ваш код более читаемым и удобным для сопровождения! + +Промисы остаются основополагающей концепцией, которую должен знать каждый разработчик JavaScript. Другие возможности, такие как синтаксис `async`/`await` (синтаксический сахар для промисов) и такие функции, как [Async Generators](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator), предоставляют еще больше возможностей для работы с асинхронным кодом, если вы хотите узнать больше. + +:material-information-outline: Источник — diff --git a/docs/articles/tsconfig-json.md b/docs/articles/tsconfig-json.md new file mode 100644 index 0000000..144e475 --- /dev/null +++ b/docs/articles/tsconfig-json.md @@ -0,0 +1,766 @@ +--- +description: Чтобы чувствовать себя более уверенно в своем tsconfig.json, я решил просмотреть документацию по tsconfig.json, собрать все часто используемые опции и описать их ниже +--- + +# Контрольный список для tsconfig.json + +Версия: TypeScript 5.7 + +Чтобы чувствовать себя более уверенно в своем `tsconfig.json`, я решил просмотреть [документацию по `tsconfig.json`](https://www.typescriptlang.org/tsconfig/), собрать все часто используемые опции и описать их ниже: + +- Вы можете присоединиться ко мне и пройтись по опциям вместе со мной. После этого вы должны быть в состоянии понять весь ваш `tsconfig`. +- Или вы можете перейти к краткому описанию в конце. +- Я также привожу рекомендации по `tsconfig.json` от нескольких других людей. + +Мне интересно, каков ваш опыт работы с `tsconfig.json`: Согласны ли вы с моими рекомендациями? Может быть, я что-то упустил? + +## Функции, не вошедшие в эту статью + +В этой статье описывается только создание проектов, локальные модули которых состоят из ESM. Однако в ней даются советы по импорту CommonJS. + +Здесь не объясняется: + +- Импорт и проверка типа обычного JavaScript в вашей кодовой базе, а именно опции [`allowJs`](https://www.typescriptlang.org/tsconfig/#allowJs) и [`checkJs`](https://www.typescriptlang.org/tsconfig/#checkJs). +- Как настроить JSX. См. раздел [«JSX»](https://www.typescriptlang.org/docs/handbook/jsx.html) в TypeScript Handbook. +- «Проекты» (полезно для монорепов): опция `composite` и т. д. Дополнительную информацию по этой теме см: + - Глава [«Ссылки на проекты»](https://www.typescriptlang.org/docs/handbook/project-references.html) в TypeScript Handbook. + - Заметка в моем блоге [«Простые монорепозитории с помощью рабочих пространств npm и ссылок на проекты TypeScript»](https://2ality.com/2021/07/simple-monorepos.html) + +## Нотация + +Для отображения выводимых типов в исходном коде я использую пакет npm `ts-expect` - например: + +```ts +// Check that the inferred type of `someVariable` is `boolean` +expectType(someVariable); +``` + +Я часто использую запятые в конце JSON, потому что это поддерживается в `tsconfig.json` и потому что это помогает при перестановке, копировании и т. д. + +## Расширение базовых файлов с помощью `extends` + +Эта опция позволяет нам ссылаться на существующий `tsconfig.json` через спецификатор модуля (как если бы мы импортировали JSON-файл). Этот файл становится базой, которую расширяет наш `tsconfig`. Это означает, что наш `tsconfig` имеет все опции базы, но может переопределять любые из них и добавлять опции, не упомянутые в базе. + +В репозитории GitHub [`tsconfig/bases`](https://github.com/tsconfig/bases) перечислены базы, которые доступны в пространстве имен npm `@tsconfig` и могут быть использованы подобным образом (после их локальной установки через npm): + +```json +{ + "extends": "@tsconfig/node-lts/tsconfig.json" +} +``` + +Увы, ни один из этих файлов не подходит для моих нужд. Но они могут послужить вдохновением для вашего `tsconfig`. + +## Где находятся входные файлы? + +```json +{ + "include": ["src/**/*", "test/**/*"] +} +``` + +С одной стороны, мы должны указать TypeScript, какие файлы будут использоваться для ввода. Вот доступные варианты: + +- `files`: исчерпывающий массив всех входных файлов +- `include`: Указывает входные файлы через массив шаблонов с подстановочными знаками, которые интерпретируются как относительные к `tsconfig.json`. +- `exlude`: Указывает, какие файлы должны быть исключены из набора включаемых файлов - с помощью массива шаблонов. + +## Каков результат? + +### Куда записываются выходные файлы? + +```json +{ + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + } +} +``` + +Как TypeScript определяет, куда записать выходной файл: + +- Берется входной путь (относительно `tsconfig.json`), +- удаляет префикс, указанный в `rootDir`, и +- «добавляет» результат в `outDir`. + +По умолчанию значение `rootDir` - это самый длинный общий префикс относительных путей входных файлов. + +В качестве примера рассмотрим следующий `tsconfig.json`: + +```json +{ + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + } +} +``` + +Это файловая структура проекта: + +``` +/tmp/my-proj/ + tsconfig.json + src/ + main.ts + test/ + test.ts +``` + +Компилятор TypeScript выдает такой результат: + +``` +/tmp/my-proj/ + dist/ + src/ + main.js + test/ + test.js +``` + +Если мы удалим `rootDir` из `tsconfig.json`, то результат будет таким же, потому что его значение по умолчанию - «`.`». + +Однако результат будет другим, если мы также изменим `include`: + +```json +{ + "include": ["src/**/*"], + "compilerOptions": { + "outDir": "dist" + } +} +``` + +Теперь значение по умолчанию для `rootDir` равно `src`, и вывод будет таким: + +``` +/tmp/my-proj/ + dist/ + main.js +``` + +Поскольку значение `rootDir` по умолчанию меняется в зависимости от `include`, я предпочитаю указывать его явно в `tsconfig.json`. Но вы можете не указывать его, если вас устраивает то, как работает значение по умолчанию. + +### Карты исходного кода + +```json +{ + "compilerOptions": { + "sourceMap": true + } +} +``` + +`sourceMap` создает файлы карты исходных текстов, которые указывают на исходный TypeScript из транспилированного JavaScript. Это помогает при отладке и обычно является хорошей идеей. + +### Создание файлов `.d.ts` (например, для библиотек) + +Если мы хотим, чтобы код TypeScript потреблял наш транспилированный код TypeScript, нам обычно нужно включать файлы `.d.ts`: + +```json +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true // enables importers to jump to source + } +} +``` + +В качестве опции мы можем включить исходный код TypeScript в наш пакет npm и активировать `declarationMap`. Импортеры могут, например, щелкнуть на типах или перейти к определению значения, а их редактор отправит их к исходному коду. + +#### Опция `declarationDir` + +По умолчанию каждый файл `.d.ts` помещается рядом с файлом `.js`. Если вы хотите изменить это, вы можете использовать опцию [`declarationDir`](https://www.typescriptlang.org/tsconfig/#declarationDir). + +### Тонкая настройка выдаваемых файлов + +```json +{ + "compilerOptions": { + "newLine": "lf", + "removeComments": false + } +} +``` + +Значения, указанные выше, являются значениями по умолчанию. + +- `newLine` настраивает окончания строк для выдаваемых файлов. Допустимыми значениями являются: + - `"lf": "\n"` (Unix) + - `"crlf": "\r\n"` (Windows) +- `removeComments`: Если активна, все комментарии в файлах TypeScript будут опущены в транслируемых файлах JavaScript. Я слабо склоняюсь к тому, чтобы оставить значение по умолчанию и не удалять комментарии: + - Это помогает при чтении транслированного JavaScript - особенно если исходный код TypeScript не включен. + - Сборщики удаляют комментарии. + - На Node.js дополнительное бремя не имеет большого значения. + +## Особенности языка и платформы + +```json +{ + "compilerOptions": { + "target": "ES2024", + // Omit if you want to use the DOM + "lib": ["ES2024"] + } +} +``` + +### `target` + +`target` определяет, какой новый синтаксис JavaScript будет транспонирован в старый синтаксис. Например, если target - `"ES5"`, то стрелочная функция `() => {}` будет транспонирована в функцию выражения `function () {}`. + +- Рекомендуемые настройки для различных платформ можно посмотреть в репозитории GitHub [`tsconfig/bases`](https://github.com/tsconfig/bases). +- Значение `"ESNext"` означает «наивысшая версия, поддерживаемая установленным TypeScript». Поскольку это значение меняется между версиями TypeScript, оно может вызвать проблемы при обновлении. + +Я задаюсь вопросом, должна ли быть опция, позволяющая никогда не транспонировать JavaScript-функции. С другой стороны, возможность писать современный JavaScript на потенциально старых браузерах весьма удобна. + +#### Как выбрать хорошую целевую платформу + +Мы должны выбрать версию ECMAScript, которая подходит для наших целевых платформ. Есть две таблицы, которые дают хороший обзор: + +- Для браузеров: [compat-table.github.io](https://compat-table.github.io/compat-table/es2016plus/) +- Для Node.js: [node.green](https://node.green/) + +Мы также можем посмотреть [официальные базы tsconfig](https://github.com/tsconfig/bases), которые предоставляют значения для `target`. + +### `lib` + +`lib` определяет, какие типы для встроенных API доступны - например, `Math` или методы встроенных типов: + +- В [документации TypeScript](https://www.typescriptlang.org/tsconfig/#lib) описано, какие значения можно добавлять в массив. Полный их список можно найти в [репозитории TypeScript](https://github.com/microsoft/TypeScript/tree/main/src/lib). Если вы ищете тип, то ищите его в этих файлах! +- Там есть такие категории, как `"ES2024"` и `"DOM"`, и такие подкатегории, как `"DOM.Iterable"` и `"ES2024.Promise"`. +- Значения не зависят от регистра: Предложения автозаполнения Visual Studio Code содержат много заглавных букв, а имена файлов - ни одной. Значения `lib` могут быть записаны в любом случае. + +Когда TypeScript поддерживает тот или иной API? Он должен быть «доступен без префиксов/флагов как минимум в 2 браузерных движках (то есть не только в 2 браузерах chromium)» ([источник](https://github.com/microsoft/TypeScript/tree/main/src/lib)). + +#### Настройка `lib` через `target` + +`target` определяет значение `lib` по умолчанию: Если последний параметр опущен, а `target` - `"ES20YY"`, то будет использоваться `"ES20YY.Full"`. Однако это не то значение, которое мы можем использовать сами. Если мы хотим воспроизвести то, что делает удаление `lib`, мы должны сами перечислить содержимое (например, `es2024.full.d.ts`): + +```ts +/// +/// +/// +/// +/// +/// +``` + +В этом файле мы можем наблюдать интересное явление: + +- Категория `"ES20YY"` обычно включает все свои подкатегории. +- Категория `"DOM"` не включает - например, подкатегория `"DOM.Iterable"` еще не является ее частью. + +Помимо прочего, `"DOM.Iterable"` позволяет выполнять итерацию над списками NodeLists - например: + +```ts +for (const x of document.querySelectorAll('div')) { +} +``` + +### Типы для встроенных API Node.js + +Типы для API Node.js должны быть установлены с помощью [пакета npm](https://www.npmjs.com/package/@types/node): + +```bash +npm install @types/node +``` + +## Система модулей + +### `module`: Как TypeScript будет искать импортированные модули? + +Эти опции влияют на то, как TypeScript ищет импортированные модули: + +```json +{ + "compilerOptions": { + "module": "NodeNext", + "noUncheckedSideEffectImports": true + } +} +``` + +#### Опция `module` + +С помощью этой опции мы определяем системы для работы с модулями. Если мы правильно настроим ее, то позаботимся и о связанной с ней опции `moduleResolution`, для которой она предоставляет хорошие значения по умолчанию. Документация TypeScript рекомендует использовать одно из двух следующих значений: + +- Node.js: `"NodeNext"` поддерживает как CommonJS, так и последние возможности ESM. + - Подразумевает `"moduleResolution": "NodeNext"`. +- Bundlers: `"Preserve"` поддерживает как CommonJS, так и последние возможности ESM. Это поведение соответствует тому, что делает большинство бандлеров. + - Подразумевает `"moduleResolution": "bundler"`. + +Учитывая, что бандлеры в основном имитируют работу Node.js, я всегда использую `"NodeNext"` и не сталкивался с какими-либо проблемами. + +Обратите внимание, что в обоих случаях TypeScript заставляет нас указывать полные имена локальных модулей, которые мы импортируем. Мы не можем опускать расширения имен файлов, как это часто делалось, когда Node.js компилировался только в CommonJS. Новый подход отражает то, как работает ESM на чистом JavaScript. + +#### Опция `noUncheckedSideEffectImports` + +По умолчанию TypeScript не жалуется, если пустой импорт не существует. Причина такого поведения заключается в том, что это шаблон, поддерживаемый некоторыми бандлерами для связывания артефактов, не относящихся к TypeScript, с модулями. А TypeScript видит только файлы TypeScript. Вот как выглядит такой импорт: + +```ts +import './component-styles.css'; +``` + +Интересно, что TypeScript нормально воспринимает несуществующие файлы TypeScript, импортированные в пустоту. Он жалуется только в том случае, если мы импортируем что-то из несуществующего файла. + +```ts +import './does-not-exist.js'; // no error! +``` + +Установка значения `noUncheckedSideEffectImports` в `true` изменит это. Позже я расскажу об альтернативе для импорта артефактов, не относящихся к TypeScript. + +### Запуск TypeScript напрямую (без генерации JS-файлов) + +Большинство небраузерных JavaScript-платформ теперь могут запускать код TypeScript напрямую, без его транспонирования. + +```json +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + // Only needed if compiling to JavaScript: + "rewriteRelativeImportExtensions": true + } +} +``` + +- `allowImportingTsExtensions`: Эта опция позволяет нам при импорте ссылаться на TypeScript-версию модуля, а не на его транслируемую версию. +- `rewriteRelativeImportExtensions`: С помощью этой опции мы также можем транслировать код TypeScript, предназначенный для непосредственного выполнения. По умолчанию TypeScript не изменяет спецификаторы модулей при импорте. Эта опция имеет несколько оговорок: + - Переписываются только относительные пути. + - Они переписываются «наивно» - без учета опций `baseUrl` и `paths` (которые выходят за рамки этой статьи). + - Пути, проложенные через свойства `"exports"` и `"imports"`, не выглядят как относительные пути и поэтому также не переписываются. + +Если вы хотите использовать `tsc` для проверки типов (только), то обратите внимание на раздел, посвященный опции `noEmit`. + +Для получения дополнительной информации о встроенной поддержке TypeScript в Node вы можете прочитать [мою статью](https://2ality.com/2025/01/nodejs-strip-type.html) в блоге. + +### Импортирование JSON + +```json +{ + "compilerOptions": { + "resolveJsonModule": true + } +} +``` + +Опция `resolveJsonModule` позволяет импортировать файлы JSON: + +```ts +import config from './config.json' with {type: 'json'}; +console.log(config.hello); +``` + +### Импорт других артефактов, не относящихся к TypeScript + +Всякий раз, когда мы импортируем файл `basename.ext`, расширение которого `ext` TypeScript не знает, он ищет файл `basename.d.ext.ts`. Если он его не находит, то выдает ошибку. В документации по TypeScript есть [хороший пример](https://www.typescriptlang.org/tsconfig/#allowArbitraryExtensions) того, как может выглядеть такой файл. + +Есть два способа предотвратить появление в TypeScript ошибок, связанных с неизвестным импортом. + +Во-первых, мы можем использовать опцию `allowArbitraryExtensions`, чтобы предотвратить любое сообщение об ошибке в этом случае. + +Во-вторых, мы можем создать объявление окружающего модуля со спецификатором wildcard - файл `.d.ts`, который должен находиться где-то среди файлов, о которых знает TypeScript. Следующий пример подавляет ошибки для всех импортов с расширением имени файла `.css`: + +```ts +// ./src/globals.d.ts +declare module '*.css' {} +``` + +## Проверка типов + +```json +{ + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true + } +} +``` + +`strict` является обязательным, на мой взгляд. С остальными настройками вы должны сами решить, нужна ли вам дополнительная строгость для вашего кода. Вы можете начать с добавления всех из них и посмотреть, какие из них вызывают слишком много проблем на ваш вкус. В этом разделе мы будем игнорировать настройки, которые покрываются `strict` (например, `noImplicitAny`). + +- `noFallthroughCasesInSwitch`: Если `true`, то непустые случаи `switch` должны заканчиваться `break`, `return` или `throw`. +- `noImplicitOverride`: Если `true`, то методы, переопределяющие методы суперкласса, должны иметь модификатор `override`. +- `noImplicitReturns`: Если `true`, то «неявный возврат» (завершение функции или метода) разрешен только в том случае, если тип возврата - `void`. + +### `exactOptionalPropertyTypes` + +Если `true`, то `.colorTheme` может быть только опущен, а не установлен в значение `undefined`, как в следующем примере: + +```ts +interface Settings { + // Absent property means “system” + colorTheme?: 'dark' | 'light'; +} +const obj1: Settings = {}; // allowed +const obj2: Settings = { colorTheme: undefined }; // not allowed +``` + +### `noPropertyAccessFromIndexSignature` + +Если `true`, то для типов, подобных следующему, мы не можем использовать точечную нотацию для неизвестных свойств, только для известных: + +```ts +interface ObjectWithId { + id: string; + [key: string]: string; +} +declare const obj: ObjectWithId; + +const value1 = obj.id; // allowed +const value2 = obj['unknownProp']; // allowed +const value3 = obj.unknownProp; // not allowed +``` + +### `noUncheckedIndexedAccess` + +Если `true`, то тип неизвестного свойства является объединением `undefined` и типа индексной подписи: + +```ts +interface ObjectWithId { + id: string; + [key: string]: string; +} +declare const obj: ObjectWithId; +expectType(obj.id); +expectType(obj.unknownProp); +``` + +### `noUncheckedIndexedAccess` и массивы + +Опция `noUncheckedIndexedAccess` также влияет на работу с массивами: + +```ts +const arr = ['a', 'b']; +const elem = arr[0]; +expectType(elem); +``` + +Если этот параметр равен `false`, то `elem` имеет тип `string`. + +Одним из распространенных шаблонов для массивов является проверка длины перед обращением к элементу. Однако эта схема становится неудобной при использовании `noUncheckedIndexedAccess`: + +```ts +function logElemAt0(arr: Array) { + if (0 < arr.length) { + const elem = arr[0]; + expectType(elem); + console.log(elem); + } +} +``` + +Поэтому разумнее использовать другой шаблон: + +```ts +function logElemAt0(arr: Array) { + if (0 in arr) { + const elem = arr[0]; + expectType(elem); + console.log(elem); + } +} +``` + +Я размышляю над этим вариантом: С одной стороны, новый шаблон отражает, что массивы могут содержать дыры. С другой стороны, дыры встречаются редко, и, начиная с ES6, JavaScript делает вид, что это элементы, имеющие значение `undefined`: + +``` +> Array.from([,,,]) +[ undefined, undefined, undefined ] +``` + +### Параметры проверки типов, которые имеют хорошие значения по умолчанию + +По умолчанию следующие опции выдают предупреждения в редакторах, но мы также можем выбрать выдачу ошибок компилятора или игнорирование проблем: + +- `allowUnreachableCode` +- `allowUnusedLabels` +- `noUnusedLocals` +- `noUnusedParameters` + +## Совместимость: помощь внешним инструментам в компиляции TypeScript в JavaScript и декларации + +```json +{ + "compilerOptions": { + "verbatimModuleSyntax": true, + "isolatedDeclarations": true + } +} +``` + +Компилятор TypeScript выполняет три задачи: + +- Проверка типов +- Эмиссия файлов JavaScript +- Эмиссия файлов деклараций + +В настоящее время стали популярны внешние инструменты, которые выполняют задачи #2 и #3 гораздо быстрее. У них есть два требования: + +- Выдача выходного файла не должна требовать поиска информации в файлах, импортированных входным файлом. +- Он также не должен требовать семантического анализа; только синтаксический анализ. + +Есть две настройки, которые обеспечивают статическое соблюдение этих ограничений - они вызывают ошибки компилятора, но не изменяют способ вывода JavaScript и деклараций: + +- `verbatimModuleSyntax` помогает при компиляции TypeScript в JavaScript. +- `isolatedDeclarations` помогает компилировать TypeScript в декларации. + +### `verbatimModuleSyntax`: компиляция TypeScript в JavaScript + +Большинство частей файла TypeScript, не относящихся к JavaScript, легко обнаружить. Исключением являются импорты: Без (относительно простого) семантического анализа мы не знаем, является ли импорт типом (TypeScript) или значением (JavaScript). + +Если активен `verbatimModuleSyntax`, мы вынуждены добавлять ключевое слово `type` к импортам, относящимся только к типам - например: + +```ts +// Input +import {type SomeInterface, SomeClass} from './my-module.js'; + +// Output +import {SomeClass} from './my-module.js'; +``` + +Обратите внимание, что класс - это и значение, и тип. В этом случае ключевое слово `type` не нужно, потому что эта часть синтаксиса может оставаться в обычном JavaScript. + +Нам также нужно добавить `type`, если мы упоминаем тип в предложении экспорта: + +```ts +interface MyInterface {} +export {type MyInterface}; + +// Alternative: +export interface MyInterface {} +``` + +#### `isolatedModules` + +Активация `verbatimModuleSyntax` также активирует `isolatedModules`, поэтому нам нужна только первая настройка. Вторая не позволяет нам использовать некоторые более непонятные функции, которые также являются проблематичными. + +Кроме того, эта опция позволяет esbuild компилировать TypeScript в JavaScript ([исходный текст](https://esbuild.github.io/content-types/#isolated-modules)). + +#### `isolatedDeclarations`: компиляция TypeScript в декларации + +`isolatedDeclarations` в основном заставляет нас добавлять аннотации возвращаемого типа к экспортируемым функциям и методам. Это означает, что внешним инструментам не придется выводить возвращаемые типы. + +**Дополнительное чтение**: В примечаниях к выпуску TypeScript 5.5 есть [обширный раздел](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations), посвященный изолированным объявлениям. + +#### `noEmit`: использование tsc только для проверки типов + +Иногда мы хотим использовать tsc только для проверки типов - например, если мы запускаем TypeScript напрямую или используем внешние инструменты для компиляции файлов TypeScript (в файлы JavaScript, файлы деклараций и т. д.): + +```json +{ + "compilerOptions": { + "noEmit": true + } +} +``` + +- `noEmit`: Если значение равно `true`, мы можем запустить `tsc`, и он будет проверять только код TypeScript, но не будет выдавать никаких файлов. + +Нужно ли дополнительно удалять опции, связанные с выводом, зависит от того, какие из них используются вашими внешними инструментами. + +## Импорт CommonJS из ESM + +Одна из ключевых проблем связана с импортом модуля CommonJS из модуля ESM: + +- В ESM экспорт по умолчанию - это свойство `.default` объекта пространства имен модуля. +- В CommonJS экспортом по умолчанию является объект модуля - например, многие модули CommonJS устанавливают `module.exports` в функцию. + +Давайте рассмотрим две опции, которые помогут вам в этом. + +### `allowSyntheticDefaultImports`: проверка типов импорта по умолчанию модулей CommonJS + +Эта опция влияет только на проверку типов, но не на JavaScript-код, создаваемый TypeScript: Если она активна, импорт по умолчанию модуля CommonJS будет ссылаться на `module.exports` (а не на `module.exports.default`) - но только если нет `module.exports.default`. + +Это отражает то, как Node.js обрабатывает импорт по умолчанию модулей CommonJS ([источник](https://nodejs.org/api/esm.html#interoperability-with-commonjs)): «При импорте модулей CommonJS объект `module.exports` предоставляется в качестве экспорта по умолчанию. Могут быть доступны именованные экспорты, предоставляемые статическим анализом в качестве удобства для лучшей совместимости с экосистемой.» + +**Нужна ли нам эта опция?** Да, но она автоматически активируется, если `moduleResolution` имеет значение `"bundler"` или если `module` имеет значение `"NodeNext"` (что активирует `esModuleInterop`, который активирует `allowSyntheticDefaultImports`). + +### `esModuleInterop`: улучшенная компиляция TypeScript в код CommonJS + +Эта опция влияет на эмулируемый код CommonJS: + +- Если `false`: + - `import * as m from 'm'` компилируется в `const m = require('m')`. + - `import m from 'm'` (приблизительно) компилируется в `const m = require('m')`, а каждое обращение к `m` компилируется в `m.default`. +- Если `true`: + - `import * as m from 'm'` присваивает `m` новый объект, который имеет те же свойства, что и `module.exports`, плюс свойство `.default`, которое ссылается на `module.exports`. + - `import m from 'm'` присваивает новый объект `m`, который имеет единственное свойство `.default`, ссылающееся на `module.exports`. Каждое обращение к `m` компилируется в `m.default`. +- Если модуль CommonJS имеет маркерное свойство `.__esModule`, то он всегда импортируется так, как если бы `esModuleInterop` был выключен. + +**Нужна ли нам эта опция?** Нет, так как мы создаем только модули ESM. + +## Другие опции с хорошими значениями по умолчанию + +Обычно мы можем игнорировать эти опции: + +- `moduleDetection`: Эта опция определяет, как TypeScript определяет, является ли файл скриптом или модулем. Обычно его можно не указывать, потому что его значение по умолчанию `"auto"` хорошо работает в большинстве случаев. Вам нужно явно установить значение `"force"`, только если в вашей кодовой базе есть модуль, у которого нет ни импорта, ни экспорта. Если `module` имеет значение `"NodeNext"` и в `package.json` есть `"type": "module"`, то даже эти файлы будут интерпретироваться как модули. +- `skipLibCheck`: Если вы не делаете ничего сложного с файлами деклараций, вы можете проигнорировать эту опцию и просто использовать настройку по умолчанию. Согласно [обсуждению на GitHub](https://github.com/sindresorhus/tsconfig/issues/15), установка этого параметра в `true` (по умолчанию `false`) имеет много минусов. + +## Параметры `package.json`, учитываемые TypeScript + +TypeScript учитывает несколько свойств `package.json`: + +- `type`: Это важный параметр. Если вы компилируете в модули ESM, ваш `package.json` всегда должен содержать: + + ```json + { + "type": "module" + } + ``` + +- `exports` определяет, какие файлы пакета будут публично видны, и ремапирует пути (чтобы то, что видят импортеры, отличалось от реальных внутренних путей). Все эти настройки могут применяться условно - в зависимости от среды импорта (браузеры, Node.js и т. д.). Более подробную информацию можно найти в моем блоге в статье [«TypeScript и нативный ESM на Node.js»](https://2ality.com/2021/06/typescript-esm-nodejs.html). Одна из интересных особенностей экспорта пакетов заключается в том, что мы можем ссылаться на свой собственный пакет через «голый» импорт, и при этом будут применяться правила экспорта пакетов. Это полезно для модульных тестов. + +- `imports` позволяет нам определять сокращения, такие как `#util`, для внутренних модулей и внешних пакетов. Более подробную информацию можно найти в главе [«Пакеты: Единицы JavaScript для распространения программного обеспечения»](https://exploringjs.com/nodejs-shell-scripting/ch_packages.html#package-imports) моей книги „Shell scripting with Node.js“. + +## Visual Studio Code + +Если вас не устраивают спецификаторы модулей для локального импорта в автоматически создаваемых импортах, то вы можете воспользоваться следующими двумя настройками: + +``` +javascript.preferences.importModuleSpecifierEnding +typescript.preferences.importModuleSpecifierEnding +``` + +По умолчанию VSC должен быть достаточно умным, чтобы добавлять расширения имен файлов там, где это необходимо. + +## Резюме + +После проведения исследований для этой статьи в блоге, это «база», которую я использую в настоящее время. В следующих подразделах объясняется, как адаптировать ее для различных случаев использования. + +```json +{ + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + // Specify explicitly (don’t derive from source file paths): + "rootDir": ".", + "outDir": "dist", + + //===== Output: JavaScript ===== + "target": "ES2024", + "module": "NodeNext", // sets up "moduleResolution" + // Emptily imported modules must exist + "noUncheckedSideEffectImports": true, + // + "sourceMap": true, // .js.map files + + //===== Interoperability: help external tools ===== + // Helps tools that compile .ts to .js by enforcing + // `type` modifiers for type-only imports etc. + "verbatimModuleSyntax": true, + + //===== Type checking ===== + "strict": true, // activates several useful options + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + + //===== Other options ===== + // Lets us import JSON files: + "resolveJsonModule": true + } +} +``` + +Примечания: + +- Для получения дополнительной информации о выборе хорошего `target` см. раздел, посвященный этой теме, ранее в этом блоге. +- `verbatimModuleSyntax`: Мне нравятся ограничения, которые он накладывает на мой код: tsc работает без них, но они необходимы для многих инструментов, которые компилируют TypeScript в JavaScript. + +### npm package (библиотеки и т. д.) + +```json +{ + "compilerOptions": { + // ··· + //===== Output: declarations ===== + "declaration": true, // .d.ts files + // “Go to definition” jumps to TS source etc.: + "declarationMap": true, // .d.ts.map files + + //===== Interoperability: help external tools ===== + // Helps tools that compile .ts to .d.ts by enforcing + // return type annotations for exported functions, etc. + "isolatedDeclarations": true, + + //===== Misc ===== + "lib": ["ES2024"] // don’t provide types for DOM + } +} +``` + +Примечание: Если ваша библиотека использует DOM, вам следует убрать `"lib"`. + +Я бы с удовольствием всегда использовал `isolatedDeclarations`, но TypeScript позволяет это делать только в том случае, если активна опция `declaration` или опция `composite`. Джейк Бейли [объясняет](https://github.com/microsoft/TypeScript/issues/58262#issuecomment-2597014286), почему это так: + +На уровне реализации диагностика `isolatedDeclarations` - это дополнительная диагностика декларации, производимая трансформатором декларации, которую мы запускаем только тогда, когда `declaration` включена. + +Теоретически можно реализовать так, что `isolatedDeclarations` включает эти проверки (диагностика на самом деле происходит от того, что мы запускаем трансформатор и затем отбрасываем полученный AST), но это изменение по сравнению с первоначальным дизайном. + +### Приложение Node.js + +```json +{ + "compilerOptions": { + // ··· + //===== Misc ===== + "lib": ["ES2024"] // don’t provide types for DOM + } +} +``` + +### Веб-приложение + +`"module":"NodeNext"` должен хорошо работать и для бандлеров. Но вы можете перейти на более специфичный для бандлеров `"module":"preserve"`. + +### Запуск TypeScript напрямую + +```json +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + // Only needed if compiling to JavaScript: + "rewriteRelativeImportExtensions": true + } +} +``` + +Дополнительную информацию см. в разделе «Выполнение TypeScript напрямую». + +### Использование tsc только для проверки типов + +```json +{ + "compilerOptions": { + "noEmit": true + } +} +``` + +Дополнительные сведения см. в разделе «Использование tsc только для проверки типов». + +## Рекомендации по использованию `tsconfig.json` от других людей + +- Мэтт Покок [«Шпаргалка по TSConfig»](https://www.totaltypescript.com/tsconfig-cheat-sheet) +- [`base.json`](https://github.com/voxpelli/tsconfig/blob/main/base.json) Пелле Вессмана +- [`tsconfig.json`](https://github.com/sindresorhus/tsconfig/blob/main/tsconfig.json) Синдре Сорхуса + +## Источники этой статьи в блоге + +Некоторые источники уже были упомянуты ранее. Это дополнительные источники, которые я использовал: + +- [Официальная документация TSConfig](https://www.typescriptlang.org/tsconfig/) +- Раздел [«Path Rewriting for Relative Paths»](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/#path-rewriting-for-relative-paths) в анонсе TypeScript 5.7. +- В документации по esbuild содержатся [интересные наблюдения](https://esbuild.github.io/content-types/#typescript) о компиляции TypeScript. + +:material-information-outline: Источник — diff --git a/docs/book/browser.md b/docs/book/browser.md index bb9d900..2716193 100644 --- a/docs/book/browser.md +++ b/docs/book/browser.md @@ -145,7 +145,7 @@ ReactDOM.render( ); ``` -## Разрабатывайте свое удивительное приложение +## Разрабатывайте свое удивительное приложение {#develop-your-amazing-application} > Вы можете установить самые последние пакеты, используя `npm install typescript@latest react@latest react-dom@latest @types/react@latest @types/react-dom@latest webpack@latest webpack-dev-server@latest webpack-cli@latest ts-loader@latest clean-webpack-plugin@latest html-webpack-plugin@latest --save-exact` diff --git a/docs/book/types/type-assertion.md b/docs/book/types/type-assertion.md index 453bea2..2158175 100644 --- a/docs/book/types/type-assertion.md +++ b/docs/book/types/type-assertion.md @@ -22,7 +22,7 @@ foo.bar = 123; foo.bas = 'hello'; ``` -## `as foo` против `` +## `as foo` против `` {#as-foo-vs-foo} Первоначально был добавлен синтаксис ``. Это продемонстрировано ниже: diff --git a/docs/boosty-color.svg b/docs/boosty-color.svg new file mode 100644 index 0000000..f00cf27 --- /dev/null +++ b/docs/boosty-color.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/docs/boosty-dark.svg b/docs/boosty-dark.svg new file mode 100644 index 0000000..2f6799a --- /dev/null +++ b/docs/boosty-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/docs/boosty-white.svg b/docs/boosty-white.svg new file mode 100644 index 0000000..f6b84a1 --- /dev/null +++ b/docs/boosty-white.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/docs/db.svg b/docs/db.svg new file mode 100644 index 0000000..a82ada1 --- /dev/null +++ b/docs/db.svg @@ -0,0 +1,11 @@ + + + diff --git a/docs/guide/012.md b/docs/guide/012.md index 0fa0c0a..90c74ac 100644 --- a/docs/guide/012.md +++ b/docs/guide/012.md @@ -42,7 +42,7 @@ function weight(fruit) { } ``` -Кроме того, если функция возвращает значение, принадлежащее к типу, который компилятор не в состоянии вывести, то возвращаемый этой функцией тип данных будет также будет выведен как тип `any`. +Кроме того, если функция возвращает значение, принадлежащее к типу, который компилятор не в состоянии вывести, то возвращаемый этой функцией тип данных будет также выведен как тип `any`. ```ts function sum(a, b) { diff --git a/docs/guide/014.md b/docs/guide/014.md index e78039c..4f00553 100644 --- a/docs/guide/014.md +++ b/docs/guide/014.md @@ -24,6 +24,7 @@ class TypeSystem { static number: number = null; // Ok static string: string = null; // Ok static boolean: boolean = null; // Ok + static undefined: undefined = null; // Ok static null: null = null; // Ok } ``` @@ -35,6 +36,7 @@ TypeSystem.null = TypeSystem.any; // Ok TypeSystem.null = TypeSystem.number; // Error TypeSystem.null = TypeSystem.string; // Error TypeSystem.null = TypeSystem.boolean; // Error +TypeSystem.null = TypeSystem.undefined; // Ok TypeSystem.null = TypeSystem.null; // Ok ``` @@ -52,6 +54,7 @@ class TypeSystem { static number: number = null; // Error static string: string = null; // Error static boolean: boolean = null; // Error + static undefined: undefined = null; // Error static null: null = null; // Ok } @@ -59,7 +62,8 @@ TypeSystem.null = TypeSystem.any; // Ok TypeSystem.null = TypeSystem.number; // Error TypeSystem.null = TypeSystem.string; // Error TypeSystem.null = TypeSystem.boolean; // Error -TypeSystem.null = TypeSystem.undefined; // Ok +TypeSystem.null = TypeSystem.undefined; // Error +TypeSystem.null = TypeSystem.null; // Ok ``` При активном флаге `--strictNullChecks`, при условии, что в качестве значения выступает значение `null`, вывод типов определяет принадлежность к типу `null`. @@ -103,6 +107,7 @@ TypeSystem.undefined = TypeSystem.number; // Error TypeSystem.undefined = TypeSystem.string; // Error TypeSystem.undefined = TypeSystem.boolean; // Error TypeSystem.undefined = TypeSystem.null; // Ok +TypeSystem.undefined = TypeSystem.undefined; // Ok ``` Тогда, когда тип данных `undefined` указывается не явно, компилятор устанавливает тип `any`. @@ -128,6 +133,7 @@ TypeSystem.undefined = TypeSystem.number; // Error TypeSystem.undefined = TypeSystem.string; // Error TypeSystem.undefined = TypeSystem.boolean; // Error TypeSystem.undefined = TypeSystem.null; // Error +TypeSystem.undefined = TypeSystem.undefined; // Ok ``` При активном флаге `--strictNullChecks`, при условии, что в качестве значения выступает значение `undefined`, вывод типов определяет принадлежность к типу `undefined`. @@ -168,7 +174,7 @@ TypeSystem.void = TypeSystem.undefined; // Ok TypeSystem.void = TypeSystem.void; // Ok ``` -Однако с активным флагом `--strictNullChecks`, тип данных `void` совместим лишь с `any` и `undefined`. +Однако с активным флагом `--strictNullChecks`, тип данных `void`, помимо самого себя, совместим лишь с `any` и `undefined`. ```ts function action(): void {} diff --git a/docs/guide/015.md b/docs/guide/015.md index 8b24944..b043953 100644 --- a/docs/guide/015.md +++ b/docs/guide/015.md @@ -296,7 +296,7 @@ enum Stones { Может возникнуть мысль использовать перечисление в качестве типа данных переменной или параметра. Это вполне нормальное желание, но нужно быть очень осторожным: в _TypeScript_ с перечислением связан один достаточно неприятный нюанс. Дело в том, что пока в перечислении есть хотя бы одна константа с числовым значением, он будет совместим с типом `number`. Простыми словами, любое число проходит проверку совместимости типов с любым перечислением. -Функцию, тип параметра которой является смешанным перечислением, благополучно получится вызвать как с константой перечисления в качестве аргумента, так и с любым числом. Вызвать эту же функцию с идентичной константе перечисления строкой уже не получится. +Функцию, тип параметра которой является смешанным перечислением, благополучно получится вызвать как с константой перечисления в качестве аргумента, так и с любым числом (до 5 версии TS). Вызвать эту же функцию с идентичной константе перечисления строкой уже не получится. ```ts enum Fruits { diff --git a/docs/guide/017.md b/docs/guide/017.md index ba9817a..0008f56 100644 --- a/docs/guide/017.md +++ b/docs/guide/017.md @@ -198,7 +198,7 @@ animalSignValidate(new InsectSignDataProvider()); ```ts let data = { x: 0, y: 0 }; -function valid(standart: data) { +function valid(standart: typeof data) { // data, что это? } ``` diff --git a/docs/guide/018.md b/docs/guide/018.md index 3d7a5e5..a61c6b4 100644 --- a/docs/guide/018.md +++ b/docs/guide/018.md @@ -91,9 +91,9 @@ type Type = "Type"; type Script = "Script"; /** - * type Messge = "I ❤️ TypeScript" + * type Message = "I ❤️ TypeScript" */ -type Messge = `I ❤️ ${Type}${Script}`; +type Message = `I ❤️ ${Type}${Script}`; ``` Но вся мощь данного типа раскрывается в момент определения нового типа на основе объединения (`union`). В подобных случаях новый тип будет также представлять объединение, элементы которого представляют все возможные варианты, полученные на основе исходного объединения. diff --git a/docs/guide/021.md b/docs/guide/021.md index d9e6039..37a0003 100644 --- a/docs/guide/021.md +++ b/docs/guide/021.md @@ -162,7 +162,7 @@ let bird: IAnimal = new Bird(); let raven: IAnimal = new Raven(); ``` -Класс, реализующий множество интерфейсов, принадлежит к типу каждого из них. Когда экземпляр класса, реализующего интерфейс, присваивают ссылке с типом интерфейса, то говорят, что экземпляр был _ограничен_ типом интерфейса. То есть, функционал экземпляра класса урезается до описанного в интерфейсе (подробнее об этом речь пойдет в главе [Совместимость объектов](038.md)) и [Совместимость функций](039.md)). +Класс, реализующий множество интерфейсов, принадлежит к типу каждого из них. Когда экземпляр класса, реализующего интерфейс, присваивают ссылке с типом интерфейса, то говорят, что экземпляр был _ограничен_ типом интерфейса. То есть, функционал экземпляра класса урезается до описанного в интерфейсе (подробнее об этом речь пойдет в главе [Совместимость объектов](038.md) и [Совместимость функций](039.md)). ```ts interface IAnimal { @@ -204,7 +204,7 @@ interface ILiving {} interface IAnimal extends IIdentifiable, ILiving {} ``` -Для тех, кто только знакомится понятием интерфейса, будет нелишним узнать о _“Принципе разделения интерфейсов”_ (_Interface Segregation Principle_ или сокращенно _ISP_), который гласит, что более крупные интерфейсы нужно _“дробить”_ на более мелкие интерфейсы. Но нужно понимать, что условия дробления диктуются конкретным приложением. Если во всех случаях руководствоваться только принципами, то можно раздуть небольшое приложение до масштабов вселенной. +Для тех, кто только знакомится с понятием интерфейса, будет нелишним узнать о _“Принципе разделения интерфейсов”_ (_Interface Segregation Principle_ или сокращенно _ISP_), который гласит, что более крупные интерфейсы нужно _“дробить”_ на более мелкие интерфейсы. Но нужно понимать, что условия дробления диктуются конкретным приложением. Если во всех случаях руководствоваться только принципами, то можно раздуть небольшое приложение до масштабов вселенной. Для примера представьте приложение, которое только выводит в консоль информацию о животных. Так как над объектом `Animal` будет выполняться только одна операция, то можно не бояться разгневать богов объектно-ориентированного проектирования и включить все нужные характеристики прямо в интерфейс `IAnimal`. @@ -385,7 +385,7 @@ let fish: IAnimal = new Fish(); // Error Описание интерфейса для функции конструктора может потребоваться, когда в качестве значения выступает сам класс. -Конструктор указывается с помощью ключевого слова `new`, затем открываются фигурные скобки, в которых при наличии указываются параметры, а в конце указывается тип возвращаемого значения. +Конструктор указывается с помощью ключевого слова `new`, затем открываются круглые скобки, в которых при наличии указываются параметры, а в конце указывается тип возвращаемого значения. ```ts new(p1: type, p2: type): type; @@ -415,10 +415,10 @@ class Bird extends Animal { } class Fish extends Animal { - static DEFAULT_NAME: string = 'bird'; + static DEFAULT_NAME: string = 'fish'; static create(): IAnimal { - return new Bird(Bird.DEFAULT_NAME); + return new Fish(Fish.DEFAULT_NAME); } } @@ -631,11 +631,11 @@ interface IAnimalFactory { } /** при компиляции -interface IAnimalFactory { - getAnimalByID(id: string): IInsect; - getAnimalByID(id: string): IReptile; - getAnimalByID(id: string): IFish; - getAnimalByID(id: string): IBird; +interface IAnimalFactory { + getAnimalByID(id: number): IBird; + getAnimalByID(id: string): IFish; + getAnimalByID(id: boolean): IInsect; + getAnimalByID(id: object): IReptile; } */ let animal: IAnimalFactory; @@ -669,10 +669,10 @@ interface IAnimalFactory { /** при компиляции interface IAnimalFactory { + getAnimalByID(id: string): IBird; getAnimalByID(id: 'fish'): IFish; getAnimalByID(id: 'insect'): IInsect; getAnimalByID(id: number): IReptile; - getAnimalByID(id: string): IBird; } */ ``` diff --git a/docs/guide/031.md b/docs/guide/031.md index f228f2f..745dfd4 100644 --- a/docs/guide/031.md +++ b/docs/guide/031.md @@ -131,7 +131,7 @@ function handler(event?: UserEvent): void { type Target = { name: string }; -type Currenttarget = { name }; +type Currenttarget = { name: string }; type UserEvent = { type: string; @@ -153,7 +153,7 @@ function handler(event?: UserEvent): void { type Target = { name: string }; -type Currenttarget = { name }; +type Currenttarget = { name: string }; type UserEvent = { type: string; @@ -177,7 +177,7 @@ function handler(event?: UserEvent): void { type Target = { name: string }; -type Currenttarget = { name }; +type Currenttarget = { name: string }; type UserEvent = { type: string; @@ -203,7 +203,7 @@ function handler(event?: any): void { type Target = { name: string }; -type Currenttarget = { name }; +type Currenttarget = { name: string }; type UserEvent = { type: string; @@ -229,7 +229,7 @@ function handler(event: UserEvent | undefined): void { type Target = { name: string }; -type Currenttarget = { name }; +type Currenttarget = { name: string }; type UserEvent = { type: string; @@ -265,7 +265,7 @@ function initialize() { } ``` -Чтобы избежать ошибки при обращении к переменным, которые инициализированы в чужом контексте, нужно использовать _definite assignment assertions_. _definite assignment assertions_ также указывается с помощью символа восклицательного знака (`!`) и располагается после идентификатора переменной. Указывая данный оператор каждый раз при обращении к переменной, разработчик сообщает компилятору, что берет на себя все проблемы, которые могут быть вызваны отсутствием значения у переменной. +Чтобы избежать ошибки при обращении к переменным, которые инициализированы в чужом контексте, нужно использовать _definite assignment assertions_. _Definite assignment assertions_ также указывается с помощью символа восклицательного знака (`!`) и располагается после идентификатора переменной. Указывая данный оператор каждый раз при обращении к переменной, разработчик сообщает компилятору, что берет на себя все проблемы, которые могут быть вызваны отсутствием значения у переменной. ```ts let value: number; diff --git a/docs/guide/032.md b/docs/guide/032.md index ca4d0a2..61a2332 100644 --- a/docs/guide/032.md +++ b/docs/guide/032.md @@ -353,7 +353,7 @@ class Collection { this.itemAll.push(item); } - public getItemByName(name: string): T { + public getItemByName(name: string): T | undefined { return this.itemAll.find((item) => item.name === name); // Error -> Property 'name' does not exist on type 'T' } } @@ -373,7 +373,7 @@ class Collection { this.itemAll.push(item); } - public getItemByName(name: string): T { + public getItemByName(name: string): T | undefined { return this.itemAll.find((item) => item.name === name); // Ok } } @@ -658,7 +658,7 @@ class Collection { this.itemAll.push(item); } - public getItemByName(name: string): T { + public getItemByName(name: string): T | undefined { return this.itemAll.find((item) => item.name === name); // Ok } } @@ -686,10 +686,10 @@ class Collection { // 1. параметр типа U должен расширять параметр типа T // 2. возвращаемый тип указан как U // 3. возвращаемое значение нуждается в явном преобразовании к типу U - public getItemByName(name: string): U { + public getItemByName(name: string): U | undefined { return this.itemAll.find( (item) => item.name === name - ) as U; // Ok + ) as U | undefined; // Ok } } @@ -697,7 +697,7 @@ let collection: Collection = new Collection(); collection.add(new Bird('bird')); collection.add(new Fish('fish')); -var bird: Bird = collection.getItemByName('bird'); // Ok +var bird: Bird | undefined = collection.getItemByName('bird'); // Ok var birdOrFish = collection.getItemByName('bird'); // Bad, var birdOrFish: Bird | Fish var bird = collection.getItemByName('bird'); // Ok, var bird: Bird ``` diff --git a/docs/guide/033.md b/docs/guide/033.md index 2d4dc20..cb3b679 100644 --- a/docs/guide/033.md +++ b/docs/guide/033.md @@ -50,7 +50,7 @@ function move(animal: Bird | Fish | Insect): void { /** * [*] * - * Поскольку вывод типо не может + * Поскольку вывод типов не может * определить, к какому конкретно * из трех типов принадлежит параметр * animal, он не позволяет обращаться к diff --git a/docs/guide/035.md b/docs/guide/035.md index 6b54a1b..134d8da 100644 --- a/docs/guide/035.md +++ b/docs/guide/035.md @@ -373,7 +373,7 @@ const testScope = (text: any) => { isStringAssert(typeof text === 'string'); // выражение с оператором typeof isStringAssert(isString(text)); // механизм "утверждения типа" - text.touppercase(); // ..после утверждениея как тип string + text.touppercase(); // ..после утверждения как тип string }; ``` diff --git a/docs/guide/037.md b/docs/guide/037.md index b4b1583..eb4c6ab 100644 --- a/docs/guide/037.md +++ b/docs/guide/037.md @@ -279,8 +279,8 @@ class E extends D { f3 = {}; } -let v3 = [new A(), new B(), new C(), new D(), new E()]; // let v3: A[] -let v4 = [new B(), new C(), new D(), new E()]; // let v4: (B | C | D)[] +let v0 = [new A(), new B(), new C(), new D(), new E()]; // let v0: A[] +let v1 = [new B(), new C(), new D(), new E()]; // let v1: (B | C | D)[] ``` Те же самые правила применяются при выводе типа значения, возвращаемого тернарным оператором. @@ -303,6 +303,7 @@ class E extends D { let v0 = false ? new A() : new B(); // let v0: A let v1 = false ? new B() : new C(); // let v1: B | C let v2 = false ? new C() : new D(); // let v2: C | D +let v3 = false ? new A() : new E(); // let v3: A ``` Так как выражение, расположенное в блоке тернарного оператора, вычисляется на этапе выполнения программы, вывод типов не может знать результата его вычисления на этапе компиляции. Поэтому, чтобы не нарушить типобезопасность, он вынужден указывать объединенный тип, определяемый всеми блоками выражения. @@ -390,7 +391,7 @@ function f( return rest; } -let l = f(5).length; // let l: 1 | 2 | 3 +let l = f(5).length; // let l: 3 | 2 | 1 ``` Кроме того, остаточные параметры (`...rest`), аннотированные с помощью параметра типа, рассматриваются и представляются выводом типа как принадлежащие к типу-кортежу. diff --git a/docs/guide/042.md b/docs/guide/042.md index 8fa58f9..0e3157a 100644 --- a/docs/guide/042.md +++ b/docs/guide/042.md @@ -103,7 +103,7 @@ interface IInterfaceType { let v1: IInterfaceType['p1']; // v1: number let v2: IInterfaceType['p2']; // v2: string let union: IInterfaceType['p1' | 'p2']; // union: number | string -let notexist: IInterfaceType['notexist']; // Error -> Property 'notexist' does not exist on type 'IAnimal' +let notexist: IInterfaceType['notexist']; // Error -> Property 'notexist' does not exist on type 'IInterfaceType' ``` Если в качестве типа выступает класс, то получить типы можно только у членов его экземпляра. При попытке получить тип несуществующего члена возникнет ошибка. @@ -339,7 +339,7 @@ type T = { Таким образом, совмещая данный механизм с _шаблонными литеральными строковыми типами_, можно добиться переопределения исходных ключей. ```ts -type ToGetter = `get${capitalize T}`; +type ToGetter = `get${Capitalize}`; type Getters = { [K in keyof T as ToGetter]: () => T[K]; } diff --git a/docs/guide/043.md b/docs/guide/043.md index 4237500..2f82c22 100644 --- a/docs/guide/043.md +++ b/docs/guide/043.md @@ -107,9 +107,9 @@ let v1: T0; // let v1: "animal" let v2: T0; // let v2: "animal" // никогда "bird" -let v3: T1; // let v3: "raven" +let v3: T1; // let v3: "animal" | "raven" let v4: T1; // let v4: "raven" -let v5: T1; // let v5: "animal" +let v5: T1; // let v5: "animal | raven" ``` Помимо конкретного типа, в качестве правого (от ключевого слова `extends`) операнда также может выступать другой параметр типа. @@ -132,7 +132,7 @@ type T0 = T extends number ? 'text' : 'other'; -let v0: T0; // let v0: "numeric" | "text" +let v0: T0; // let v0: "numeric" | "text" let v1: T0; // let v1: "text" | "other" ``` @@ -147,7 +147,7 @@ type T0 = T extends number ? 'text' : 'other'; -let v0: T0; // let v0: "numeric" | "text" +let v0: T0; // let v0: "numeric" | "text" let v1: T0; // let v1: "text" | "other" // так видит компилятор @@ -159,10 +159,9 @@ type T0 = ? 'numeric' // number соответствует number? Да! Определяем "numeric" : T extends string ? 'text' - : - | 'other' // закончили определять один тип, приступаем к другому, в данном случае string - | string extends number - ? 'numeric' // string соответствует number? Нет! Продолжаем. + : 'other' // закончили определять один тип, приступаем к другому, в данном случае string + | string extends number + ? 'numeric' // string соответствует number? Нет! Продолжаем. : string extends string ? 'text' // string соответствует string? Да! Определяем "text". : 'other'; @@ -197,7 +196,7 @@ function f4(param: number[]): void {} let v0: ParamType; // let v0: number let v1: ParamType; // let v1: string -let v2: ParamType; // let v2: {} +let v2: ParamType; // let v2: {} (начиная с версии 3.5.1 let v2: unknown) let v3: ParamType; // let v3: undefined let v4: ParamType; // let v4: number[]. Oops, ожидалось тип number вместо number[] diff --git a/docs/guide/index.md b/docs/guide/index.md index 99c7222..8fc70f7 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -9,112 +9,112 @@ Typescript 4.2 Definition Guide. Перевод на русский язык о ## Общее -- [Что такое и для чего нужен TypeScript](000.md) +- [Что такое и для чего нужен TypeScript](000.md) ## Экскурс в типизацию -- [Вступление](001.md) -- [Система типов, тип данных, значимые и ссылочные типы](002.md) -- [Связывание, типизация, вывод типов](003.md) -- [Преобразование типов](004.md) -- [Типизированные и нетипизированные языки программирования](005.md) -- [Статическая и динамическая типизация](006.md) -- [Сильная и слабая типизация](007.md) -- [Явная и неявная типизация](008.md) -- [Совместимость типов на основе вида типизации](009.md) -- [Совместимость типов на основе вариантности](010.md) +- [Вступление](001.md) +- [Система типов, тип данных, значимые и ссылочные типы](002.md) +- [Связывание, типизация, вывод типов](003.md) +- [Преобразование типов](004.md) +- [Типизированные и нетипизированные языки программирования](005.md) +- [Статическая и динамическая типизация](006.md) +- [Сильная и слабая типизация](007.md) +- [Явная и неявная типизация](008.md) +- [Совместимость типов на основе вида типизации](009.md) +- [Совместимость типов на основе вариантности](010.md) ## Синтаксические конструкции -- [Аннотация Типов](011.md) +- [Аннотация Типов](011.md) ## Типы -- [Базовый Тип Any](012.md) -- [Примитивные типы Number, String, Boolean, Symbol, BigInt](013.md) -- [Примитивные типы Null, Undefined, Void, Never, Unknown](014.md) -- [Примитивный Тип Enum](015.md) -- [Типы - Union, Intersection](016.md) -- [Type Queries (запросы типа), Alias (псевдонимы типа)](017.md) -- [Примитивные литеральные типы Number, String, Template String, Boolean, Unique Symbol, Enum](018.md) -- [Object, Array, Tuple](019.md) -- [Function, Functional Types](020.md) -- [Interfaces](021.md) -- [Объектные типы с индексными членами (объектный тип с динамическими ключами)](022.md) +- [Базовый Тип Any](012.md) +- [Примитивные типы Number, String, Boolean, Symbol, BigInt](013.md) +- [Примитивные типы Null, Undefined, Void, Never, Unknown](014.md) +- [Примитивный Тип Enum](015.md) +- [Типы - Union, Intersection](016.md) +- [Type Queries (запросы типа), Alias (псевдонимы типа)](017.md) +- [Примитивные литеральные типы Number, String, Template String, Boolean, Unique Symbol, Enum](018.md) +- [Object, Array, Tuple](019.md) +- [Function, Functional Types](020.md) +- [Interfaces](021.md) +- [Объектные типы с индексными членами (объектный тип с динамическими ключами)](022.md) ## Классы -- [Модификаторы доступа (Access Modifiers)](023.md) -- [Закрытые поля определенные спецификацией ECMAScript](024.md) -- [Абстрактные классы (abstract classes)](025.md) -- [Полиморфный тип this](026.md) -- [Модификатор readonly (только для чтения)](027.md) -- [Definite Assignment Assertion Modifier](028.md) -- [Классы — Тонкости](029.md) +- [Модификаторы доступа (Access Modifiers)](023.md) +- [Закрытые поля определенные спецификацией ECMAScript](024.md) +- [Абстрактные классы (abstract classes)](025.md) +- [Полиморфный тип this](026.md) +- [Модификатор readonly (только для чтения)](027.md) +- [Definite Assignment Assertion Modifier](028.md) +- [Классы — Тонкости](029.md) ## Тонкости TypeScript -- [Различия var, let, const и модификатора readonly при неявном указании примитивных типов](030.md) +- [Различия var, let, const и модификатора readonly при неявном указании примитивных типов](030.md) ## Работа с типами -- [Операторы - Optional, Not-Null Not-Undefined, Definite Assignment Assertion](031.md) +- [Операторы - Optional, Not-Null Not-Undefined, Definite Assignment Assertion](031.md) ## Типы -- [Обобщения (Generics)](032.md) -- [Дискриминантное объединение (Discriminated Union)](033.md) +- [Обобщения (Generics)](032.md) +- [Дискриминантное объединение (Discriminated Union)](033.md) ## Работа с типами -- [Импорт и экспорт только типа](034.md) +- [Импорт и экспорт только типа](034.md) ## Типизация -- [Утверждение типов (Type Assertion)](035.md) -- [Защитники типа](036.md) -- [Вывод типов](037.md) -- [Совместимость объектных типов (Compatible Object Types)](038.md) -- [Совместимость функциональных типов (Compatible Function Types)](039.md) -- [Совместимость объединений (Union Types)](040.md) -- [Типизация в TypeScript](041.md) +- [Утверждение типов (Type Assertion)](035.md) +- [Защитники типа](036.md) +- [Вывод типов](037.md) +- [Совместимость объектных типов (Compatible Object Types)](038.md) +- [Совместимость функциональных типов (Compatible Function Types)](039.md) +- [Совместимость объединений (Union Types)](040.md) +- [Типизация в TypeScript](041.md) ## Работа с типами -- [Оператор keyof, Lookup Types, Mapped Types, Mapped Types - префиксы + и -](042.md) -- [Условные типы (Conditional Types)](043.md) +- [Оператор keyof, Lookup Types, Mapped Types, Mapped Types - префиксы + и -](042.md) +- [Условные типы (Conditional Types)](043.md) ## Расширенные типы -- [Readonly, Partial, Required, Pick, Record](044.md) -- [Exclude, Extract, NonNullable, ReturnType, InstanceType, Omit](045.md) -- [Массивоподобные readonly типы, ReadonlyArray, ReadonlyMap, ReadonlySet](046.md) +- [Readonly, Partial, Required, Pick, Record](044.md) +- [Exclude, Extract, NonNullable, ReturnType, InstanceType, Omit](045.md) +- [Массивоподобные readonly типы, ReadonlyArray, ReadonlyMap, ReadonlySet](046.md) ## Синтаксические конструкции -- [Синтаксические конструкции и операторы](047.md) +- [Синтаксические конструкции и операторы](047.md) ## React -- [Типизированный React](/react/types/048/) -- [Функциональные компоненты](/react/types/049/) -- [Классовые компоненты](/react/types/050/) -- [Универсальные компоненты](/react/types/051/) -- [Типизированные хуки](/react/types/052/) -- [Контекст (Context)](/react/types/053/) -- [HOC (Higher-Order Components)](/react/types/054/) +- [Типизированный React](https://reactdev.ru/types/048/) +- [Функциональные компоненты](https://reactdev.ru/types/049/) +- [Классовые компоненты](https://reactdev.ru/types/050/) +- [Универсальные компоненты](https://reactdev.ru/types/051/) +- [Типизированные хуки](https://reactdev.ru/types/052/) +- [Контекст (Context)](https://reactdev.ru/types/053/) +- [HOC (Higher-Order Components)](https://reactdev.ru/types/054/) ## Синтаксические конструкции -- [Пространства имен (namespace) и модули (module)](055.md) +- [Пространства имен (namespace) и модули (module)](055.md) ## Сборка -- [Настройка рабочего окружения](056.md) -- [Сборка с использованием ссылок на проекты](057.md) -- [Декларации](058.md) -- [Публикация TypeScript](059.md) +- [Настройка рабочего окружения](056.md) +- [Сборка с использованием ссылок на проекты](057.md) +- [Декларации](058.md) +- [Публикация TypeScript](059.md) ## Компилятор -- [Опции компилятора](060.md) +- [Опции компилятора](060.md) diff --git a/docs/heart.svg b/docs/heart.svg new file mode 100644 index 0000000..483f012 --- /dev/null +++ b/docs/heart.svg @@ -0,0 +1,6 @@ + + + diff --git a/docs/index.md b/docs/index.md index 457eda7..eaa2212 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,29 +2,81 @@ title: Главная description: Справочник Typescript hide: - - toc - - navigation + - toc + - navigation --- -# ![TS](ts.svg){: style="height:16px;width:16px;vertical-align:middle;"} Справочник Typescript +# ![TS](ts.svg){: style="height:42px;width:42px;vertical-align:middle;"} Справочник Typescript -[Руководство Typescript](/guide/) -: Typescript 4.2 Definition Guide. Перевод на русский язык официальной документации. +**[TypeScript :octicons-link-external-16:](https://www.typescriptlang.org/)** — язык программирования, позиционируемый как средство разработки веб-приложений, расширяющее возможности JavaScript. -[Книга Typescript](/book/) -: Глубокое погружение в Typescript +
-## Экосистема +- :simple-typescript:{ .lg .middle } **Typescript** -[Prisma ORM](/prisma-orm/) -: Prisma — это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для Node.js и TypeScript. + *** + + Перевод на русский язык официальной документации. + + **[:octicons-arrow-right-24: Typescript](./guide/index.md)** v4.2 + +- :octicons-book-24:{ .lg .middle } **Книга** + + *** + + Глубокое погружение в Typescript + + [:octicons-arrow-right-24: Книга Typescript](./book/index.md) + +- :simple-zod:{ .lg .middle } **Библиотеки** + + *** + + Экосистема Typescript + + [:octicons-arrow-right-24: Zod](./libs/zod.4/index.md) v4 + +
+ +## Бесплатные курсы по Typescript + +Бесплатные курсы и видео-уроки по языку Typescript. + +
+ +- :material-book-education:{ .lg .middle } **Курсы** + + *** + + [:octicons-arrow-right-24: Курс TypeScript](https://code-basics.com/ru/languages/typescript) Hexlet + + [:octicons-arrow-right-24: Создание приложений с помощью TypeScript](https://learn.microsoft.com/ru-ru/training/paths/build-javascript-applications-typescript/) Microsoft + + [:octicons-arrow-right-24: Введение в TypeScript](https://stepik.org/course/118563/promo) stepik + +- :material-youtube-tv:{ .lg .middle } **Видео-курсы** + + *** + + [:octicons-arrow-right-24: TypeScript](https://www.youtube.com/playlist?list=PLNkWIWHIRwMEm1FgiLjHqSky27x5rXvQa) webDev + + [:octicons-arrow-right-24: Уроки TypeScript](https://www.youtube.com/playlist?list=PLFe0raUk4QF5QqZ8PYuPcoInxAJ1gaHpj) Веб Программирование + + [:octicons-arrow-right-24: Курс по TypeScript](https://www.youtube.com/playlist?list=PL0BJapvyPWKGg5TsDbLKwT0k1A_MSYRjc) Mykhailovskyi StudyNew + +
+ +:material-chat-plus-outline: Ссылки на другие хорошие и **бесплатные** курсы можно оставить в комментарии или через пул-реквест. + +--- !!!abstract "Справочники" - [![HTML](html.svg){: style="height:16px;width:16px;vertical-align:middle;"} HTML + CSS](https://hcdev.ru/)     - **[![TS](ts.svg){: style="height:16px;width:16px;vertical-align:middle;"} Typescript](https://scriptdev.ru/)**     - [![Angular](angular.svg){: style="height:16px;width:16px;vertical-align:middle;"} Angular](https://angdev.ru/)     - [![React](react.svg){: style="height:16px;width:16px;vertical-align:middle;"} React](https://reactdev.ru/)     - [![Node.js](nodejs.svg){: style="height:16px;width:16px;vertical-align:middle;"} Node.js](https://nodejsdev.ru/)     - [![Python 3](python.svg){: style="height:16px;width:16px;vertical-align:middle;"} Python 3](https://py3dev.ru/)     - [![XSLT](xslt.svg){: style="height:16px;width:16px;vertical-align:middle;"} XSLT](https://xsltdev.ru/) + [![HTML](html.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} HTML + CSS](https://hcdev.ru/)     + **[![TS](ts.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Typescript](https://scriptdev.ru/)**     + [![Angular](angular.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Angular](https://angdev.ru/)     + [![React](react.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} React](https://reactdev.ru/)     + [![React Native](rn.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} React Native](https://reactnativedev.ru/)     + [![PWA](pwa.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} PWA](https://pwadev.ru/)     + [![Node.js](nodejs.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Node.js](https://nodejsdev.ru/)     + [![XSLT](xslt.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} XSLT](https://xsltdev.ru/)     diff --git a/docs/libs/.pages b/docs/libs/.pages new file mode 100644 index 0000000..8fc2ea7 --- /dev/null +++ b/docs/libs/.pages @@ -0,0 +1,4 @@ +title: Экосистема +nav: + - index.md + - zod.4 diff --git a/docs/libs/index.md b/docs/libs/index.md new file mode 100644 index 0000000..a4c2b2e --- /dev/null +++ b/docs/libs/index.md @@ -0,0 +1,17 @@ +--- +description: Экосистема библиотек Typescript +--- + +# Экосистема Typescript + +
+ +- :simple-zod:{ .lg .middle } **Библиотеки** + + *** + + Экосистема Typescript + + [:octicons-arrow-right-24: Zod](./zod.4/index.md) v4 + +
diff --git a/docs/libs/zod.4/.pages b/docs/libs/zod.4/.pages new file mode 100644 index 0000000..8694323 --- /dev/null +++ b/docs/libs/zod.4/.pages @@ -0,0 +1,9 @@ +title: Zod 4 +nav: + - index.md + - basics.md + - api.md + - error-customization.md + - error-formatting.md + - metadata.md + - json-schema.md diff --git a/docs/libs/zod.4/api.md b/docs/libs/zod.4/api.md new file mode 100644 index 0000000..e8499a3 --- /dev/null +++ b/docs/libs/zod.4/api.md @@ -0,0 +1,1901 @@ +--- +description: Для проверки данных необходимо сначала определить схему. Схемы представляют типы, от простых примитивных значений до сложных вложенных объектов и массивов +--- + +# Определение схем + +Для проверки данных необходимо сначала определить _схему_. Схемы представляют _типы_, от простых примитивных значений до сложных вложенных объектов и массивов. + +## Примитивы + +```ts +import * as z from 'zod'; + +// primitive types +z.string(); +z.number(); +z.bigint(); +z.boolean(); +z.symbol(); +z.undefined(); +z.null(); +``` + +### Принудительное преобразование + +Чтобы принудительно преобразовать входные данные в соответствующий тип, используйте вместо этого `z.coerce`: + +```ts +z.coerce.string(); // String(input) +z.coerce.number(); // Number(input) +z.coerce.boolean(); // Boolean(input) +z.coerce.bigint(); // BigInt(input) +``` + +Принудительный вариант этих схем пытается преобразовать входное значение в соответствующий тип. + +```ts +const schema = z.coerce.string(); + +schema.parse('tuna'); // => "tuna" +schema.parse(42); // => "42" +schema.parse(true); // => "true" +schema.parse(null); // => "null" +``` + +!!!note "Как работает принуждение в Zod" + + Zod принудительно обрабатывает все входные данные с помощью встроенных конструкторов. + + | Zod API | Приведение | + | -------------------- | ----------------- | + | `z.coerce.string()` | `String(value)` | + | `z.coerce.number()` | `Number(value)` | + | `z.coerce.boolean()` | `Boolean(value)` | + | `z.coerce.bigint()` | `BigInt(value)` | + | `z.coerce.date()` | `new Date(value)` | + + Булевое принудительное преобразование с помощью `z.coerce.boolean()` может работать не так, как вы ожидаете. Любое [истинное](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) значение принудительно преобразуется в `true`, а любое [ложное](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) значение принудительно преобразуется в `false`. + + ```ts + const schema = z.coerce.boolean(); // Boolean(input) + + schema.parse('tuna'); // => true + schema.parse('true'); // => true + schema.parse('false'); // => true + schema.parse(1); // => true + schema.parse([]); // => true + + schema.parse(0); // => false + schema.parse(''); // => false + schema.parse(undefined); // => false + schema.parse(null); // => false + ``` + + Для полного контроля над логикой принуждения рассмотрите возможность использования [`z.transform()`](#transforms) или [`z.pipe()`](#pipes). + +## Литералы + +Литеральные схемы представляют собой [литеральный тип](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types), например `"hello world"` или `"5"`. + +```ts +const tuna = z.literal('tuna'); +const twelve = z.literal(12); +const twobig = z.literal(2n); +const tru = z.literal(true); +``` + +Для представления литералов JavaScript `null` и `undefined`: + +```ts +z.null(); +z.undefined(); +z.void(); // equivalent to z.undefined() +``` + +Чтобы разрешить несколько литеральных значений: + +```ts +const colors = z.literal(['red', 'green', 'blue']); + +colors.parse('green'); // ✅ +colors.parse('yellow'); // ❌ +``` + +Чтобы извлечь набор допустимых значений из литеральной схемы: + +```ts +colors.values; // => Set<"red" | "green" | "blue"> +``` + +## Строки + +Zod предоставляет несколько встроенных API для проверки и преобразования строк. Для выполнения некоторых распространенных проверок строк: + +```ts +z.string().max(5); +z.string().min(5); +z.string().length(5); +z.string().regex(/^[a-z]+$/); +z.string().startsWith('aaa'); +z.string().endsWith('zzz'); +z.string().includes('---'); +z.string().uppercase(); +z.string().lowercase(); +``` + +Чтобы выполнить несколько простых преобразований строк: + +```ts +z.string().trim(); // trim whitespace +z.string().toLowerCase(); // toLowerCase +z.string().toUpperCase(); // toUpperCase +``` + +## Форматы строк + +Для проверки на соответствие некоторым распространенным форматам строк: + +```ts +z.email(); +z.uuid(); +z.url(); +z.emoji(); // проверяет один символ эмодзи +z.base64(); +z.base64url(); +z.nanoid(); +z.cuid(); +z.cuid2(); +z.ulid(); +z.ipv4(); +z.ipv6(); +z.cidrv4(); // ipv4 CIDR block +z.cidrv6(); // ipv6 CIDR block +z.iso.date(); +z.iso.time(); +z.iso.datetime(); +z.iso.duration(); +``` + +### Электронные адреса + +Для проверки адресов электронной почты: + +```ts +z.email(); +``` + +По умолчанию Zod использует сравнительно строгое регулярное выражение для проверки нормальных адресов электронной почты, содержащих общие символы. Оно примерно соответствует правилам, применяемым Gmail. Чтобы узнать больше об этом регулярном выражении, обратитесь к [этой публикации](https://colinhacks.com/essays/reasonable-email-regex). + +```ts +/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i; +``` + +Чтобы настроить поведение проверки электронной почты, вы можете передать пользовательское регулярное выражение в параметр `pattern`. + +```ts +z.email({ pattern: /your regex here/ }); +``` + +Zod экспортирует несколько полезных регулярных выражений, которые вы можете использовать. + +```ts +// Zod's default email regex +z.email(); +z.email({ pattern: z.regexes.email }); // equivalent + +// the regex used by browsers to validate input[type=email] fields +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email +z.email({ pattern: z.regexes.html5Email }); + +// the classic emailregex.com regex (RFC 5322) +z.email({ pattern: z.regexes.rfc5322Email }); + +// a loose regex that allows Unicode (good for intl emails) +z.email({ pattern: z.regexes.unicodeEmail }); +``` + +### UUIDs + +Для проверки UUID: + +```ts +z.uuid(); +``` + +Чтобы указать конкретную версию UUID: + +```ts +// supports "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8" +z.uuid({ version: 'v4' }); + +// for convenience +z.uuidv4(); +z.uuidv6(); +z.uuidv7(); +``` + +Спецификация UUID RFC 9562/4122 требует, чтобы первые два бита байта 8 были равны `10`. Другие идентификаторы, подобные UUID, не налагают этого ограничения. Для проверки любого идентификатора, подобного UUID: + +```ts +z.guid(); +``` + +### URLs + +Для проверки любого URL-адреса, совместимого с WHATWG: + +```ts +const schema = z.url(); + +schema.parse('https://example.com'); // ✅ +schema.parse('http://localhost'); // ✅ +schema.parse('mailto:noreply@zod.dev'); // ✅ +schema.parse('sup'); // ✅ +``` + +Как видите, это довольно допустимо. Внутренне для проверки входных данных используется конструктор `new URL()`; это поведение может отличаться на разных платформах и в разных средах выполнения, но это наиболее строгий способ проверки URI/URL в любой среде выполнения/движке JS. + +Чтобы проверить имя хоста по определенному регулярному выражению: + +```ts +const schema = z.url({ hostname: /^example\.com$/ }); + +schema.parse('https://example.com'); // ✅ +schema.parse('https://zombo.com'); // ❌ +``` + +Чтобы проверить протокол на соответствие определенному регулярному выражению, используйте параметр `protocol`. + +```ts +const schema = z.url({ protocol: /^https$/ }); + +schema.parse('https://example.com'); // ✅ +schema.parse('http://example.com'); // ❌ +``` + +!!!note "" + + **Веб-URL** — во многих случаях вам потребуется проверить именно веб-URL. Вот рекомендуемая схема для этого: + + ```ts + const httpUrl = z.url({ + protocol: /^https?$/, + hostname: z.regexes.domain, + }); + ``` + + Это ограничивает протокол `http`/`https` и гарантирует, что имя хоста является действительным доменным именем с помощью регулярного выражения `z.regexes.domain`: + + ```ts + /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; + ``` + +### Даты и время ISO + +Как вы, возможно, заметили, строка Zod включает в себя несколько проверок, связанных с датой и временем. Эти проверки основаны на регулярных выражениях, поэтому они не так строги, как полная библиотека даты и времени. Однако они очень удобны для проверки ввода пользователя. + +Метод `z.iso.datetime()` обеспечивает соблюдение стандарта ISO 8601; по умолчанию смещения часовых поясов не допускаются: + +```ts +const datetime = z.iso.datetime(); + +datetime.parse('2020-01-01T06:15:00Z'); // ✅ +datetime.parse('2020-01-01T06:15:00.123Z'); // ✅ +datetime.parse('2020-01-01T06:15:00.123456Z'); // ✅ (arbitrary precision) +datetime.parse('2020-01-01T06:15:00+02:00'); // ❌ (offsets not allowed) +datetime.parse('2020-01-01T06:15:00'); // ❌ (local not allowed) +``` + +Чтобы использовать смещение часовых поясов: + +```ts +const datetime = z.iso.datetime({ offset: true }); + +// allows timezone offsets +datetime.parse('2020-01-01T06:15:00+02:00'); // ✅ + +// basic offsets not allowed +datetime.parse('2020-01-01T06:15:00+02'); // ❌ +datetime.parse('2020-01-01T06:15:00+0200'); // ❌ + +// Z is still supported +datetime.parse('2020-01-01T06:15:00Z'); // ✅ +``` + +Чтобы позволить использование неквалифицированных (без указания часового пояса) дат и времени: + +```ts +const schema = z.iso.datetime({ local: true }); +schema.parse('2020-01-01T06:15:01'); // ✅ +schema.parse('2020-01-01T06:15'); // ✅ seconds optional +``` + +Ограничить допустимое время `precision`. По умолчанию секунды не являются обязательными, и допускается произвольная точность до долей секунды. + +```ts +const a = z.iso.datetime(); +a.parse('2020-01-01T06:15Z'); // ✅ +a.parse('2020-01-01T06:15:00Z'); // ✅ +a.parse('2020-01-01T06:15:00.123Z'); // ✅ + +const b = z.iso.datetime({ precision: -1 }); // minute precision (no seconds) +b.parse('2020-01-01T06:15Z'); // ✅ +b.parse('2020-01-01T06:15:00Z'); // ❌ +b.parse('2020-01-01T06:15:00.123Z'); // ❌ + +const c = z.iso.datetime({ precision: 0 }); // second precision only +c.parse('2020-01-01T06:15Z'); // ❌ +c.parse('2020-01-01T06:15:00Z'); // ✅ +c.parse('2020-01-01T06:15:00.123Z'); // ❌ + +const d = z.iso.datetime({ precision: 3 }); // millisecond precision only +d.parse('2020-01-01T06:15Z'); // ❌ +d.parse('2020-01-01T06:15:00Z'); // ❌ +d.parse('2020-01-01T06:15:00.123Z'); // ✅ +``` + +### Даты ISO + +Метод `z.iso.date()` проверяет строки в формате `YYYY-MM-DD`. + +```ts +const date = z.iso.date(); + +date.parse('2020-01-01'); // ✅ +date.parse('2020-1-1'); // ❌ +date.parse('2020-01-32'); // ❌ +``` + +### Время ISO + +Метод `z.iso.time()` проверяет строки в формате `HH:MM[:SS[.s+]]`. По умолчанию секунды являются необязательными, как и десятичные доли секунды. + +```ts +const time = z.iso.time(); + +time.parse('03:15'); // ✅ +time.parse('03:15:00'); // ✅ +time.parse('03:15:00.9999999'); // ✅ (arbitrary precision) +``` + +Никакие смещения не допускаются. + +```ts +time.parse('03:15:00Z'); // ❌ (no `Z` allowed) +time.parse('03:15:00+02:00'); // ❌ (no offsets allowed) +``` + +Используйте параметр `precision` для ограничения допустимой десятичной точности. + +```ts +z.iso.time({ precision: -1 }); // HH:MM (minute precision) +z.iso.time({ precision: 0 }); // HH:MM:SS (second precision) +z.iso.time({ precision: 1 }); // HH:MM:SS.s (decisecond precision) +z.iso.time({ precision: 2 }); // HH:MM:SS.ss (centisecond precision) +z.iso.time({ precision: 3 }); // HH:MM:SS.sss (millisecond precision) +``` + +### IP адреса + +```ts +const ipv4 = z.ipv4(); +ipv4.parse('192.168.0.0'); // ✅ + +const ipv6 = z.ipv6(); +ipv6.parse('2001:db8:85a3::8a2e:370:7334'); // ✅ +``` + +### IP блоки (CIDR) + +Проверьте диапазоны IP-адресов, указанные с помощью [нотации CIDR](https://ru.wikipedia.org/wiki/Бесклассовая_адресация). + +```ts +const cidrv4 = z.string().cidrv4(); +cidrv4.parse('192.168.0.0/24'); // ✅ + +const cidrv6 = z.string().cidrv6(); +cidrv6.parse('2001:db8::/32'); // ✅ +``` + +## Шаблонные литералы + +**Новое в Zod 4** + +Чтобы определить схему шаблонного литерала: + +```ts +const schema = z.templateLiteral([ + 'hello, ', + z.string(), + '!', +]); +// `hello, ${string}!` +``` + +API `z.templateLiteral` может обрабатывать любое количество строковых литералов (например, `"hello"`) и схем. Можно передавать любую схему с выведенным типом, который можно присвоить `string | number | bigint | boolean | null | undefined`. + +```ts +z.templateLiteral(['hi there']); +// `hi there` + +z.templateLiteral(['email: ', z.string()]); +// `email: ${string}` + +z.templateLiteral(['high', z.literal(5)]); +// `high5` + +z.templateLiteral([z.nullable(z.literal('grassy'))]); +// `grassy` | `null` + +z.templateLiteral([ + z.number(), + z.enum(['px', 'em', 'rem']), +]); +// `${number}px` | `${number}em` | `${number}rem` +``` + +## Числа + +Используйте `z.number()` для проверки чисел. Допускаются любые конечные числа. + +```ts +const schema = z.number(); + +schema.parse(3.14); // ✅ +schema.parse(NaN); // ❌ +schema.parse(Infinity); // ❌ +``` + +Zod реализует несколько проверок, связанных с числами: + +```ts +z.number().gt(5); +z.number().gte(5); // alias .min(5) +z.number().lt(5); +z.number().lte(5); // alias .max(5) +z.number().positive(); +z.number().nonnegative(); +z.number().negative(); +z.number().nonpositive(); +z.number().multipleOf(5); // alias .step(5) +``` + +Если (по какой-то причине) вы хотите проверить `NaN`, используйте `z.nan()`. + +```ts +z.nan().parse(NaN); // ✅ +z.nan().parse('anything else'); // ❌ +``` + +## Целые числа + +Для проверки целых чисел: + +```ts +z.int(); // restricts to safe integer range +z.int32(); // restrict to int32 range +``` + +## BigInt + +Для проверки BigInt: + +```ts +z.bigint(); +``` + +Zod включает в себя несколько проверок, специфичных для BigInt. + +```ts +z.bigint().gt(5n); +z.bigint().gte(5n); // alias `.min(5n)` +z.bigint().lt(5n); +z.bigint().lte(5n); // alias `.max(5n)` +z.bigint().positive(); +z.bigint().nonnegative(); +z.bigint().negative(); +z.bigint().nonpositive(); +z.bigint().multipleOf(5n); // alias `.step(5n)` +``` + +## Логические значения + +Для проверки логических значений: + +```ts +z.boolean().parse(true); // => true +z.boolean().parse(false); // => false +``` + +## Даты + +Используйте `z.date()` для проверки экземпляров `Date`. + +```ts +z.date().safeParse(new Date()); // success: true +z.date().safeParse('2022-01-12T06:15:00.000Z'); // success: false +``` + +Чтобы настроить сообщение об ошибке: + +```ts +z.date({ + error: (issue) => + issue.input === undefined + ? 'Required' + : 'Invalid date', +}); +``` + +Zod предоставляет несколько проверок, связанных с конкретными датами. + +```ts +z.date().min(new Date('1900-01-01'), { error: 'Too old!' }); +z.date().max(new Date(), { error: 'Too young!' }); +``` + +## Enums {#zod-enums} + +Используйте `z.enum` для проверки вводимых данных на соответствие фиксированному набору допустимых _строковых_ значений. + +```ts +const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']); + +FishEnum.parse('Salmon'); // => "Salmon" +FishEnum.parse('Swordfish'); // => ❌ +``` + +!!!note "Внимание" + + Если вы объявите массив строк как переменную, Zod не сможет правильно определить точные значения каждого элемента. + + ```ts + const fish = ['Salmon', 'Tuna', 'Trout']; + + const FishEnum = z.enum(fish); + type FishEnum = z.infer; // string + ``` + + Чтобы исправить это, всегда передавайте массив непосредственно в функцию `z.enum()` или используйте [`as const`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions). + + ```ts + const fish = ['Salmon', 'Tuna', 'Trout'] as const; + + const FishEnum = z.enum(fish); + type FishEnum = z.infer; // "Salmon" | "Tuna" | "Trout" + ``` + +Вы также можете передать внешне объявленный TypeScript enum. + +!!!note "Zod 4" + + Заменяет API `z.nativeEnum()` в Zod 3. + + Обратите внимание, что использование ключевого слова `enum` в TypeScript [не рекомендуется](https://www.totaltypescript.com/why-i-dont-like-typescript-enums). + +```ts +enum Fish { + Salmon = 'Salmon', + Tuna = 'Tuna', + Trout = 'Trout', +} + +const FishEnum = z.enum(Fish); +``` + +### `.enum` + +Чтобы извлечь значения схемы в виде объекта, похожего на перечисление: + +```ts +const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']); + +FishEnum.enum; +// => { Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout" } +``` + +### `.exclude()` + +Чтобы создать новую схему перечисления, исключив определенные значения: + +```ts +const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']); +const TunaOnly = FishEnum.exclude(['Salmon', 'Trout']); +``` + +### `.extract()` + +Чтобы создать новую схему перечисления, извлекая определенные значения: + +```ts +const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']); +const SalmonAndTroutOnly = FishEnum.extract([ + 'Salmon', + 'Trout', +]); +``` + +## Stringbools {#stringbool} + +**💎 Новое в Zod 4** + +В некоторых случаях (например, при разборе переменных окружения) полезно преобразовать определенные строковые «булевы» значения в простые значения `boolean`. Для этого в Zod 4 введена функция `z.stringbool()`: + +```ts +const strbool = z.stringbool(); + +strbool.parse('true'); // => true +strbool.parse('1'); // => true +strbool.parse('yes'); // => true +strbool.parse('on'); // => true +strbool.parse('y'); // => true +strbool.parse('enabled'); // => true + +strbool.parse('false'); // => false +strbool.parse('0'); // => false +strbool.parse('no'); // => false +strbool.parse('off'); // => false +strbool.parse('n'); // => false +strbool.parse('disabled'); // => false + +strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]> +``` + +Чтобы настроить значения `truthy` и `falsy`: + +```ts +// these are the defaults +z.stringbool({ + truthy: ['true', '1', 'yes', 'on', 'y', 'enabled'], + falsy: ['false', '0', 'no', 'off', 'n', 'disabled'], +}); +``` + +По умолчанию схема _нечувствительна к регистру_; все вводимые данные преобразуются в нижний регистр перед сравнением со значениями `truthy`/`falsy`. Чтобы сделать схему чувствительной к регистру: + +```ts +z.stringbool({ + case: 'sensitive', +}); +``` + +## Опциональные + +Чтобы сделать схему _опциональной_ (то есть разрешить ввод `undefined`). + +```ts +z.optional(z.literal('yoda')); // or z.literal("yoda").optional() +``` + +Это возвращает экземпляр `ZodOptional`, который оборачивает исходную схему. Чтобы извлечь внутреннюю схему: + +```ts +optionalYoda.unwrap(); // ZodLiteral<"yoda"> +``` + +## Nullables + +Чтобы сделать схему _nullable_ (то есть разрешить ввод значений `null`). + +```ts +z.nullable(z.literal('yoda')); // or z.literal("yoda").nullable() +``` + +Это возвращает экземпляр `ZodNullable`, который оборачивает исходную схему. Чтобы извлечь внутреннюю схему: + +```ts +nullableYoda.unwrap(); // ZodLiteral<"yoda"> +``` + +## Nullish + +Чтобы сделать схему _nullish_ (как опциональной, так и допускающей нулевые значения): + +```ts +const nullishYoda = z.nullish(z.literal('yoda')); +``` + +Более подробную информацию о концепции [nullish](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing) см. в руководстве по TypeScript. + +## Unknown + +Zod стремится полностью повторить систему типов TypeScript. Таким образом, Zod предоставляет API для представления следующих специальных типов: + +```ts +// allows any values +z.any(); // inferred type: `any` +z.unknown(); // inferred type: `unknown` +``` + +## Never + +Ни одно значение не пройдет проверку. + +```ts +z.never(); // inferred type: `never` +``` + +## Объекты + +Чтобы определить тип объекта: + +```ts +// all properties are required by default +const Person = z.object({ + name: z.string(), + age: z.number(), +}); + +type Person = z.infer; +// => { name: string; age: number; } +``` + +По умолчанию все свойства являются обязательными. Чтобы сделать некоторые свойства необязательными: + +```ts +const Dog = z.object({ + name: z.string(), + age: z.number().optional(), +}); + +Dog.parse({ name: 'Yeller' }); // ✅ +``` + +По умолчанию, нераспознанные ключи _удаляются_ из результата анализа: + +```ts +Dog.parse({ name: 'Yeller', extraKey: true }); +// => { name: "Yeller" } +``` + +### `z.strictObject` + +Чтобы определить _строгую_ схему, которая выдает ошибку при обнаружении неизвестных ключей: + +```ts +const StrictDog = z.strictObject({ + name: z.string(), +}); + +StrictDog.parse({ name: 'Yeller', extraKey: true }); +// ❌ throws +``` + +### `z.looseObject` + +Чтобы определить _гибкую_ схему, которая пропускает неизвестные ключи: + +```ts +const LooseDog = z.looseObject({ + name: z.string(), +}); + +Dog.parse({ name: 'Yeller', extraKey: true }); +// => { name: "Yeller", extraKey: true } +``` + +### `.catchall()` + +Чтобы определить _универсальную схему_, которая будет использоваться для проверки любых нераспознанных ключей: + +```ts +const DogWithStrings = z + .object({ + name: z.string(), + age: z.number().optional(), + }) + .catchall(z.string()); + +DogWithStrings.parse({ + name: 'Yeller', + extraKey: 'extraValue', +}); // ✅ +DogWithStrings.parse({ name: 'Yeller', extraKey: 42 }); // ❌ +``` + +### `.shape` + +Для доступа к внутренним схемам: + +```ts +Dog.shape.name; // => string schema +Dog.shape.age; // => number schema +``` + +### `.keyof()` + +Чтобы создать схему `ZodEnum` из ключей схемы объекта: + +```ts +const keySchema = Dog.keyof(); +// => ZodEnum<["name", "age"]> +``` + +### `.extend()` {#extend} + +Чтобы добавить дополнительные поля в схему объекта: + +```ts +const DogWithBreed = Dog.extend({ + breed: z.string(), +}); +``` + +Этот API можно использовать для перезаписи существующих полей! Будьте осторожны с этой функцией! Если две схемы имеют общие ключи, B перезапишет A. + +!!!note "Альтернатива: деструктуризация" + + Вы также можете полностью избежать использования `.extend()`, создав совершенно новую схему объекта. Это сделает уровень строгости результирующей схемы визуально очевидным. + + ```ts + const DogWithBreed = z.object({ + // or z.strictObject() or z.looseObject()... + ...Dog.shape, + breed: z.string(), + }); + ``` + + Вы также можете использовать эту функцию для объединения нескольких объектов за один раз. + + ```ts + const DogWithBreed = z.object({ + ...Animal.shape, + ...Pet.shape, + breed: z.string(), + }); + ``` + + Этот подход имеет несколько преимуществ: + + 1. Он использует функции на уровне языка ([синтаксис деструктуризации](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring)) вместо библиотечных API. + 2. Один и тот же синтаксис работает в Zod и Zod Mini + 3. Он более эффективен для `tsc` — метод `.extend()` может быть дорогостоящим для больших схем, а из-за [ограничения TypeScript](https://github.com/microsoft/TypeScript/pull/61505) его стоимость возрастает в квадратичной зависимости при цепочке вызовов + 4. При желании вы можете изменить уровень строгости результирующей схемы с помощью `z.strictObject()` или `z.looseObject()` + +### `.pick()` + +Вдохновленный встроенными типами утилит `Pick` и `Omit` в TypeScript, Zod предоставляет специальные API для выбора и исключения определенных ключей из схемы объекта. + +Начнем с этой начальной схемы: + +```ts +const Recipe = z.object({ + title: z.string(), + description: z.string().optional(), + ingredients: z.array(z.string()), +}); +// { title: string; description?: string | undefined; ingredients: string[] } +``` + +Чтобы выбрать определенные ключи: + +```ts +const JustTheTitle = Recipe.pick({ title: true }); +``` + +### `.omit()` + +Чтобы пропустить определенные ключи: + +```ts +const RecipeNoId = Recipe.omit({ id: true }); +``` + +### `.partial()` + +Для удобства Zod предоставляет специальный API для того, чтобы сделать некоторые или все свойства опциональными, вдохновленный встроенным типом утилиты TypeScript [`Partial`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype). + +Чтобы сделать все поля опциональными: + +```ts +const PartialRecipe = Recipe.partial(); +// { title?: string | undefined; description?: string | undefined; ingredients?: string[] | undefined } +``` + +Чтобы сделать определенные свойства необязательными: + +```ts +const RecipeOptionalIngredients = Recipe.partial({ + ingredients: true, +}); +// { title: string; description?: string | undefined; ingredients?: string[] | undefined } +``` + +### `.required()` + +Zod предоставляет API для того, чтобы сделать некоторые или все свойства _обязательными_, вдохновленный типом утилиты TypeScript [`Required`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype). + +Чтобы сделать все свойства обязательными: + +```ts +const RequiredRecipe = Recipe.required(); +// { title: string; description: string; ingredients: string[] } +``` + +Чтобы сделать определенные свойства обязательными: + +```ts +const RecipeRequiredDescription = Recipe.required({ + description: true, +}); +// { title: string; description: string; ingredients: string[] } +``` + +## Рекурсивные объекты + +Чтобы определить самореференциальный тип, используйте [геттер](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) для ключа. Это позволяет JavaScript разрешить циклическую схему во время выполнения. + +```ts +const Category = z.object({ + name: z.string(), + get subcategories() { + return z.array(Category); + }, +}); + +type Category = z.infer; +// { name: string; subcategories: Category[] } +``` + +!!!warn "" + + Хотя рекурсивные схемы поддерживаются, передача циклических данных в Zod приведет к бесконечному циклу. + +Вы также можете представлять _взаимно рекурсивные типы_: + +```ts +const User = z.object({ + email: z.email(), + get posts() { + return z.array(Post); + }, +}); + +const Post = z.object({ + title: z.string(), + get author() { + return User; + }, +}); +``` + +Все API объектов (`.pick()`, `.omit()`, `.required()`, `.partial()` и т. д.) работают так, как и ожидается. + +### Ошибки цикличности + +Из-за ограничений TypeScript рекурсивный вывод типов может быть привередливым и работает только в определенных сценариях. Некоторые более сложные типы могут вызывать рекурсивные ошибки типов, подобные этой: + +```ts +const Activity = z.object({ + name: z.string(), + get subactivities() { + // ^ ❌ 'subactivities' implicitly has return type 'any' because it does not + // have a return type annotation and is referenced directly or indirectly + // in one of its return expressions.ts(7023) + + return z.nullable(z.array(Activity)); + }, +}); +``` + +В таких случаях ошибку можно устранить с помощью аннотации типа для проблемного геттера: + +```ts +const Activity = z.object({ + name: z.string(), + get subactivities(): z.ZodNullable< + z.ZodArray + > { + return z.nullable(z.array(Activity)); + }, +}); +``` + +## Массивы + +Чтобы определить схему массива: + +```ts +const stringArray = z.array(z.string()); // or z.string().array() +``` + +Для доступа к внутренней схеме элемента массива. + +```ts +stringArray.unwrap(); // => string schema +``` + +Zod реализует ряд проверок, специфичных для массивов: + +```ts +z.array(z.string()).min(5); // must contain 5 or more items +z.array(z.string()).max(5); // must contain 5 or fewer items +z.array(z.string()).length(5); // must contain 5 items exactly +``` + +## Кортежи + +В отличие от массивов, кортежи обычно представляют собой массивы фиксированной длины, которые задают разные схемы для каждого индекса. + +```ts +const MyTuple = z.tuple([ + z.string(), + z.number(), + z.boolean(), +]); + +type MyTuple = z.infer; +// [string, number, boolean] +``` + +Чтобы добавить вариативный («остаточный») аргумент: + +```ts +const variadicTuple = z.tuple([z.string()], z.number()); +// => [string, ...number[]]; +``` + +## Объединения + +Типы объединений (`A | B`) представляют собой логическое «ИЛИ». Схемы объединений Zod проверяют входные данные по порядку для каждого варианта. Возвращается первое значение, которое прошло проверку. + +```ts +const stringOrNumber = z.union([z.string(), z.number()]); +// string | number + +stringOrNumber.parse('foo'); // passes +stringOrNumber.parse(14); // passes +``` + +Чтобы извлечь внутренние варианты схем: + +```ts +stringOrNumber.options; // [ZodString, ZodNumber] +``` + +## Дискриминированные объединения + +[Дискриминированное объединение](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) — это особый вид объединения, в котором а) все варианты являются схемами объектов, которые б) имеют общий ключ («дискриминатор»). На основе значения ключа дискриминатора TypeScript может «сузить» сигнатуру типа, как и ожидается. + +```ts +type MyResult = + | { status: 'success'; data: string } + | { status: 'failed'; error: string }; + +function handleResult(result: MyResult) { + if (result.status === 'success') { + result.data; // string + } else { + result.error; // string + } +} +``` + +Вы можете представить это с помощью обычного `z.union()`. Но обычные объединения являются _наивными_ — они проверяют входные данные по порядку для каждого варианта и возвращают первый, который проходит проверку. Это может быть медленно для больших объединений. + +Поэтому Zod предоставляет API `z.discriminatedUnion()`, который использует _ключ дискриминатора_ для более эффективного разбора. + +```ts +const MyResult = z.discriminatedUnion('status', [ + z.object({ + status: z.literal('success'), + data: z.string(), + }), + z.object({ + status: z.literal('failed'), + error: z.string(), + }), +]); +``` + +!!!note "Вложенные дискриминированные объединения" + + Для сложных случаев использования дискриминируемые объединения могут быть вложенными. Zod определит оптимальную стратегию разбора, чтобы использовать дискриминаторы на каждом уровне. + + ```ts + const BaseError = { + status: z.literal('failed'), + message: z.string(), + }; + const MyErrors = z.discriminatedUnion('code', [ + z.object({ ...BaseError, code: z.literal(400) }), + z.object({ ...BaseError, code: z.literal(401) }), + z.object({ ...BaseError, code: z.literal(500) }), + ]); + + const MyResult = z.discriminatedUnion('status', [ + z.object({ + status: z.literal('success'), + data: z.string(), + }), + MyErrors, + ]); + ``` + +## Пересечения + +Типы пересечений (`A & B`) представляют собой логическое «И». + +```ts +const a = z.union([z.number(), z.string()]); +const b = z.union([z.number(), z.boolean()]); +const c = z.intersection(a, b); + +type c = z.infer; // => number +``` + +Это может быть полезно для пересечения двух типов объектов. + +```ts +const Person = z.object({ name: z.string() }); +type Person = z.infer; + +const Employee = z.object({ role: z.string() }); +type Employee = z.infer; + +const EmployedPerson = z.intersection(Person, Employee); +type EmployedPerson = z.infer; +// Person & Employee +``` + +!!!warn "" + + При объединении схем объектов предпочтительнее использовать [`A.extend(B)`](#extend), а не пересечения. Использование `.extend()` даст вам новую схему объекта, тогда как `z.intersection(A, B)` возвращает экземпляр `ZodIntersection`, в котором отсутствуют общие методы объектов, такие как `pick` и `omit`. + +## Record + +Схемы записей используются для проверки типов, таких как `Record`. + +```ts +const IdCache = z.record(z.string(), z.string()); +type IdCache = z.infer; // Record + +IdCache.parse({ + carlotta: '77d2586b-9e8e-4ecf-8b21-ea7e0530eadd', + jimmie: '77d2586b-9e8e-4ecf-8b21-ea7e0530eadd', +}); +``` + +Ключевая схема может быть любой схемой Zod, которая может быть присвоена `string | number | symbol`. + +```ts +const Keys = z.union([z.string(), z.number(), z.symbol()]); +const AnyObject = z.record(Keys, z.unknown()); +// Record +``` + +Чтобы создать схемы объектов, содержащие ключи, определенные перечислением: + +```ts +const Keys = z.enum(['id', 'name', 'email']); +const Person = z.record(Keys, z.string()); +// { id: string; name: string; email: string } +``` + +!!!info "Zod 4" + + В Zod 4, если вы передадите `z.enum` в качестве первого аргумента в `z.record()`, Zod тщательно проверит, что все значения перечисления присутствуют во входных данных в качестве ключей. Это поведение соответствует TypeScript: + + ```ts + type MyRecord = Record<'a' | 'b', string>; + const myRecord: MyRecord = { a: 'foo', b: 'bar' }; // ✅ + const myRecord: MyRecord = { a: 'foo' }; // ❌ missing required key `b` + ``` + + В Zod 3 исчерпываемость не проверялась. Чтобы воспроизвести прежнее поведение, используйте `z.partialRecord()`. + +Если вам нужен тип записи _partial_, используйте `z.partialRecord()`. Это позволяет пропустить специальные проверки полноты, которые Zod обычно выполняет с ключевыми схемами `z.enum()` и `z.literal()`. + +```ts +const Keys = z.enum(['id', 'name', 'email']).or(z.never()); +const Person = z.partialRecord(Keys, z.string()); +// { id?: string; name?: string; email?: string } +``` + +!!!note "Примечание о числовых ключах" + + Хотя TypeScript позволяет определять типы `Record` с ключами `number` (например, `Record`), числовые ключи на самом деле не существуют в JavaScript, который преобразует все ключи в строки. + + ```ts + const myObject = { 1: 'one' }; + + Object.keys(myObject); + // => ["1"] + ``` + + Как видите, JavaScript автоматически преобразует все числовые ключи в строки. Таким образом, использование `z.number()` в качестве схемы ключа внутри `z.record()` всегда будет вызывать ошибку при разборе, но Zod допускает это для обеспечения совместимости с системой типов TypeScript. + +## Map + +```ts +const StringNumberMap = z.map(z.string(), z.number()); +type StringNumberMap = z.infer; // Map + +const myMap: StringNumberMap = new Map(); +myMap.set('one', 1); +myMap.set('two', 2); + +StringNumberMap.parse(myMap); +``` + +## Set + +```ts +const NumberSet = z.set(z.number()); +type NumberSet = z.infer; // Set + +const mySet: NumberSet = new Set(); +mySet.add(1); +mySet.add(2); +NumberSet.parse(mySet); +``` + +Схемы наборов могут быть дополнительно ограничены с помощью следующих служебных методов. + +```ts +z.set(z.string()).min(5); // must contain 5 or more items +z.set(z.string()).max(5); // must contain 5 or fewer items +z.set(z.string()).size(5); // must contain 5 items exactly +``` + +## Файлы + +Для проверки экземпляров `File`: + +```ts +const fileSchema = z.file(); + +fileSchema.min(10_000); // minimum .size (bytes) +fileSchema.max(1_000_000); // maximum .size (bytes) +fileSchema.mime('image/png'); // MIME type +fileSchema.mime(['image/png', 'image/jpeg']); // multiple MIME types +``` + +## Промисы + +!!!warn "Устарело" + + `z.promise()` устарело в Zod 4. Существует крайне мало действительных случаев использования схемы `Promise`. Если вы подозреваете, что значение может быть `Promise`, просто `await` его перед разбором с помощью Zod. + +!!!note "См. документацию по z.promise()" + + ```ts + const numberPromise = z.promise(z.number()); + ``` + + «Парсинг» работает немного по-другому с промисами схем. Проверка происходит в два этапа: + + 1. Zod синхронно проверяет, что входные данные являются экземпляром Promise (т. е. объектом с методами `.then` и `.catch`). + 2. Zod использует `.then`, чтобы добавить дополнительный шаг проверки к существующему Promise. Вам нужно будет использовать `.catch` на возвращенном Promise, чтобы обработать ошибки проверки. + + ```ts + numberPromise.parse('tuna'); + // ZodError: Non-Promise type: string + + numberPromise.parse(Promise.resolve('tuna')); + // => Promise + + const test = async () => { + await numberPromise.parse(Promise.resolve('tuna')); + // ZodError: Non-number type: string + + await numberPromise.parse(Promise.resolve(3.14)); + // => 3.14 + }; + ``` + +## Instanceof + +Вы можете использовать `z.instanceof`, чтобы проверить, является ли ввод экземпляром класса. Это полезно для проверки ввода по отношению к классам, экспортированным из сторонних библиотек. + +```ts +class Test { + name: string; +} + +const TestSchema = z.instanceof(Test); + +TestSchema.parse(new Test()); // ✅ +TestSchema.parse('whatever'); // ❌ +``` + +## Уточнения + +Каждая схема Zod хранит массив _уточнений_. Уточнения — это способ выполнения настраиваемой проверки, для которой Zod не предоставляет собственного API. + +### `.refine()` + +```ts +const myString = z + .string() + .refine((val) => val.length <= 255); +``` + +!!!warn "" + + Функции усовершенствования никогда не должны вызывать исключения. Вместо этого они должны возвращать ложное значение, чтобы сигнализировать об ошибке. Вызванные ошибки не перехватываются Zod. + +#### `error` + +Чтобы настроить сообщение об ошибке: + +```ts +const myString = z + .string() + .refine((val) => val.length > 8, { + error: 'Too short!', + }); +``` + +#### `abort` + +По умолчанию проблемы валидации, выявленные в ходе проверок, считаются _продолжаемыми_; то есть Zod выполнит _все_ проверки по порядку, даже если одна из них вызовет ошибку валидации. Обычно это желательно, поскольку Zod может выявить как можно больше ошибок за один раз. + +```ts +const myString = z + .string() + .refine((val) => val.length > 8, { + error: 'Too short!', + }) + .refine((val) => val === val.toLowerCase(), { + error: 'Must be lowercase', + }); + +const result = myString.safeParse('OH NO'); +result.error.issues; +/* [ + { "code": "custom", "message": "Too short!" }, + { "code": "custom", "message": "Must be lowercase" } +] */ +``` + +Чтобы пометить конкретное уточнение как _непродолжаемое_, используйте параметр `abort`. Проверка будет прервана, если проверка не пройдет. + +```ts +const myString = z + .string() + .refine((val) => val.length > 8, { + error: 'Too short!', + abort: true, + }) + .refine((val) => val === val.toLowerCase(), { + error: 'Must be lowercase', + abort: true, + }); + +const result = myString.safeParse('OH NO'); +result.error!.issues; +// => [{ "code": "custom", "message": "Too short!" }] +``` + +#### `path` + +Чтобы настроить путь ошибки, используйте параметр `path`. Обычно это полезно только в контексте схем объектов. + +```ts +const passwordForm = z + .object({ + password: z.string(), + confirm: z.string(), + }) + .refine((data) => data.password === data.confirm, { + message: "Passwords don't match", + path: ['confirm'], // path of error + }); +``` + +Это установит параметр `path` в связанной проблеме: + +```ts +const result = passwordForm.safeParse({ + password: 'asdf', + confirm: 'qwer', +}); +result.error.issues; +/* [{ + "code": "custom", + "path": [ "confirm" ], + "message": "Passwords don't match" +}] */ +``` + +Чтобы определить асинхронное уточнение, просто передайте функцию `async`: + +```ts +const userId = z.string().refine(async (id) => { + // verify that ID exists in database + return true; +}); +``` + +!!!note "" + + Если вы используете асинхронные уточнения, вы должны использовать метод `.parseAsync` для анализа данных! В противном случае Zod выдаст ошибку. + + ```ts + const result = await userId.parseAsync('abc123'); + ``` + +#### `when` + +!!!note "Примечание" + + Это функция для опытных пользователей, которая может быть использована не по назначению, что увеличит вероятность возникновения невыявленных ошибок, происходящих изнутри ваших уточнений. + +По умолчанию уточнения не запускаются, если уже были обнаружены какие-либо _непрерывные_ проблемы. Zod тщательно проверяет правильность сигнатуры типа значения, прежде чем передавать его в какие-либо функции уточнения. + +```ts +const schema = z.string().refine((val) => { + return val.length > 8; +}); + +schema.parse(1234); // invalid_type: refinement won't be executed +``` + +В некоторых случаях требуется более точный контроль над временем выполнения уточнений. Рассмотрим, например, проверку «подтверждение пароля»: + +```ts +const schema = z + .object({ + password: z.string().min(8), + confirmPassword: z.string(), + anotherField: z.string(), + }) + .refine( + (data) => data.password === data.confirmPassword, + { + message: 'Passwords do not match', + path: ['confirmPassword'], + } + ); + +schema.parse({ + password: 'asdf', + confirmPassword: 'asdf', + anotherField: 1234, // ❌ this error will prevent the password check from running +}); +``` + +Ошибка в `anotherField` помешает выполнению проверки подтверждения пароля, даже если эта проверка не зависит от `anotherField`. Чтобы контролировать, когда будет выполняться уточнение, используйте параметр `when`: + +```ts +const schema = z + .object({ + password: z.string().min(8), + confirmPassword: z.string(), + anotherField: z.string(), + }) + .refine( + (data) => data.password === data.confirmPassword, + { + message: 'Passwords do not match', + path: ['confirmPassword'], + + // run if password & confirmPassword are valid + when(payload) { + // [!code ++] + return schema // [!code ++] + .pick({ + password: true, + confirmPassword: true, + }) // [!code ++] + .safeParse(payload.value).success; // [!code ++] + }, // [!code ++] + } + ); + +schema.parse({ + password: 'asdf', + confirmPassword: 'asdf', + anotherField: 1234, // ❌ this error will prevent the password check from running +}); +``` + +### `.superRefine()` + +В Zod 4 функция `.superRefine()` была признана устаревшей и заменена на `.check()`. + +```ts +const UniqueStringArray = z + .array(z.string()) + .superRefine((val, ctx) => { + if (val.length > 3) { + ctx.addIssue({ + code: 'too_big', + maximum: 3, + origin: 'array', + inclusive: true, + message: 'Too many items 😡', + input: val, + }); + } + + if (val.length !== new Set(val).size) { + ctx.addIssue({ + code: 'custom', + message: `No duplicates allowed.`, + input: val, + }); + } + }); +``` + +### `.check()` + +API `.refine()` является синтаксическим сахаром поверх более универсального (и многословного) API под названием `.check()`. Вы можете использовать этот API для создания нескольких проблем в одном уточнении или для полного контроля над сгенерированными объектами проблем. + +```ts +const UniqueStringArray = z + .array(z.string()) + .check((ctx) => { + if (ctx.value.length > 3) { + // полный контроль над объектами выпуска + ctx.issues.push({ + code: 'too_big', + maximum: 3, + origin: 'array', + inclusive: true, + message: 'Too many items 😡', + input: ctx.value, + }); + } + + // создать несколько задач в одном уточнении + if (ctx.value.length !== new Set(ctx.value).size) { + ctx.issues.push({ + code: 'custom', + message: `No duplicates allowed.`, + input: ctx.value, + continue: true, // сделать этот вопрос продолжительным + // (по умолчанию: false) + }); + } + }); +``` + +Обычный API `.refine` генерирует только проблемы с кодом ошибки `"custom"`, но `.check()` позволяет выбрасывать другие типы проблем. Для получения дополнительной информации о внутренних типах проблем Zod, прочтите документацию [Настройка ошибок](./error-customization.md). + +## Пайпы {#pipes} + +Схемы можно объединять в «пайпы». Пайпы в основном полезны при использовании в сочетании с [трансформациями](#transforms). + +```ts +const stringToLength = z + .string() + .pipe(z.transform((val) => val.length)); + +stringToLength.parse('hello'); // => 5 +``` + +## Трансформации {#transforms} + +**Трансформации** — это особый вид схем. Вместо проверки вводимых данных они принимают любые данные и выполняют над ними определенные преобразования. Чтобы определить трансформацию: + +```ts +const castToString = z.transform((val) => String(val)); + +castToString.parse('asdf'); // => "asdf" +castToString.parse(123); // => "123" +castToString.parse(true); // => "true" +``` + +Чтобы выполнить логику валидации внутри преобразования, используйте `ctx`. Чтобы сообщить о проблеме валидации, добавьте новую проблему в `ctx.issues` (аналогично API [`.check()`](#check)). + +```ts +const coercedInt = z.transform((val, ctx) => { + try { + const parsed = Number.parseInt(String(val)); + return parsed; + } catch (e) { + ctx.issues.push({ + code: 'custom', + message: 'Not a number', + input: val, + }); + + // это специальная константа типа `never` + // ее возвращение позволяет выйти из преобразования без влияния + // на выведенный тип возвращаемого значения + return z.NEVER; + } +}); +``` + +Чаще всего преобразования используются в сочетании с [пайпами](#pipes). Такое сочетание полезно для выполнения некоторой первоначальной проверки, а затем преобразования проанализированных данных в другую форму. + +```ts +const stringToLength = z + .string() + .pipe(z.transform((val) => val.length)); + +stringToLength.parse('hello'); // => 5 +``` + +### `.transform()` + +Передача схемы в трансформацию является распространенным паттерном, поэтому Zod предоставляет удобный метод `.transform()`. + +```ts +const stringToLength = z + .string() + .transform((val) => val.length); +``` + +Трансформации также могут быть асинхронными: + +```ts +const idToUser = z.string().transform(async (id) => { + // fetch user from database + return db.getUserById(id); +}); + +const user = await idToUser.parseAsync('abc123'); +``` + +!!!note "" + + Если вы используете асинхронные трансформации, при разборе данных необходимо использовать `.parseAsync` или `.safeParseAsync`! В противном случае Zod выдаст ошибку. + +### `.preprocess()` + +Передача преобразования в другую схему — еще один распространенный паттерн, поэтому Zod предоставляет удобную функцию `z.preprocess()`. + +```ts +const coercedInt = z.preprocess((val) => { + if (typeof val === 'string') { + return Number.parseInt(val); + } + return val; +}, z.int()); +``` + +## Настройки по умолчанию + +Чтобы установить значение по умолчанию для схемы: + +```ts +const defaultTuna = z.string().default('tuna'); + +defaultTuna.parse(undefined); // => "tuna" +``` + +В качестве альтернативы можно передать функцию, которая будет повторно выполняться всякий раз, когда необходимо сгенерировать значение по умолчанию: + +```ts +const randomDefault = z.number().default(Math.random); + +randomDefault.parse(undefined); // => 0.4413456736055323 +randomDefault.parse(undefined); // => 0.1871840107401901 +randomDefault.parse(undefined); // => 0.7223408162401552 +``` + +## Предустановленные значения + +В Zod установка _предустановленного_ значения приводит к прерыванию процесса разбора. Если входные данные имеют значение `undefined`, то предустановленное значение возвращается немедленно. Таким образом, предустановленное значение должно быть присвоимым _типу вывода_ схемы. + +```ts +const schema = z + .string() + .transform((val) => val.length) + .default(0); +schema.parse(undefined); // => 0 +``` + +Иногда полезно определить значение _prefault_ («предустановленное значение по умолчанию»). Если входные данные имеют значение `undefined`, вместо них будет проанализировано значение `prefault`. Процесс анализа _не_ прерывается. Таким образом, значение prefault должно быть присвоимым _типу входных данных_ схемы. + +```ts +z.string() + .transform((val) => val.length) + .prefault('tuna'); +schema.parse(undefined); // => 4 +``` + +Это также полезно, если вы хотите передать некоторое входное значение через некоторые изменяющиеся уточнения. + +```ts +const a = z + .string() + .trim() + .toUpperCase() + .prefault(' tuna '); +a.parse(undefined); // => "TUNA" + +const b = z + .string() + .trim() + .toUpperCase() + .default(' tuna '); +b.parse(undefined); // => " tuna " +``` + +## Catch + +Используйте `.catch()`, чтобы определить запасное значение, которое будет возвращено в случае ошибки проверки: + +```ts +const numberWithCatch = z.number().catch(42); + +numberWithCatch.parse(5); // => 5 +numberWithCatch.parse('tuna'); // => 42 +``` + +В качестве альтернативы можно передать функцию, которая будет повторно выполняться всякий раз, когда необходимо сгенерировать значение catch. + +```ts +const numberWithRandomCatch = z.number().catch((ctx) => { + ctx.error; // the caught ZodError + return Math.random(); +}); + +numberWithRandomCatch.parse('sup'); // => 0.4413456736055323 +numberWithRandomCatch.parse('sup'); // => 0.1871840107401901 +numberWithRandomCatch.parse('sup'); // => 0.7223408162401552 +``` + +## Брендированные типы + +Система типов TypeScript является [структурной](https://www.typescriptlang.org/docs/handbook/type-compatibility.html), что означает, что два типа, которые являются структурно эквивалентными, считаются одинаковыми. + +```ts +type Cat = { name: string }; +type Dog = { name: string }; + +const pluto: Dog = { name: 'pluto' }; +const simba: Cat = pluto; // works fine +``` + +В некоторых случаях может быть желательным имитировать [номинальное типирование](https://en.wikipedia.org/wiki/Nominal_type_system) внутри TypeScript. Это можно сделать с помощью _брендированных типов_ (также известных как «непрозрачные типы»). + +```ts +const Cat = z.object({ name: z.string() }).brand<'Cat'>(); +const Dog = z.object({ name: z.string() }).brand<'Dog'>(); + +type Cat = z.infer; // { name: string } & z.$brand<"Cat"> +type Dog = z.infer; // { name: string } & z.$brand<"Dog"> + +const pluto = Dog.parse({ name: 'pluto' }); +const simba: Cat = pluto; // ❌ not allowed +``` + +Под капотом это работает путем привязки «бренда» к выведенному типу схемы. + +```ts +const Cat = z.object({ name: z.string() }).brand<'Cat'>(); +type Cat = z.infer; // { name: string } & z.$brand<"Cat"> +``` + +С этим брендом любые простые (без бренда) структуры данных больше не могут быть присвоены выведенному типу. Чтобы получить брендированные данные, необходимо проанализировать некоторые данные с помощью схемы. + +> Обратите внимание, что брендированные типы не влияют на результат выполнения `.parse`. Это только статическая конструкция. + +## Readonly + +Чтобы пометить схему как доступную только для чтения: + +```ts +const ReadonlyUser = z + .object({ name: z.string() }) + .readonly(); +type ReadonlyUser = z.infer; +// Readonly<{ name: string }> +``` + +Выведенный тип новых схем будет помечен как `readonly`. Обратите внимание, что в TypeScript это влияет только на объекты, массивы, кортежи, `Set` и `Map`: + +```ts +z.object({ name: z.string() }).readonly(); // { readonly name: string } +z.array(z.string()).readonly(); // readonly string[] +z.tuple([z.string(), z.number()]).readonly(); // readonly [string, number] +z.map(z.string(), z.date()).readonly(); // ReadonlyMap +z.set(z.string()).readonly(); // ReadonlySet +``` + +Входные данные будут анализироваться как обычно, затем результат будет заморожен с помощью [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze), чтобы предотвратить изменения. + +```ts +const result = ReadonlyUser.parse({ name: 'fido' }); +result.name = 'simba'; // throws TypeError +``` + +## JSON + +Для проверки любого значения, которое можно закодировать в формате JSON: + +```ts +const jsonSchema = z.json(); +``` + +Это удобный API, который возвращает следующую объединенную схему: + +```ts +const jsonSchema = z.lazy(() => { + return z.union([ + z.string(params), + z.number(), + z.boolean(), + z.null(), + z.array(jsonSchema), + z.record(z.string(), jsonSchema), + ]); +}); +``` + +## Пользовательский + +Вы можете создать схему Zod для любого типа TypeScript с помощью `z.custom()`. Это полезно для создания схем для типов, которые не поддерживаются Zod из коробки, таких как шаблонные строковые литералы. + +```ts +const px = + z.custom < + `${number}px` > + ((val) => { + return typeof val === 'string' + ? /^\d+px$/.test(val) + : false; + }); + +type px = z.infer; // `${number}px` + +px.parse('42px'); // "42px" +px.parse('42vw'); // throws; +``` + +Если вы не предоставите функцию проверки, Zod будет допускать любые значения. Это может быть опасно! + +```ts +z.custom<{ arg: string }>(); // performs no validation +``` + +Вы можете настроить сообщение об ошибке и другие параметры, передавая второй аргумент. Этот параметр работает так же, как параметр params в [`.refine`](#refine). + +```ts +z.custom<...>((val) => ..., "custom error message"); +``` + +## Функции + +!!!warn "" + + В Zod 4 функция `z.function()` больше не возвращает схему Zod. + +Zod предоставляет утилиту `z.function()` для определения функций, проверенных Zod. Таким образом, вы можете избежать смешивания кода проверки с вашей бизнес-логикой. + +```ts +const MyFunction = z.function({ + input: [z.string()], // parameters (must be an array or a ZodTuple) + output: z.number(), // return type +}); +``` + +Схемы функций имеют метод `.implement()`, который принимает функцию и возвращает новую функцию, которая автоматически проверяет ее входы и выходы. + +```ts +const computeTrimmedLength = MyFunction.implement( + (input) => { + // TypeScript knows input is a string! + return input.trim().length; + } +); + +computeTrimmedLength('sandwich'); // => 8 +computeTrimmedLength(' asdf '); // => 4 +``` + +Эта функция вызовет ошибку `ZodError`, если входные данные недействительны: + +```ts +computeTrimmedLength(42); // throws ZodError +``` + +Если вас интересует только проверка входных данных, поле `output` можно опустить. + +```ts +const MyFunction = z.function({ + input: [z.string()], // parameters (must be an array or a ZodTuple) +}); + +const computeTrimmedLength = MyFunction.implement( + (input) => input.trim.length +); +``` diff --git a/docs/libs/zod.4/basics.md b/docs/libs/zod.4/basics.md new file mode 100644 index 0000000..6bb32f7 --- /dev/null +++ b/docs/libs/zod.4/basics.md @@ -0,0 +1,120 @@ +--- +description: На этой странице вы найдете основную информацию о создании схем, анализе данных и использовании выведенных типов +--- + +# Основные принципы использования + +На этой странице вы найдете основные сведения о создании схем, анализе данных и использовании выведенных типов. Полную документацию по API схем Zod см. в разделе [Определение схем](./api.md). + +## Определение схемы + +Прежде чем приступить к каким-либо действиям, необходимо определить схему. В целях данного руководства мы будем использовать простую схему объекта. + +```ts +import * as z from 'zod'; + +const Player = z.object({ + username: z.string(), + xp: z.number(), +}); +``` + +## Анализ данных + +Для любой схемы Zod используйте `.parse` для проверки входных данных. Если они действительны, Zod возвращает строго типизированный _глубокий клон_ входных данных. + +```ts +Player.parse({ username: 'billie', xp: 100 }); +// => returns { username: "billie", xp: 100 } +``` + +!!!note "" + + Если ваша схема использует определенные асинхронные API, такие как `async` [уточнения](./api.md/#refine) или [трансформации](./api.md#transform), вам необходимо использовать метод `.parseAsync()`. + + ```ts + await Player.parseAsync({ username: 'billie', xp: 100 }); + ``` + +## Обработка ошибок + +При сбое валидации метод `.parse()` выдает экземпляр `ZodError` с подробной информацией о проблемах валидации. + +```ts +try { + Player.parse({ username: 42, xp: '100' }); +} catch (error) { + if (error instanceof z.ZodError) { + error.issues; + /* + [ + { + expected: 'string', + code: 'invalid_type', + path: [ 'username' ], + message: 'Invalid input: expected string' + }, + { + expected: 'number', + code: 'invalid_type', + path: [ 'xp' ], + message: 'Invalid input: expected number' + } + ] + */ + } +} +``` + +Чтобы избежать использования блока `try/catch`, можно использовать метод `.safeParse()`, который возвращает простой объект результата, содержащий либо успешно проанализированные данные, либо `ZodError`. Тип результата является [дискриминированным объединением](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions), поэтому вы можете удобно обрабатывать оба случая. + +```ts +const result = Player.safeParse({ + username: 42, + xp: '100', +}); +if (!result.success) { + result.error; // ZodError instance +} else { + result.data; // { username: string; xp: number } +} +``` + +!!!note "" + + Если ваша схема использует определенные асинхронные API, такие как `async` [уточнения](./api.md#refine) или [преобразования](./api.md#transform), вам необходимо использовать метод `.safeParseAsync()`. + + ```ts + await schema.safeParseAsync('hello'); + ``` + +## Вывод типов + +Zod выводит статический тип из определений вашей схемы. Вы можете извлечь этот тип с помощью утилиты `z.infer<>` и использовать его по своему усмотрению. + +```ts +const Player = z.object({ + username: z.string(), + xp: z.number(), +}); + +// extract the inferred type +type Player = z.infer; + +// use it in your code +const player: Player = { username: 'billie', xp: 100 }; +``` + +В некоторых случаях типы ввода и вывода схемы могут различаться. Например, API `.transform()` может преобразовывать ввод из одного типа в другой. В таких случаях вы можете извлекать типы ввода и вывода независимо друг от друга: + +```ts +const mySchema = z.string().transform((val) => val.length); + +type MySchemaIn = z.input; +// => string + +type MySchemaOut = z.output; // equivalent to z.infer +// number +``` + +Теперь, когда мы рассмотрели основы, давайте перейдем к Schema API. diff --git a/docs/libs/zod.4/error-customization.md b/docs/libs/zod.4/error-customization.md new file mode 100644 index 0000000..124c4f0 --- /dev/null +++ b/docs/libs/zod.4/error-customization.md @@ -0,0 +1,351 @@ +--- +description: В Zod ошибки валидации отображаются как экземпляры класса +--- + +# Настройка ошибок + +В Zod ошибки валидации отображаются как экземпляры класса `z.core.$ZodError`. + +> Класс `ZodError` в пакете `zod` является подклассом, который реализует некоторые дополнительные удобные методы. + +Экземпляры `$ZodError` содержат массив `.issues`. Каждая проблема содержит удобочитаемое `message` и дополнительные структурированные метаданные о проблеме. + +```ts +import * as z from 'zod'; + +const result = z.string().safeParse(12); // { success: false, error: ZodError } +result.error.issues; +// [ +// { +// expected: 'string', +// code: 'invalid_type', +// path: [], +// message: 'Invalid input: expected string, received number' +// } +// ] +``` + +Каждый выпуск содержит свойство `message` с понятным для человека сообщением об ошибке. Сообщения об ошибках можно настраивать различными способами. + +## Параметр `error` + +Практически каждый API Zod принимает необязательное сообщение об ошибке. + +```ts +z.string('Not a string!'); +``` + +Эта настраиваемая ошибка будет отображаться как свойство `message` для всех проблем валидации, возникающих в этой схеме. + +```ts +z.string('Not a string!').parse(12); +// ❌ throws ZodError { +// issues: [ +// { +// expected: 'string', +// code: 'invalid_type', +// path: [], +// message: 'Not a string!' <-- 👀 custom error message +// } +// ] +// } +``` + +Все функции `z` и методы схемы принимают пользовательские ошибки. + +```ts +z.string('Bad!'); +z.string().min(5, 'Too short!'); +z.uuid('Bad UUID!'); +z.iso.date('Bad date!'); +z.array(z.string(), 'Not an array!'); +z.array(z.string()).min(5, 'Too few items!'); +z.set(z.string(), 'Bad set!'); +``` + +Если хотите, вы можете вместо этого передать объект params с параметром `error`. + +```ts +z.string({ error: 'Bad!' }); +z.string().min(5, { error: 'Too short!' }); +z.uuid({ error: 'Bad UUID!' }); +z.iso.date({ error: 'Bad date!' }); +z.array(z.string(), { error: 'Bad array!' }); +z.array(z.string()).min(5, { error: 'Too few items!' }); +z.set(z.string(), { error: 'Bad set!' }); +``` + +Параметр `error` опционально принимает функцию. Функция настройки ошибок в терминологии Zod называется **картой ошибок**. Карта ошибок запускается во время анализа, если происходит ошибка проверки. + +```ts +z.string({ + error: () => `[${Date.now()}]: Validation failure.`, +}); +``` + +!!!note "" + + В Zod v3 были отдельные параметры для `message` (строка) и `errorMap` (функция). В Zod 4 они были объединены в `error`. + +Карта ошибок получает объект контекста, который можно использовать для настройки сообщения об ошибке в зависимости от проблемы проверки. + +```ts +z.string({ + error: (iss) => + iss.input === undefined + ? 'Field is required.' + : 'Invalid input.', +}); +``` + +В сложных случаях объект `iss` предоставляет дополнительную информацию, которую можно использовать для настройки ошибки. + +```ts +z.string({ + error: (iss) => { + iss.code; // the issue code + iss.input; // the input data + iss.inst; // the schema/check that originated this issue + iss.path; // the path of the error + }, +}); +``` + +В зависимости от используемого API могут быть доступны дополнительные свойства. Воспользуйтесь автозаполнением TypeScript, чтобы ознакомиться с доступными свойствами. + +```ts +z.string().min(5, { + error: (iss) => { + // ...the same as above + iss.minimum; // the minimum value + iss.inclusive; // whether the minimum is inclusive + return `Password must have ${iss.minimum} characters or more`; + }, +}); +``` + +Возвращайте `undefined`, чтобы избежать настройки сообщения об ошибке и вернуться к сообщению по умолчанию. (Точнее говоря, Zod передаст управление следующей карте ошибок в [цепочке приоритетов](#error-precedence).) Это полезно для выборочной настройки определенных сообщений об ошибках, но не других. + +```ts +z.int64({ + error: (issue) => { + // override too_big error message + if (issue.code === 'too_big') { + return { + message: `Value must be <${issue.maximum}`, + }; + } + + // defer to default + return undefined; + }, +}); +``` + +## Настройка ошибок для каждого разбора + +Чтобы настроить ошибки для каждого разбора, передайте карту ошибок в метод разбора: + +```ts +const schema = z.string(); + +schema.parse(12, { + error: (iss) => 'per-parse custom error', +}); +``` + +Это имеет _более низкий приоритет_, чем любые настраиваемые сообщения на уровне схемы. + +```ts +const schema = z.string({ error: 'highest priority' }); +const result = schema.safeParse(12, { + error: (iss) => 'lower priority', +}); + +result.error.issues; +// [{ message: "highest priority", ... }] +``` + +Объект `iss` представляет собой [дискриминированное объединение](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) всех возможных типов проблем. Для их различения используйте свойство `code`. + +> Подробное описание всех кодов проблем Zod см. в документации [`zod/v4/core`](https://zod.dev/packages/core#issue-types). + +```ts +const result = schema.safeParse(12, { + error: (iss) => { + if (iss.code === 'invalid_type') { + return `invalid type, expected ${iss.expected}`; + } + if (iss.code === 'too_small') { + return `minimum is ${iss.minimum}`; + } + // ... + }, +}); +``` + +### Включение ввода в задачи + +По умолчанию Zod не включает вводные данные в задачи. Это сделано для предотвращения непреднамеренной регистрации потенциально конфиденциальных вводных данных. Чтобы включить вводные данные в каждую задачу, используйте флаг `reportInput`: + +```ts +z.string().parse(12, { + reportInput: true, +}); + +// ZodError: [ +// { +// "expected": "string", +// "code": "invalid_type", +// "input": 12, // 👀 +// "path": [], +// "message": "Invalid input: expected string, received number" +// } +// ] +``` + +## Настройка глобальных ошибок + +Чтобы указать глобальную карту ошибок, используйте `z.config()` для установки параметра конфигурации Zod `customError`: + +```ts +z.config({ + customError: (iss) => { + return 'globally modified error'; + }, +}); +``` + +Глобальные сообщения об ошибках имеют _более низкий приоритет_, чем сообщения об ошибках на уровне схемы или при разборе. + +Объект `iss` представляет собой [дискриминированное объединение](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) всех возможных типов проблем. Для их различения используйте свойство `code`. + +> Подробное описание всех кодов проблем Zod см. в документации [`zod/v4/core`](https://zod.dev/packages/core#issue-types). + +```ts +const result = schema.safeParse(12, { + error: (iss) => { + if (iss.code === 'invalid_type') { + return `invalid type, expected ${iss.expected}`; + } + if (iss.code === 'too_small') { + return `minimum is ${iss.minimum}`; + } + // ... + }, +}); +``` + +## Интернационализация + +Для поддержки интернационализации сообщений об ошибках Zod предоставляет несколько встроенных **локалей**. Они экспортируются из пакета `zod/v4/core`. + +!!!note "" + + Обычная библиотека `zod` автоматически загружает локаль `en`. Zod Mini по умолчанию не загружает никаких локалей; вместо этого все сообщения об ошибках по умолчанию имеют вид `Invalid input`. + +```ts +import * as z from 'zod'; +import { en } from 'zod/locales'; + +z.config(en()); +``` + +Чтобы загружать локаль в режиме отложенной загрузки, рассмотрите возможность использования динамического импорта: + +```ts +import * as z from 'zod'; + +async function loadLocale(locale: string) { + const { default: locale } = await import( + `zod/v4/locales/${locale}.js` + ); + z.config(locale()); +} + +await loadLocale('fr'); +``` + +Для удобства все локализации экспортируются как `z.locales` из `"zod"`. В некоторых пакетах это может быть невозможно. + +```ts +import * as z from 'zod'; + +z.config(z.locales.en()); +``` + +### Локальные настройки + +Доступны следующие локальные настройки: + +- `ar` — арабский +- `az` — азербайджанский +- `be` — белорусский +- `ca` — каталонский +- `cs` — чешский +- `de` — немецкий +- `en` — английский +- `eo` — эсперанто +- `es` — испанский +- `fa` — фарси +- `fi` — финский +- `fr` — французский +- `frCA` — канадский французский +- `he` — иврит +- `hu` — венгерский +- `id` — индонезийский +- `it` — итальянский +- `ja` — японский +- `kh` — кхмерский +- `ko` — корейский +- `mk` — македонский +- `ms` — малайский +- `nl` — нидерландский +- `no` — норвежский +- `ota` — тюркский +- `ps` — пушту +- `pl` — польский +- `pt` — португальский +- `ru` — русский +- `sl` — словенский +- `sv` — шведский +- `ta` — тамильский +- `th` — тайский +- `tr` — турецкий +- `ua` — украинский +- `ur` — урду +- `vi` — вьетнамский +- `zhCN` — упрощенный китайский +- `zhTW` — традиционный китайский + +## Приоритет ошибок {#error-precedence} + +Ниже приводится краткая справка по определению приоритета ошибок: если определено несколько настроек ошибок, какая из них имеет приоритет? От _наивысшего к наименьшему_ приоритету: + +1. **Ошибка на уровне схемы** — любое сообщение об ошибке, «жестко запрограммированное» в определении схемы. + + ```ts + z.string('Not a string!'); + ``` + +2. **Ошибка при разборе** — пользовательская карта ошибок, передаваемая в метод `.parse()`. + + ```ts + z.string().parse(12, { + error: (iss) => 'My custom error', + }); + ``` + +3. **Глобальная карта ошибок** — настраиваемая карта ошибок, передаваемая в `z.config()`. + + ```ts + z.config({ + customError: (iss) => 'My custom error', + }); + ``` + +4. **Карта локальных ошибок** — настраиваемая карта ошибок, передаваемая в `z.config()`. + + ```ts + z.config(z.locales.en()); + ``` diff --git a/docs/libs/zod.4/error-formatting.md b/docs/libs/zod.4/error-formatting.md new file mode 100644 index 0000000..a606cc6 --- /dev/null +++ b/docs/libs/zod.4/error-formatting.md @@ -0,0 +1,165 @@ +--- +title: Formatting errors +--- + +# Ошибки форматирования + +Zod уделяет особое внимание _полноте_ и _правильности_ отчетов об ошибках. Во многих случаях полезно преобразовать `$ZodError` в более удобный формат. Zod предоставляет для этого несколько утилит. + +Рассмотрим эту простую схему объекта. + +```ts +import * as z from 'zod'; + +const schema = z.strictObject({ + username: z.string(), + favoriteNumbers: z.array(z.number()), +}); +``` + +Попытка анализа этих недействительных данных приводит к ошибке, содержащей две проблемы. + +```ts +const result = schema.safeParse({ + username: 1234, + favoriteNumbers: [1234, '4567'], + extraKey: 1234, +}); + +result.error!.issues; +[ + { + expected: 'string', + code: 'invalid_type', + path: ['username'], + message: + 'Invalid input: expected string, received number', + }, + { + expected: 'number', + code: 'invalid_type', + path: ['favoriteNumbers', 1], + message: + 'Invalid input: expected number, received string', + }, + { + code: 'unrecognized_keys', + keys: ['extraKey'], + path: [], + message: 'Unrecognized key: "extraKey"', + }, +]; +``` + +## `z.treeifyError()` + +Чтобы преобразовать ("treeify") эту ошибку в вложенный объект, используйте `z.treeifyError()`. + +```ts +const tree = z.treeifyError(result.error); + +// => +// { +// errors: [ 'Unrecognized key: "extraKey"' ], +// properties: { +// username: { errors: [ 'Invalid input: expected string, received number' ] }, +// favoriteNumbers: { +// errors: [], +// items: [ +// undefined, +// { +// errors: [ 'Invalid input: expected number, received string' ], +// } +// ] +// } +// } +// } +``` + +В результате получается вложенная структура, которая отражает саму схему. Вы можете легко получить доступ к ошибкам, которые произошли на определенном пути. Поле `errors` содержит сообщения об ошибках на данном пути, а специальные свойства `properties` и `items` позволяют вам продвигаться глубже в дерево. + +```ts +tree.properties?.username?.errors; +// => ["Invalid input: expected string, received number"] + +tree.properties?.favoriteNumbers?.items?.[1]?.errors; +// => ["Invalid input: expected number, received string"]; +``` + +> Обязательно используйте опциональное цепочечное соединение (`?.`) для предотвращения ошибок при доступе к вложенным свойствам. + +## `z.prettifyError()` + +Функция `z.prettifyError()` предоставляет удобочитаемое строковое представление ошибки. + +```ts +const pretty = z.prettifyError(result.error); +``` + +Это возвращает следующую строку: + +``` +✖ Unrecognized key: "extraKey" +✖ Invalid input: expected string, received number + → at username +✖ Invalid input: expected number, received string + → at favoriteNumbers[1] +``` + +## `z.formatError()` + +!!!warn "" + + Это было признано устаревшим в пользу `z.treeifyError()`. + +Чтобы преобразовать ошибку в вложенный объект: + +```ts +const formatted = z.formatError(result.error); + +// returns: +// { +// _errors: [ 'Unrecognized key: "extraKey"' ], +// username: { _errors: [ 'Invalid input: expected string, received number' ] }, +// favoriteNumbers: { +// '1': { _errors: [ 'Invalid input: expected number, received string' ] }, +// _errors: [] +// } +// } +``` + +В результате получается вложенная структура, которая отражает саму схему. Вы можете легко получить доступ к ошибкам, которые произошли на определенном пути. + +```ts +formatted?.username?._errors; +// => ["Invalid input: expected string, received number"] + +formatted?.favoriteNumbers?.[1]?._errors; +// => ["Invalid input: expected number, received string"] +``` + +> Обязательно используйте опциональное цепочечное соединение (`?.`) для предотвращения ошибок при доступе к вложенным свойствам. + +## `z.flattenError()` + +Хотя `z.treeifyError()` полезен для обхода потенциально сложной вложенной структуры, большинство схем являются _плоскими_ — всего одного уровня глубины. В этом случае используйте `z.flattenError()` для получения чистого, неглубокого объекта ошибки. + +```ts +const flattened = z.flattenError(result.error); +// { errors: string[], properties: { [key: string]: string[] } } + +// { +// formErrors: [ 'Unrecognized key: "extraKey"' ], +// fieldErrors: { +// username: [ 'Invalid input: expected string, received number' ], +// favoriteNumbers: [ 'Invalid input: expected number, received string' ] +// } +// } +``` + +Массив `formErrors` содержит все ошибки верхнего уровня (где `path` равен `[]`). Объект `fieldErrors` предоставляет массив ошибок для каждого поля в схеме. + +```ts +flattened.fieldErrors.username; // => [ 'Invalid input: expected string, received number' ] +flattened.fieldErrors.favoriteNumbers; // => [ 'Invalid input: expected number, received string' ] +``` diff --git a/docs/libs/zod.4/index.md b/docs/libs/zod.4/index.md new file mode 100644 index 0000000..851dd72 --- /dev/null +++ b/docs/libs/zod.4/index.md @@ -0,0 +1,86 @@ +--- +description: Zod — это библиотека для проверки данных, основанная на TypeScript +--- + +# Zod + +## Введение + +**Zod** — это библиотека для проверки данных, основанная на TypeScript. С помощью Zod вы можете определять схемы, которые можно использовать для проверки данных, от простых строк до сложных вложенных объектов. + +```ts +import * as z from 'zod'; + +const User = z.object({ + name: z.string(), +}); + +// some untrusted data... +const input = { + /* stuff */ +}; + +// the parsed result is validated and type safe! +const data = User.parse(input); + +// so you can use it with confidence :) +console.log(data.name); +``` + +## Особенности + +- Полное отсутствие внешних зависимостей +- Работает в Node.js и всех современных браузерах +- Миниатюрный: 2 КБ основного пакета (сжатый gzip) +- Неизменяемый API: методы возвращают новый экземпляр +- Лаконичный интерфейс +- Работает с TypeScript и простым JS +- Встроенное преобразование JSON Schema +- Обширная экосистема + +## Установка + +```sh +npm install zod # npm +deno add npm:zod # deno +yarn add zod # yarn +bun add zod # bun +pnpm add zod # pnpm +``` + +Zod также публикует тестовую версию при каждом коммите. Чтобы установить тестовую версию: + +```sh +npm install zod@canary # npm +deno add npm:zod@canary # deno +yarn add zod@canary # yarn +bun add zod@canary # bun +pnpm add zod@canary # pnpm +``` + +## Требования + +Zod тестируется на _TypeScript v5.5_ и более поздних версиях. Более старые версии могут работать, но официально не поддерживаются. + +### `"strict"` + +Вы должны включить режим `strict` в вашем `tsconfig.json`. Это лучшая практика для всех проектов TypeScript. + +```json +// tsconfig.json +{ + // ... + "compilerOptions": { + // ... + "strict": true + } +} +``` + +### `"moduleResolution"` + +Ваш `"moduleResolution"` должен быть установлен в одно из следующих значений. Устаревшие режимы `"node"` и `"classic"` не поддерживаются, так как они не поддерживают импорт подпутей. + +- `"node16"` (default if `"module"` is set to `"node16"`/`"node18"`) +- `"nodenext"` (default if `"module"` is set to `"nodenext"`) +- `"bundler"` diff --git a/docs/libs/zod.4/json-schema.md b/docs/libs/zod.4/json-schema.md new file mode 100644 index 0000000..4782a7a --- /dev/null +++ b/docs/libs/zod.4/json-schema.md @@ -0,0 +1,526 @@ +--- +description: Zod 4 представляет новую функцию - встроенное преобразование JSON Schema +--- + +# JSON схема + +!!!note "" + + Zod 4 представляет новую функцию: встроенное преобразование [JSON Schema](https://json-schema.org/). JSON Schema — это стандарт для описания структуры JSON (с помощью JSON). Он широко используется в определениях [OpenAPI](https://www.openapis.org/) и определении [структурированных выводов](https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat) для ИИ. + +Чтобы преобразовать схему Zod в схему JSON, используйте функцию `z.toJSONSchema()`. + +```ts +import * as z from 'zod'; + +const schema = z.object({ + name: z.string(), + age: z.number(), +}); + +z.toJSONSchema(schema); +// => { +// type: 'object', +// properties: { name: { type: 'string' }, age: { type: 'number' } }, +// required: [ 'name', 'age' ], +// additionalProperties: false, +// } +``` + +Все схемы и проверки преобразуются в наиболее близкий эквивалент JSON Schema. Некоторые типы не имеют аналогов и не могут быть адекватно представлены. Дополнительные сведения об обработке таких случаев см. в разделе [`unrepresentable`](#unrepresentable) ниже. + +```ts +z.bigint(); // ❌ +z.int64(); // ❌ +z.symbol(); // ❌ +z.void(); // ❌ +z.date(); // ❌ +z.map(); // ❌ +z.set(); // ❌ +z.transform(); // ❌ +z.nan(); // ❌ +z.custom(); // ❌ +``` + +## Форматы строк + +Zod преобразует следующие типы схем в эквивалентный формат JSON Schema `format`: + +```ts +// Supported via `format` +z.email(); // => { type: "string", format: "email" } +z.iso.datetime(); // => { type: "string", format: "date-time" } +z.iso.date(); // => { type: "string", format: "date" } +z.iso.time(); // => { type: "string", format: "time" } +z.iso.duration(); // => { type: "string", format: "duration" } +z.ipv4(); // => { type: "string", format: "ipv4" } +z.ipv6(); // => { type: "string", format: "ipv6" } +z.uuid(); // => { type: "string", format: "uuid" } +z.guid(); // => { type: "string", format: "uuid" } +z.url(); // => { type: "string", format: "uri" } +``` + +Эти схемы поддерживаются через `contentEncoding`: + +```ts +z.base64(); // => { type: "string", contentEncoding: "base64" } +``` + +Все другие форматы строк поддерживаются через `pattern`: + +```ts +z.base64url(); +z.cuid(); +z.regex(); +z.emoji(); +z.nanoid(); +z.cuid2(); +z.ulid(); +z.cidrv4(); +z.cidrv6(); +``` + +## Числовые типы + +Zod преобразует следующие числовые типы в JSON Schema: + +```ts +// number +z.number(); // => { type: "number" } +z.float32(); // => { type: "number", exclusiveMinimum: ..., exclusiveMaximum: ... } +z.float64(); // => { type: "number", exclusiveMinimum: ..., exclusiveMaximum: ... } + +// integer +z.int(); // => { type: "integer" } +z.int32(); // => { type: "integer", exclusiveMinimum: ..., exclusiveMaximum: ... } +``` + +## Схемы объектов + +По умолчанию схемы `z.object()` содержат `additionalProperties: "false"`. Это точно отражает поведение Zod по умолчанию, поскольку простая схема `z.object()` удаляет дополнительные свойства. + +```ts +import * as z from 'zod'; + +const schema = z.object({ + name: z.string(), + age: z.number(), +}); + +z.toJSONSchema(schema); +// => { +// type: 'object', +// properties: { name: { type: 'string' }, age: { type: 'number' } }, +// required: [ 'name', 'age' ], +// additionalProperties: false, +// } +``` + +При преобразовании в JSON Schema в режиме `"input"`, `additionalProperties` не устанавливается. Дополнительную информацию см. в [документации по io](#io). + +```ts +import * as z from 'zod'; + +const schema = z.object({ + name: z.string(), + age: z.number(), +}); + +z.toJSONSchema(schema, { io: 'input' }); +// => { +// type: 'object', +// properties: { name: { type: 'string' }, age: { type: 'number' } }, +// required: [ 'name', 'age' ], +// } +``` + +Напротив: + +- `z.looseObject()` никогда не будет устанавливать `additionalProperties: false` +- `z.strictObject()` всегда будет устанавливать `additionalProperties: false` + +## Схемы файлов + +Zod преобразует `z.file()` в следующую схему, совместимую с OpenAPI: + +```ts +z.file(); +// => { type: "string", format: "binary", contentEncoding: "binary" } +``` + +Также представлены проверки размера и MIME: + +```ts +z.file() + .min(1) + .max(1024 * 1024) + .mime('image/png'); +// => { +// type: "string", +// format: "binary", +// contentEncoding: "binary", +// contentMediaType: "image/png", +// minLength: 1, +// maxLength: 1048576, +// } +``` + +## Nullability + +Zod преобразует как `undefined`/`null`, так и `{ type: "null" }` в JSON Schema. + +```ts +z.null(); +// => { type: "null" } + +z.undefined(); +// => { type: "null" } +``` + +Аналогично, `nullable` представляется с помощью объединения с `null`: + +```ts +z.nullable(z.string()); +// => { oneOf: [{ type: "string" }, { type: "null" }] } +``` + +Необязательные схемы представлены в том виде, в котором они есть, хотя они украшены аннотацией `optional`. + +```ts +z.optional(z.string()); +// => { type: "string" } +``` + +## Конфигурация + +Второй аргумент можно использовать для настройки логики преобразования. + +```ts +z.toJSONSchema(schema, { + // ...params +}); +``` + +Ниже приводится краткая справка по каждому поддерживаемому параметру. Каждый из них более подробно описан ниже. + +```ts +interface ToJSONSchemaParams { + /** The JSON Schema version to target. + * - `"draft-2020-12"` — Default. JSON Schema Draft 2020-12 + * - `"draft-7"` — JSON Schema Draft 7 */ + target?: 'draft-7' | 'draft-2020-12'; + + /** A registry used to look up metadata for each schema. + * Any schema with an `id` property will be extracted as a $def. */ + metadata?: $ZodRegistry>; + + /** How to handle unrepresentable types. + * - `"throw"` — Default. Unrepresentable types throw an error + * - `"any"` — Unrepresentable types become `{}` */ + unrepresentable?: 'throw' | 'any'; + + /** How to handle cycles. + * - `"ref"` — Default. Cycles will be broken using $defs + * - `"throw"` — Cycles will throw an error if encountered */ + cycles?: 'ref' | 'throw'; + + /* How to handle reused schemas. + * - `"inline"` — Default. Reused schemas will be inlined + * - `"ref"` — Reused schemas will be extracted as $defs */ + reused?: 'ref' | 'inline'; + + /** A function used to convert `id` values to URIs to be used in *external* $refs. + * + * Default is `(id) => id`. + */ + uri?: (id: string) => string; +} +``` + +### `target` + +Чтобы установить целевую версию JSON Schema, используйте параметр `target`. По умолчанию Zod будет ориентироваться на Draft 2020-12. + +```ts +z.toJSONSchema(schema, { target: 'draft-7' }); +z.toJSONSchema(schema, { target: 'draft-2020-12' }); +``` + +### `metadata` + +> Если вы еще не сделали этого, прочтите страницу [Метаданные и реестры](./metadata.md), чтобы узнать о хранении метаданных в Zod. + +В Zod метаданные хранятся в реестрах. Zod экспортирует глобальный реестр `z.globalRegistry`, который можно использовать для хранения общих полей метаданных, таких как `id`, `title`, `description` и `examples`. + +```ts +import * as z from 'zod'; + +// `.meta()` is a convenience method for registering a schema in `z.globalRegistry` +const emailSchema = z.string().meta({ + title: 'Email address', + description: 'Your email address', +}); + +z.toJSONSchema(emailSchema); +// => { type: "string", title: "Email address", description: "Your email address", ... } +``` + +Все поля метаданных копируются в результирующую схему JSON. + +```ts +const schema = z.string().meta({ + whatever: 1234, +}); + +z.toJSONSchema(schema); +// => { type: "string", whatever: 1234 } +``` + +### `unrepresentable` {#unrepresentable} + +Следующие API не могут быть представлены в JSON Schema. По умолчанию Zod выдаст ошибку, если они будут обнаружены. Попытка преобразования в JSON Schema нецелесообразна; вам следует изменить свои схемы, поскольку они не имеют эквивалента в JSON. При обнаружении любого из них будет выведена ошибка. + +```ts +z.bigint(); // ❌ +z.int64(); // ❌ +z.symbol(); // ❌ +z.void(); // ❌ +z.date(); // ❌ +z.map(); // ❌ +z.set(); // ❌ +z.transform(); // ❌ +z.nan(); // ❌ +z.custom(); // ❌ +``` + +По умолчанию Zod выдаст ошибку, если встретит любое из этих событий. + +```ts +z.toJSONSchema(z.bigint()); +// => throws Error +``` + +Вы можете изменить это поведение, установив для параметра `unrepresentable` значение `"any"`. Это приведет к преобразованию всех непредставимых типов в `{}` (эквивалент `unknown` в JSON Schema). + +```ts +z.toJSONSchema(z.bigint(), { unrepresentable: 'any' }); +// => {} +``` + +### `cycles` + +Как обрабатывать циклы. Если при прохождении схемы функцией `z.toJSONSchema()` встречается цикл, он будет представлен с помощью `$ref`. + +```ts +const User = z.object({ + name: z.string(), + get friend() { + return User; + }, +}); + +z.toJSONSchema(User); +// => { +// type: 'object', +// properties: { name: { type: 'string' }, friend: { '$ref': '#' } }, +// required: [ 'name', 'friend' ], +// additionalProperties: false, +// } +``` + +Если же вы хотите выдать ошибку, установите для параметра `cycles` значение `"throw"`. + +```ts +z.toJSONSchema(User, { cycles: 'throw' }); +// => throws Error +``` + +### `reused` + +Как обрабатывать схемы, которые повторяются несколько раз в одной схеме. По умолчанию Zod будет вставлять эти схемы в текст. + +```ts +const name = z.string(); +const User = z.object({ + firstName: name, + lastName: name, +}); + +z.toJSONSchema(User); +// => { +// type: 'object', +// properties: { +// firstName: { type: 'string' }, +// lastName: { type: 'string' } +// }, +// required: [ 'firstName', 'lastName' ], +// additionalProperties: false, +// } +``` + +Вместо этого вы можете установить опцию `reused` в значение `"ref"`, чтобы извлечь эти схемы в `$defs`. + +```ts +z.toJSONSchema(User, { reused: 'ref' }); +// => { +// type: 'object', +// properties: { +// firstName: { '$ref': '#/$defs/__schema0' }, +// lastName: { '$ref': '#/$defs/__schema0' } +// }, +// required: [ 'firstName', 'lastName' ], +// additionalProperties: false, +// '$defs': { __schema0: { type: 'string' } } +// } +``` + +### `override` + +Чтобы определить некоторую пользовательскую логику переопределения, используйте `override`. Предоставленный обратный вызов имеет доступ к исходной схеме Zod и схеме JSON по умолчанию. _Эта функция должна напрямую изменять `ctx.jsonSchema`._ + +```ts +const mySchema = + /* ... */ + z.toJSONSchema(mySchema, { + override: (ctx) => { + ctx.zodSchema; // the original Zod schema + ctx.jsonSchema; // the default JSON Schema + + // directly modify + ctx.jsonSchema.whatever = 'sup'; + }, + }); +``` + +Обратите внимание, что непредставимые типы будут вызывать ошибку `Error` перед вызовом этой функции. Если вы пытаетесь определить пользовательское поведение для непредставимого типа, вам необходимо использовать настройку `unrepresentable: "any"` вместе с `override`. + +```ts +// support z.date() as ISO datetime strings +const result = z.toJSONSchema(z.date(), { + unrepresentable: 'any', + override: (ctx) => { + const def = ctx.zodSchema._zod.def; + if (def.type === 'date') { + ctx.jsonSchema.type = 'string'; + ctx.jsonSchema.format = 'date-time'; + } + }, +}); +``` + +### `io` {#io} + +Некоторые типы схем имеют разные типы ввода и вывода, например `ZodPipe`, `ZodDefault` и принудительные примитивы. По умолчанию результат `z.toJSONSchema` представляет тип вывода; чтобы извлечь тип ввода, используйте `"io": "input"`. + +```ts +const mySchema = z + .string() + .transform((val) => val.length) + .pipe(z.number()); +// ZodPipe + +const jsonSchema = z.toJSONSchema(mySchema); +// => { type: "number" } + +const jsonSchema = z.toJSONSchema(mySchema, { + io: 'input', +}); +// => { type: "string" } +``` + +## Реестры + +Передача схемы в `z.toJSONSchema()` вернет _самостоятельную_ схему JSON. + +В других случаях у вас может быть набор схем Zod, которые вы хотите представить с помощью нескольких взаимосвязанных схем JSON, например, для записи в файлы `.json` и обслуживания с веб-сервера. + +```ts +import * as z from 'zod'; + +const User = z.object({ + name: z.string(), + get posts() { + return z.array(Post); + }, +}); + +const Post = z.object({ + title: z.string(), + content: z.string(), + get author() { + return User; + }, +}); + +z.globalRegistry.add(User, { id: 'User' }); +z.globalRegistry.add(Post, { id: 'Post' }); +``` + +Для этого можно передать [реестр](./metadata.md#registries) в `z.toJSONSchema()`. + +> **Важно** — Все схемы должны иметь зарегистрированное свойство `id` в реестре! Любые схемы без `id` будут игнорироваться. + +```ts +z.toJSONSchema(z.globalRegistry); +// => { +// schemas: { +// User: { +// id: 'User', +// type: 'object', +// properties: { +// name: { type: 'string' }, +// posts: { type: 'array', items: { '$ref': 'Post' } } +// }, +// required: [ 'name', 'posts' ], +// additionalProperties: false, +// }, +// Post: { +// id: 'Post', +// type: 'object', +// properties: { +// title: { type: 'string' }, +// content: { type: 'string' }, +// author: { '$ref': 'User' } +// }, +// required: [ 'title', 'content', 'author' ], +// additionalProperties: false, +// } +// } +// } +``` + +По умолчанию URI `$ref` представляют собой простые относительные пути, такие как `"User"`. Чтобы сделать эти URI абсолютными, используйте опцию `uri`. Для этого требуется функция, которая преобразует `id` в полный URI. + +```ts +z.toJSONSchema(z.globalRegistry, { + uri: (id) => `https://example.com/${id}.json`, +}); +// => { +// schemas: { +// User: { +// id: 'User', +// type: 'object', +// properties: { +// name: { type: 'string' }, +// posts: { +// type: 'array', +// items: { '$ref': 'https://example.com/Post.json' } +// } +// }, +// required: [ 'name', 'posts' ], +// additionalProperties: false, +// }, +// Post: { +// id: 'Post', +// type: 'object', +// properties: { +// title: { type: 'string' }, +// content: { type: 'string' }, +// author: { '$ref': 'https://example.com/User.json' } +// }, +// required: [ 'title', 'content', 'author' ], +// additionalProperties: false, +// } +// } +// } +``` diff --git a/docs/libs/zod.4/metadata.md b/docs/libs/zod.4/metadata.md new file mode 100644 index 0000000..a8a985d --- /dev/null +++ b/docs/libs/zod.4/metadata.md @@ -0,0 +1,205 @@ +--- +description: Часто бывает полезно связать схему с некоторыми дополнительными метаданными для документирования, генерации кода, структурированных выводов ИИ, проверки форм и других целей +--- + +# Метаданные и реестры + +Часто бывает полезно связать схему с некоторыми дополнительными метаданными для документирования, генерации кода, структурированных выводов ИИ, проверки форм и других целей. + +## Реестры {#registries} + +Метаданные в Zod обрабатываются с помощью реестров. Реестры — это наборы схем, каждая из которых связана с некоторыми строго типизированными метаданными. Чтобы создать простой реестр: + +```ts +import * as z from 'zod'; + +const myRegistry = z.registry<{ description: string }>(); +``` + +Чтобы зарегистрировать, найти и удалить схемы из этого реестра: + +```ts +const mySchema = z.string(); + +myRegistry.add(mySchema, { description: 'A cool schema!' }); +myRegistry.has(mySchema); // => true +myRegistry.get(mySchema); // => { description: "A cool schema!" } +myRegistry.remove(mySchema); +myRegistry.clear(); // wipe registry +``` + +TypeScript требует, чтобы метаданные для каждой схемы соответствовали **типу метаданных** реестра. + +```ts +myRegistry.add(mySchema, { description: 'A cool schema!' }); // ✅ +myRegistry.add(mySchema, { description: 123 }); // ❌ +``` + +> **Особый подход к `id`** — реестры Zod обрабатывают свойство `id` особым образом. Если несколько схем зарегистрированы с одинаковым значением `id`, будет сгенерирована ошибка `Error`. Это правило действует для всех реестров, включая глобальный реестр. + +### `.register()` + +> **Примечание** — Этот метод отличается тем, что он не возвращает новую схему, а возвращает исходную схему. Ни один другой метод Zod не делает этого! Это включает в себя `.meta()` и `.describe()` (описанные ниже), которые возвращают новый экземпляр. + +Схемы предоставляют метод `.register()` для более удобного добавления в реестр. + +```ts +const mySchema = z.string(); + +mySchema.register(myRegistry, { + description: 'A cool schema!', +}); +// => mySchema +``` + +Это позволяет вам определять метаданные «встроенными» в ваших схемах. + +```ts +const mySchema = z.object({ + name: z.string().register(myRegistry, { + description: "The user's name", + }), + age: z.number().register(myRegistry, { + description: "The user's age", + }), +}); +``` + +!!!note "" + + Если реестр определен без типа метаданных, его можно использовать как общую «коллекцию», метаданные не требуются. + + ```ts + const myRegistry = z.registry(); + + myRegistry.add(z.string()); + myRegistry.add(z.number()); + ``` + +## Metadata + +### `z.globalRegistry` + +Для удобства Zod предоставляет глобальный реестр (`z.globalRegistry`), который можно использовать для хранения метаданных для генерации JSON Schema или других целей. Он принимает следующие метаданные: + +```ts +export interface GlobalMeta { + id?: string; + title?: string; + description?: string; + example?: unknown; + examples?: + | unknown[] // array-style (JSON Schema) + | Record< + string, + { value: unknown; [k: string]: unknown } + >; // map-style (OpenAPI) + deprecated?: boolean; + [k: string]: unknown; +} +``` + +Чтобы зарегистрировать некоторые метаданные в `z.globalRegistry` для схемы: + +```ts +import * as z from 'zod'; + +const emailSchema = z.email().register(z.globalRegistry, { + id: 'email_address', + title: 'Email address', + description: 'Your email address', + examples: ['first.last@example.com'], +}); +``` + +### `.meta()` + +Для более удобного подхода используйте метод `.meta()`, чтобы зарегистрировать схему в `z.globalRegistry`. + +```ts +const emailSchema = z.email().meta({ + id: 'email_address', + title: 'Email address', + description: 'Please enter a valid email address', +}); +``` + +Вызов `.meta()` без аргумента _извлекает_ метаданные для схемы. + +```ts +emailSchema.meta(); +// => { id: "email_address", title: "Email address", ... } +``` + +Метаданные связаны с _конкретным экземпляром схемы_. Это важно иметь в виду, особенно учитывая, что методы Zod являются неизменяемыми — они всегда возвращают новый экземпляр. + +```ts +const A = z.string().meta({ description: 'A cool string' }); +A.meta(); // => { hello: "true" } + +const B = A.refine((_) => true); +B.meta(); // => undefined +``` + +### `.describe()` + +!!!note "" + + Метод `.describe()` по-прежнему существует для обеспечения совместимости с Zod 3, но теперь рекомендуется использовать метод `.meta()`. + +Метод `.describe()` является сокращением для регистрации схемы в `z.globalRegistry` только с полем `description`. + +```ts +const emailSchema = z.email(); +emailSchema.describe('An email address'); + +// equivalent to +emailSchema.meta({ description: 'An email address' }); +``` + +## Пользовательские реестры + +Вы уже видели простой пример пользовательского реестра: + +```ts +import * as z from "zod"; + +const myRegistry = z.registry<{ description: string };>(); +``` + +Давайте рассмотрим несколько более сложных шаблонов. + +### Ссылки на выведенные типы + +Часто бывает полезно, чтобы тип метаданных ссылался на _выведенный тип_ схемы. Например, вы можете захотеть, чтобы поле `examples` содержало примеры вывода схемы. + +```ts +import * as z from 'zod'; + +type MyMeta = { examples: z.$output[] }; +const myRegistry = z.registry(); + +myRegistry.add(z.string(), { + examples: ['hello', 'world'], +}); +myRegistry.add(z.number(), { examples: [1, 2, 3] }); +``` + +Специальный символ `z.$output` является ссылкой на тип вывода, выведенный из схемы (`z.infer`). Аналогично, вы можете использовать `z.$input` для ссылки на тип ввода. + +### Ограничение типов схем + +Передайте второй общий параметр в `z.registry()`, чтобы ограничить типы схем, которые могут быть добавлены в реестр. Этот реестр принимает только строковые схемы. + +```ts +import * as z from 'zod'; + +const myRegistry = z.registry< + { description: string }, + z.ZodString +>(); + +myRegistry.add(z.string(), { description: 'A number' }); // ✅ +myRegistry.add(z.number(), { description: 'A number' }); // ❌ +// ^ 'ZodNumber' is not assignable to parameter of type 'ZodString' +``` diff --git a/docs/prisma-orm/.pages b/docs/prisma-orm/.pages deleted file mode 100644 index 9fc26d9..0000000 --- a/docs/prisma-orm/.pages +++ /dev/null @@ -1,12 +0,0 @@ -title: Prisma ORM -nav: - - index.md - - init.md - - cli.md - - schema.md - - client.md - - setup.md - - nested.md - - filter.md - - method.md - - transactions.md diff --git a/docs/prisma-orm/cli.md b/docs/prisma-orm/cli.md deleted file mode 100644 index aba6814..0000000 --- a/docs/prisma-orm/cli.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: CLI -description: Интерфейс командной строки ---- - -# CLI - -**Интерфейс командной строки** (Command line interface, CLI) Prisma предоставляет следующие основные возможности (команды): - -**`init`** — создает шаблон Prisma-проекта: - -- `--datasource-provider` — провайдер для работы с БД: `sqlite`, `postgresql`, `mysql`, `sqlserver` или `mongodb` (перезаписывает datasource из `schema.prisma`); -- `--url` — адрес БД (перезаписывает `DATABASE_URL`). - -```sh -npx prisma init --datasource-provider mysql --url mysql://user:password@localhost:3306/mydb -``` - -**`generate`** — генерирует клиента Prisma на основе схемы (`schema.prisma`). Клиент Prisma предоставляет программный интерфейс приложения (Application Programming Interface, API) для работы с моделями и типы для TypeScript. - -```sh -npx prisma generate -``` - -**`db pull`** — генерирует модели на основе существующей схемы БД. - -```sh -npx prisma db pull -``` - -**`db push`** — синхронизирует состояние схемы Prisma с БД без выполнения миграций. БД создается при отсутствии. Используется для прототипировании БД и в локальной разработке. Также может быть полезной в случае ограниченного доступа к БД, например, при использовании БД, предоставляемой облачными провайдерами, такими как ElephantSQL или Heroku. - -```sh -npx prisma db push -``` - -**`seed`** — выполняет скрипт для наполнения БД начальными (фиктивными) данными. Путь к соответствующему файлу определяется в `package.json`. - -```json -"prisma": { - "seed": "node prisma/seed.js" -} -``` - -```sh -npx prisma seed -``` - -**`migrate`** - -- `dev` — выполняет миграцию для разработки: -- `--name` — название миграции - -```sh -npx prisma migrate dev --name init -``` - -Это приводит к созданию БД при ее отсутствии, генерации файла `prisma/migrations/migration_name.sql`, выполнению инструкции из этого файла (синхронизации БД со схемой) и генерации (регенерации) клиента (`prisma generate`). - -Данная команда должна выполняться после каждого изменения схемы. - -**`reset`** — удаляет и заново создает БД или выполняет "мягкий сброс", удаляя все данные, таблицы, индексы и другие артефакты - -```sh -npx prisma migrate reset -``` - -**`deploy`** — выполняет производственную миграцию - -```sh -npx prisma migrate deploy -``` - -**`studio`** — позволяет просматривать и управлять данными, хранящимися в БД, в интерактивном режиме: - -- `--browser`, `-b` — название браузера (по умолчанию используется дефолтный браузер); -- `--port`, -p — номер порта (по умолчанию — `5555`) - -```sh -npx prisma studio -``` - -без автоматического открытия вкладки браузера - -```sh -npx prisma studio -b none -``` - -Подробнее о CLI можно почитать [здесь](https://www.prisma.io/docs/reference/api-reference/command-reference). diff --git a/docs/prisma-orm/client.md b/docs/prisma-orm/client.md deleted file mode 100644 index f1f32cf..0000000 --- a/docs/prisma-orm/client.md +++ /dev/null @@ -1,573 +0,0 @@ ---- -title: Клиент -description: Импортируем и создаем экземпляр клиента Prisma ---- - -# Клиент - -Импортируем и создаем экземпляр клиента Prisma: - -```js -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -export default prisma; -``` - -Иногда может потребоваться делать так: - -```js -const Prisma = require('prisma'); -const prisma = new Prisma.PrismaClient(); -module.exports = prisma; -``` - -## Запросы - -### findUnique - -`findUnique` позволяет извлекать единичные записи по идентификатору или уникальному полю. - -**Сигнатура** - -``` -findUnique({ - where: condition, - select?: fields, - include?: relations, - rejectOnNotFound?: boolean -}) -``` - -Модификатор `?` означает, что поле является опциональным. - -- `condition` — условие для выборки; -- `fields` — поля для выборки; -- `relations` — отношения (связанные поля) для выборки; -- `rejectOnNotFound` — если имеет значение `true`, при отсутствии записи выбрасывается исключение `NotFoundError`. Если имеет значение `false`, при отсутствии записи возвращается `null`. - -**Пример** - -```js -async function getUserById(id) { - try { - const user = await prisma.user.findUnique({ - where: { - id, - }, - }); - return user; - } catch (e) { - onError(e); - } -} -``` - -### findFirst - -`findFirst` возвращает первую запись, соответствующую заданному критерию. - -**Сигнатура** - -``` -findFirst({ - where?: condition, - select?: fields, - include?: relations, - rejectOnNotFound?: boolean, - distinct?: field, - orderBy?: order, - cursor?: position, - skip?: number, - take?: number -}) -``` - -- `distinct` — фильтрация по определенному полю; -- `orderBy` — сортировка по определенному полю и в определенном порядке; -- `cursor` — позиция начала списка (как правило, `id` или другое уникальное значение); -- `skip` — количество пропускаемых записей; -- `take` — количество возвращаемых записей (в данном случае может иметь значение `1` или `-1`: во втором случае возвращается последняя запись. - -**Пример** - -```js -async function getLastPostByAuthorId(author_id) { - try { - const post = await prisma.post.findFirst({ - where: { - author_id, - }, - orderBy: { - created_at: 'asc', - }, - take: -1, - }); - return post; - } catch (e) { - onError(e); - } -} -``` - -### findMany - -`findMany` возвращает все записи, соответствующие заданному критерию. - -**Сигнатура** - -``` -findMany({ - where?: condition, - select?: fields, - include?: relations, - rejectOnNotFound?: boolean, - distinct?: field, - orderBy?: order, - cursor?: position, - skip?: number, - take?: number -}) -``` - -**Пример** - -```js -async function getAllPostsByAuthorId(author_id) { - try { - const posts = await prisma.post.findMany({ - where: { - author_id, - }, - orderBy: { - updated_at: 'desc', - }, - }); - return posts; - } catch (e) { - onError(e); - } -} -``` - -### create - -`create` создает новую запись. - -**Сигнатура** - -``` -create({ - data: _data, - select?: fields, - include?: relations -}) -``` - -- `_data` — данные создаваемой записи. - -**Пример** - -```js -async function createUserWithProfile(data) { - const { - email, - password, - firstName, - lastName, - age, - } = data; - try { - const hash = await argon2.hash(password); - const user = await prisma.user.create({ - data: { - email, - hash, - profile: { - create: { - first_name: firstName, - last_name: lastName, - age, - }, - }, - }, - select: { - email: true, - }, - include: { - profile: true, - }, - }); - return user; - } catch (e) { - onError(e); - } -} -``` - -### update - -`update` обновляет существующую запись. - -**Сигнатура** - -``` -update({ - data: _data, - where: condition, - select?: fields, - include?: relations -}) -``` - -**Пример** - -```js -async function updateUserById(id, changes) { - const { email, age } = changes; - try { - const user = await prisma.user.update({ - where: { - id, - }, - data: { - email, - profile: { - update: { - age, - }, - }, - }, - select: { - email: true, - }, - include: { - profile: true, - }, - }); - return user; - } catch (e) { - onError(e); - } -} -``` - -### upsert - -`upsert` обновляет существующую или создает новую запись. - -**Сигнатура** - -``` -upsert({ - create: _data, - update: _data, - where: condition, - select?: fields, - include?: relations -}) -``` - -**Пример** - -```js -async function updateOrCreateUser(data) { - const { userName, email, password } = data; - try { - const hash = await argon2.hash(password); - const user = await prisma.user.create({ - where: { user_name: userName }, - update: { - email, - hash, - }, - create: { - email, - hash, - user_name: userName, - }, - select: { user_name: true, email: true }, - }); - return user; - } catch (e) { - onError(e); - } -} -``` - -### delete - -`delete` удаляет существующую запись по идентификатору или уникальному полю. - -**Сигнатура** - -``` -delete({ - where: condition, - select?: fields, - include?: relations -}) -``` - -**Пример** - -```js -async function removeUserById(id) { - try { - await prisma.user.delete({ - where: { - id, - }, - }); - } catch (e) { - onError(e); - } -} -``` - -### createMany - -`createMany` создает несколько записей с помощью одной транзакции (о транзакциях мы поговорим отдельно). - -**Сигнатура** - -``` -createMany({ - data: _data[], - skipDuplicates?: boolean -}) -``` - -- `_data[]` — данные для создаваемых записей в виде массива; -- `skipDuplicates` — при значении `true` создаются только уникальные записи. - -**Пример** - -```js -// предположим, что `users` - это массив объектов -async function createUsers(users) { - try { - const users = await prisma.user.createMany({ - data: users, - }); - return users; - } catch (e) { - onError(e); - } -} -``` - -### updateMany - -`updateMany` обновляет несколько существующих записей за один раз и возвращает количество (sic) обновленных записей. - -**Сигнатура** - -``` -updateMany({ - data: _data[], - where?: condition -}) -``` - -**Пример** - -```js -async function updateProductsByCategory( - category, - newDiscount -) { - try { - const count = await prisma.product.updateMany({ - where: { - category, - }, - data: { - discount: newDiscount, - }, - }); - return count; - } catch (e) { - onError(e); - } -} -``` - -### deleteMany - -`deleteMany` удаляет несколько записей с помощью одной транзакции и возвращает количество удаленных записей. - -**Сигнатура** - -``` -deleteMany({ - where?: condition -}) -``` - -**Пример** - -```js -async function removeAllPostsByUserId(author_id) { - try { - const count = await prisma.post.deleteMany({ - where: { - author_id, - }, - }); - return count; - } catch (e) { - onError(e); - } -} -``` - -### count - -`count` возвращает количество записей, соответствующих заданному критерию. - -**Сигнатура** - -``` -count({ - where?: condition, - select?: fields, - cursor?: position, - orderBy?: order, - skip?: number, - take?: number -}) -``` - -**Пример** - -```js -async function countUsersWithPublishedPosts() { - try { - const count = await prisma.user.count({ - where: { - post: { - some: { - published: true, - }, - }, - }, - }); - return count; - } catch (e) { - onError(e); - } -} -``` - -### aggregate - -`aggregate` выполняет агрегирование полей. - -**Сигнатура** - -``` -aggregate({ - where?: condition, - select?: fields, - cursor?: position, - orderBy?: order, - skip?: number, - take?: number, - - _count: count, - _avg: avg, - _sum: sum, - _min: min, - _max: max -}) -``` - -- `_count` — возвращает количество совпадающих записей или не null-полей; -- `_avg` — возвращает среднее значение определенного поля; -- `_sum` — возвращает сумму значений определенного поля; -- `_min` — возвращает наименьшее значение определенного поля; -- `_max` — возвращает наибольшее значение определенного поля. - -**Пример** - -```js -async function getAllUsersCountAndMinMaxProfileViews() { - try { - const result = await prisma.user.aggregate({ - _count: { - _all: true, - }, - _max: { - profileViews: true, - }, - _min: { - profileViews: true, - }, - }); - return result; - } catch (e) { - onError(e); - } -} -``` - -### groupBy - -`groupBy` выполняет группировку полей. - -**Сигнатура** - -``` -groupBy({ - by?: by, - having?: having, - - where?: condition, - orderBy?: order, - skip?: number, - take?: number, - - _count: count, - _avg: avg, - _sum: sum, - _min: min, - _max: max -}) -``` - -- `by` — определяет поле или комбинацию полей для группировки записей; -- `having` — позволяет фильтровать группы по агрегируемому значению. - -**Пример** - -В следующем примере мы выполняем группировку по `country` / `city`, где среднее значение `profileViews` превышает `100`, и возвращаем общее количество (`_sum`) `profileViews` для каждой группы. Запрос также возвращает количество всех (`_all`) записей в каждой группе и все записи с не `null` значениями поля `city` в каждой группе: - -```js -async function getUsers() { - try { - const result = await prisma.user.groupBy({ - by: ['country', 'city'], - _count: { - _all: true, - city: true, - }, - _sum: { - profileViews: true, - }, - orderBy: { - country: 'desc', - }, - having: { - profileViews: { - _avg: { - gt: 100, - }, - }, - }, - }); - return result; - } catch (e) { - onError(e); - } -} -``` diff --git a/docs/prisma-orm/filter.md b/docs/prisma-orm/filter.md deleted file mode 100644 index 84e3dd3..0000000 --- a/docs/prisma-orm/filter.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: Фильтры и операторы -description: Фильтры и операторы ---- - -# Фильтры и операторы - -## Фильтры - -`equals` — значение равняется `n` - -```js -const usersWithNameHarry = await prisma.user.findMany({ - where: { - name: { - equals: 'Harry', - }, - }, -}); - -// `equals` может быть опущено -const usersWithNameHarry = await prisma.user.findMany({ - where: { - name: 'Harry', - }, -}); -``` - -`not` — значение не равняется `n`; - -`in` — значение `n` содержится в списке (массиве) - -```js -const usersWithNameAliceOrBob = await prisma.user.findMany({ - where: { - user_name: { - // ! - in: ['Alice', 'Bob'], - }, - }, -}); -``` - -`notIn` — `n` не содержится в списке; - -`lt` — `n` меньше `x` - -```js -const notPopularPosts = await prisma.post.findMany({ - where: { - likeCount: { - lt: 100, - }, - }, -}); -``` - -`lte` — `n` меньше или равно `x`; - -`gt` — `n` больше `x`; - -`gte` — `n` больше или равно `x`; - -`contains` — `n` содержит `x` - -```js -const admins = await prisma.user.findMany({ - where: { - email: { - contains: 'admin', - }, - }, -}); -``` - -`startsWith` — `n` начинается с `x` - -```js -const usersWithNameStartsWithA = await prisma.user.findMany( - { - where: { - user_name: { - startsWith: 'A', - }, - }, - } -); -``` - -`endsWith` — `n` заканчивается `x`. - -## Операторы - -`AND` — все условия должны возвращать `true` - -```js -const notPublishedPostsAboutTypeScript = await prisma.post.findMany( - { - where: { - AND: [ - { - title: { - contains: 'TypeScript', - }, - }, - { - published: false, - }, - ], - }, - } -); -``` - -Обратите внимание: оператор указывается до названия поля (снаружи поля), а фильтр после (внутри). - -`OR` — хотя бы одно условие должно возвращать `true`; - -`NOT` — все условия должны возвращать `false`. - -## Фильтры для связанных записей - -`some` — возвращает все связанные записи, соответствующие одному или более критерию фильтрации - -```js -const usersWithPostsAboutTypeScript = await prisma.user.findMany( - { - where: { - posts: { - some: { - title: { - contains: 'TypeScript', - }, - }, - }, - }, - } -); -``` - -`every` — возвращает все связанные записи, соответствующие всем критериям; - -`none` — возвращает все связанные записи, не соответствующие ни одному критерию; - -`is` — возвращает все связанные записи, соответствующие критерию; - -`notIs` — возвращает все связанные записи, не соответствующие критерию. diff --git a/docs/prisma-orm/index.md b/docs/prisma-orm/index.md deleted file mode 100644 index 9faaf3a..0000000 --- a/docs/prisma-orm/index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Prisma -description: Prisma — это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для Node.js и TypeScript ---- - -# Prisma - -![Prisma](logo-prisma.svg) - -**Prisma** — это современное (продвинутое) объектно-реляционное отображение (Object-Relational Mapping, ORM) для [Node.js](https://nodejsdev.ru/) и [TypeScript](https://scriptdev.ru/). - -Проще говоря, Prisma — это инструмент, позволяющий работать с реляционными (PostgreSQL, MySQL, SQL Server, SQLite) и нереляционной (MongoDB) базами данных с помощью JavaScript или TypeScript без использования SQL (хотя такая возможность имеется). - -- [Инициализация проекта](init.md) -- [Интерфейс командной строки](cli.md) -- [Схема](schema.md) -- [Клиент](client.md) -- [Настройки](setup.md) -- [Вложенные запросы](nested.md) -- [Фильтры и операторы](filter.md) -- [Методы клиента](method.md) -- [Транзакции](transactions.md) diff --git a/docs/prisma-orm/init.md b/docs/prisma-orm/init.md deleted file mode 100644 index 00f6730..0000000 --- a/docs/prisma-orm/init.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Инициализация проекта -description: Инициализация проекта для Prisma ORM ---- - -# Инициализация проекта - -Создаем директорию, переходим в нее и инициализируем Node.js-проект: - -```sh -mkdir prisma-test -cd prisma-test - -yarn init -yp -# or -npm init -y -``` - -Устанавливаем Prisma в качестве зависимости для разработки: - -```sh -yarn add -D prisma - -# or -npm i -D prisma -``` - -Инициализируем проект Prisma: - -```sh -npx prisma init -``` - -Это приводит к генерации файлов `prisma/schema.prisma` и `.env`. - -В файле `.env` содержится переменная `DATABASE_URL`, значением которой является путь к (адрес) БД. Файл `schema.prisma` мы рассмотрим позже. diff --git a/docs/prisma-orm/logo-prisma.svg b/docs/prisma-orm/logo-prisma.svg deleted file mode 100644 index 4ae931f..0000000 --- a/docs/prisma-orm/logo-prisma.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/docs/prisma-orm/method.md b/docs/prisma-orm/method.md deleted file mode 100644 index 64462a4..0000000 --- a/docs/prisma-orm/method.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Методы клиента -description: Методы клиента ---- - -# Методы клиента - -`$disconnect` — закрывает соединение с БД, которое было установлено после вызова метода `$connect` (данный метод чаще всего не требуется вызывать явно), и останавливает движок запросов (query engine) Prisma - -```js -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function seedDb() { - try { - await prisma.model.create(data); - } catch (e) { - onError(e); - } finally { - // ! - await prisma.$disconnect(); - } -} -``` - -`$use` — добавляет посредника (`middleware`) - -```js -prisma.$use(async (params, next) => { - console.log('Это посредник'); - // работаем с `params` - return next(params); -}); -``` - -`next` — представляет "следующий уровень" в стеке посредников. Таким уровнем может быть следующий посредник или движок запросов Prisma; - -`params` — объект со следующими свойствами: - -- `action` — тип запроса, например, `create` или `findMany`; -- `args` — аргументы, переданные в запрос, например, `where` или `data`; -- `model` — модель, например, `User` или `Post`; -- `runInTransaction` — возвращает `true`, если запрос был запущен в контексте транзакции; - -методы `$queryRaw`, `$executeRaw` и `$runCommandRaw` предназначены для работы с SQL. Почитать о них можно [здесь](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access); - -`$transaction` — выполняет запросы в контексте транзакции. - -Подробнее о клиенте можно почитать [здесь](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). diff --git a/docs/prisma-orm/nested.md b/docs/prisma-orm/nested.md deleted file mode 100644 index d1ec311..0000000 --- a/docs/prisma-orm/nested.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Вложенные запросы -description: Вложенные запросы ---- - -# Вложенные запросы - -`create: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — добавляет новую связанную запись или набор записей в родительскую запись. `create` доступен при создании (`create`) новой родительской записи или обновлении (`update`) существующей родительской записи - -```js -const user = await prisma.user.create({ - data: { - email, - profile: { - // вложенный запрос - create: { - first_name, - last_name, - }, - }, - }, -}); -``` - -`createMany: [{ data1 }, { data2 }, ...{ dataN }]` — добавляет набор новых связанных записей в родительскую запись. `createMany` доступен при создании (`create`) новой родительской записи или обновлении (`update`) существующей родительской записи - -```js -const userWithPosts = await prisma.user.create({ - data: { - email, - posts: { - // ! - createMany: { - data: posts, - }, - }, - }, -}); -``` - -`update: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — обновляет одну или более связанных записей - -```js -const user = await prisma.user.update({ - where: { email }, - data: { - profile: { - // ! - update: { age }, - }, - }, -}); -``` - -`updateMany: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — обновляет массив связанных записей. Поддерживается фильтрация - -```js -const result = await prisma.user.update({ - where: { id }, - data: { - posts: { - // ! - updateMany: { - where: { - published: false, - }, - data: { - like_count: 0, - }, - }, - }, - }, -}); -``` - -`upsert: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — обновляет существующую связанную запись или создает новую - -```js -const user = await prisma.user.update({ - where: { email }, - data: { - profile: { - // ! - upsert: { - create: { age }, - update: { age }, - }, - }, - }, -}); -``` - -`delete: boolean | { data } | [{ data1 }, { data2 }, ...{ dataN }]` — удаляет связанную запись. Родительская запись при этом не удаляется - -```js -const user = await prisma.user.update({ - where: { email }, - data: { - profile: { - delete: true, - }, - }, -}); -``` - -`deleteMany: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — удаляет связанные записи. Поддерживается фильтрация - -```js -const user = await prisma.user.update({ - where: { id }, - data: { - age, - posts: { - // ! - deleteMany: {}, - }, - }, -}); -``` - -`set: { data } | [{ data1 }, { data2 }, ...{ dataN }]` — перезаписывает значение связанной записи - -```js -const userWithPosts = await prisma.user.update({ - where: { email }, - data: { - posts: { - // ! - set: newPosts, - }, - }, -}); -``` - -`connect` — подключает запись к существующей связанной записи по идентификатору или уникальному полю - -```js -const user = await prisma.post.create({ - data: { - title, - content, - author: { - connect: { email }, - }, - }, -}); -``` - -`connectOrCreate` — подключает запись к существующей связанной записи по идентификатору или уникальному полю либо создает связанную запись при отсутствии таковой; - -`disconnect` — отключает родительскую запись от связанной без удаления последней. `disconnect` доступен только если отношение является опциональным. diff --git a/docs/prisma-orm/schema.md b/docs/prisma-orm/schema.md deleted file mode 100644 index 922f3a7..0000000 --- a/docs/prisma-orm/schema.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: Схема -description: Схема ---- - -# Схема - -В файле `schema.prisma` мы видим такие строки: - -``` -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} -``` - -`datasource` — источник данных: - -- `provider` — название провайдера для доступа к БД: `sqlite`, `postgresql`, `mysql`, `sqlserver` или `mongodb` (по умолчанию — `postgresql`); -- `url` — адрес БД (по умолчанию — значение переменной `DATABASE_URL`); -- `shadowDatabaseUrl` — адрес "теневой" БД (для БД, предоставляемых облачными провайдерами): используется для миграций для разработки (`prisma migrate dev`); - -`generator` — генератор клиента на основе схемы: - -- `provider` — провайдер генератора (единственным доступным на сегодняшний день провайдером является `prisma-client-js`); -- `binaryTargets` — определяет операционную систему для клиента Prisma. Значением по умолчанию является `native`, но иногда это приходится указывать явно, например, при использовании клиента в Docker-контейнере (в этом случае также приходится явно выполнять `prisma generate`) - -``` -generator client { - provider = "prisma-client-js" - binaryTargets = ["native"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - shadowDatabaseUrl = env("SHADOW_DATABASE_URL") -} -``` - -Для работы со схемой удобно пользоваться расширением [Prisma для VSCode](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma). Соответствующий раздел в файле `settings.json` должен выглядеть так: - -```json -"[prisma]": { - "editor.defaultFormatter": "Prisma.prisma" -} -``` - -Определим в схеме модели для пользователя (`User`) и поста (`Post`): - -``` -model User { - id String @id @default(uuid()) @db.Uuid - email String @unique - hash String @map("password_hash") - first_name String? - last_name String? - age Int? - role Role @default(USER) - posts Post[] - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - - @@map("users") -} - -model Post { - id String @id @default(uuid()) - title String - content String - published Boolean - author_id String - author User @relation(fields: [author_id], references: [id]) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - - @@map("posts") -} - -enum Role { - USER - ADMIN -} -``` - -Вот что мы здесь видим: - -- `id`, `email`, `hash` etc. — названия полей (колонок таблицы); -- `@map` привязывает поле схемы (`hash`) к указанной колонке таблицы (`password_hash`). `@map` не меняет название колонки в БД и поля в генерируемом клиенте. Для MongoDB использование `@map` для `@id` является обязательным: `id String @default(auto()) @map("_id") @db.ObjectId`; -- `String`, `Int`, `DateTime` etc. — типы данных (см. ниже); -- `@db.Uuid` — тип данных, специфичный для одной или нескольких БД (в данном случае PostgreSQL); -- модификатор `?` после названия типа означает, что данное поле является опциональным (необязательным, может иметь значение `NULL`); -- модификатор `[]` после названия типа означает, что значением данного поля является список (массив). Такое поле не может быть опциональным; -- префикс `@` означает атрибут поля, а префикс `@@` — атрибут блока (модели, таблицы). Некоторые атрибуты принимают параметры; -- атрибут `@id` означает, что данное поле является первичным (основным) ключом таблицы (`PRIMARY KEY`) (идентификатор модели). Такое поле не может быть опциональным; -- атрибут `@default` присваивает полю указанное значение по умолчанию (при отсутствии значения поля) (`DEFAULT`). Дефолтными могут быть статические значения (`42`, `hi`) или значения, генерируемые функциями `autoincrement`, `dbgenerated`, `cuid`, `uuid` и `now` (функции атрибутов; см. ниже); -- атрибут `@unique` означает, что значение поля должно быть уникальным в пределах таблицы (`UNIQUE`). Таблица должна иметь хотя бы одно поле `@id` или `@unique`; -- атрибут `@relation` указывает на существование отношений между таблицами. В данном случае между таблицами `users` и `posts` существуют отношения один-ко-многим (one-to-many, 1-n) — у одного пользователя может быть несколько постов (`FOREIGN KEY` / `REFERENCES`) (об отношениях мы поговорим отдельно); -- атрибут `@updatedAt` обновляет поле текущими датой и временем при любой модификации записи; - у нас имеется перечисление (`enum`), значения которого используются в качестве значений поля `role` модели `User` (значением по умолчанию является `USER`); -- атрибут `@@map` привязывает название модели к названию таблицы в БД. `@@map` не меняет название таблицы в БД и модели в генерируемом клиенте. - -## Типы данных - -Допустимыми в названиях полей являются следующие символы: `[A-Za-z][a-za-z0-9_]*`. - -- `String` — строка переменной длины (для PostgreSQL — это тип text); -- `Boolean` — логическое значение: `true` или `false` (boolean); -- `Int` — целое число (integer); -- `BigInt` — BigInt (integer); -- `Float` — число с плавающей точкой (запятой) (double precision); -- `Decimal` (decimal); -- `DateTime` — дата и время в формате ISO 8601; -- `Json` — объект в формате JSON (jsonb); -- `Bytes` (bytea). - -Атрибут `@db` позволяет использовать типы данных, специфичные для одной или нескольких БД. - -## Атрибуты - -Кроме упомянутых выше, в схеме можно использовать следующие атрибуты: - -- `@@id` — определяет составной (composite) первичный ключ таблицы, например, `@@id[title, author]` (в данном случае соответствующее поле будет называться `title_author` — это можно изменить); -- `@@unique` — определяет составное ограничение уникальности (unique constraint) для указанных полей (такие поля не могут быть опциональными), например, `@@unique([title, author])`; -- `@@index` — определяет индекс в БД (`INDEX`), например, `@@index([title, author])`; -- `@ignore`, `@@ignore` — используется для обозначения невалидных полей и моделей, соответственно. - -## Функции атрибутов - -- `auto` — представляет дефолтные значения, генерируемые БД (только для MongoDB); -- `autoincrement` — генерирует последовательные целые числа (`SERIAL` в PostgreSQL, не поддерживается MongoDB); -- `cuid` — генерирует глобальный уникальный идентификатор на основе спецификации `cuid`; -- `uuid` — генерирует глобальный уникальный идентификатор на основе спецификации `UUID`; -- `now` — возвращает текущую отметку времени (`timestamp`) (`CURRENT_TIMESTAMP` в PostgreSQL); -- `dbgenerated` — представляет дефолтные значения, которые не могут быть выражены в схеме (например, `random()`). - -Подробнее о схеме можно почитать [здесь](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). - -## Отношения - -Атрибут `@relation` указывает на существование отношений между моделями (таблицами). Он принимает следующие параметры: - -- `name?: string` — название отношения; -- `fields?: [field1, field2, ...fieldN]` — список полей текущей модели (в нашем случае это `[author_id]` модели `Post`); обратите внимание: само поле определяется отдельно); -- `references: [field1, field2, ...fieldN]` — список полей другой модели (стороны отношений) (в нашем случае это `[id]` модели `User`). - -В приведенной выше схеме полями, указывающими на существование отношений между моделями `User` и `Post`, являются поля `posts` и `author`. Эти поля существуют только на уровне Prisma, в БД они не создаются. Скалярное поле `author_id` также существует только на уровне Prisma — это внешний ключ (`FOREIGN KEY`), соединяющий `Post` с `User`. - -Как известно, существует 3 вида отношений: - -- один-к-одному (one-to-one, 1-1); -- один-ко-многим (one-to-many, 1-n); -- многие-ко-многим (many-to-many, m-n). - -Атрибут `@relation` является обязательным только для отношений 1-1 и 1-n. - -Предположим, что в нашей схеме имеются такие модели: - -``` -model User { - id Int @id @default(autoincrement()) - posts Post[] - profile Profile? -} - -model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int -} - -model Post { - id Int @id @default(autoincrement()) - author User @relation(fields: [authorId], references: [id]) - authorId Int - categories Category[] -} - -model Category { - id Int @id @default(autoincrement()) - posts Post[] -} -``` - -Вот что мы здесь видим: - -- между моделями `User` и `Profile` существуют отношения 1-1 — у одного пользователя может быть только один профиль; -- между моделями `User` и `Post` существуют отношения 1-n — у одного пользователя может быть несколько постов; -- между моделями `Post` и `Category` существуют отношения m-n — один пост может принадлежать к нескольким категориям, в одну категорию может входить несколько постов. - -Подробнее об отношениях можно почитать [здесь](https://www.prisma.io/docs/concepts/components/prisma-schema/relations). diff --git a/docs/prisma-orm/setup.md b/docs/prisma-orm/setup.md deleted file mode 100644 index 2df4877..0000000 --- a/docs/prisma-orm/setup.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Настройки -description: Настройки ---- - -# Настройки - -## select - -`select` определяет, какие поля включаются в возвращаемый объект. - -```js -const user = await prisma.user.findUnique({ - where: { email }, - select: { - id: true, - email: true, - first_name: true, - last_name: true, - age: true, - }, -}); - -// or -const usersWithPosts = await prisma.user.findMany({ - select: { - id: true, - email: true, - posts: { - select: { - id: true, - title: true, - content: true, - author_id: true, - created_at: true, - }, - }, - }, -}); - -// or -const usersWithPostsAndComments = await prisma.user.findMany( - { - select: { - id: true, - email: true, - posts: { - include: { - comments: true, - }, - }, - }, - } -); -``` - -## include - -`include` определяет, какие отношения (связанные записи) включаются в возвращаемый объект. - -```js -const userWithPostsAndComments = await prisma.user.findUnique( - { - where: { email }, - include: { - posts: true, - comments: true, - }, - } -); -``` - -## where - -`where` определяет один или более фильтр (о фильтрах мы поговорим отдельно), применяемый к свойствам записи или связанных записей: - -```js -const admins = await prisma.user.findMany({ - where: { - email: { - contains: 'admin', - }, - }, -}); -``` - -## orderBy - -`orderBy` определяет поля и порядок сортировки. Возможными значениями `orderBy` являются `asc` и `desc`. - -```js -const usersByPostCount = await prisma.user.findMany({ - orderBy: { - posts: { - count: 'desc', - }, - }, -}); -``` - -## distinct - -`distinct` определяет поля, которые должны быть уникальными в возвращаемом объекте. - -```js -const distinctCities = await prisma.user.findMany({ - select: { - city: true, - country: true, - }, - distinct: ['city'], -}); -``` diff --git a/docs/prisma-orm/transactions.md b/docs/prisma-orm/transactions.md deleted file mode 100644 index e61f0ae..0000000 --- a/docs/prisma-orm/transactions.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: Транзакции -description: Транзакция — это последовательность операций чтения/записи, которые обрабатываются как единое целое, т. е. либо все операции завершаются успешно, либо все операции отклоняются с ошибкой ---- - -# Транзакции - -**Транзакция** — это последовательность операций чтения/записи, которые обрабатываются как единое целое, т. е. либо все операции завершаются успешно, либо все операции отклоняются с ошибкой. - -Prisma позволяет использовать транзакции тремя способами: - -_вложенные запросы_ (см. выше): операции с родительскими и связанными записями выполняются в контексте одной транзакции - -```js -const newUserWithProfile = await prisma.user.create({ - data: { - email, - profile: { - // ! - create: { - first_name, - last_name, - }, - }, - }, -}); -``` - -_пакетированные/массовые_ (batch/bulk) транзакции: выполнение нескольких операций за один раз с помощью таких запросов, как `createMany`, `updateMany` и `deleteMany` - -```js -const removedUser = await prisma.user.delete({ - where: { - email, - }, -}); - -// ! -await prisma.post.deleteMany({ - where: { - author_id: removedUser.id, - }, -}); -``` - -_вызов метода \$transaction_. - -## \$transaction - -Интерфейс `$transaction` может быть использован в двух формах: - -- `$transaction([ query1, query2, ...queryN ])` — принимает массив последовательно выполняемых запросов; -- `$transaction(fn)` — принимает функцию, которая может включать запросы и другой код. - -Пример транзакции, возвращающей посты, в заголовке которых встречается слово TypeScript и общее количество постов: - -```js -const [ - postsAboutTypeScript, - totalPostCount, -] = await prisma.$transaction([ - prisma.post.findMany({ - where: { title: { contains: 'TypeScript' } }, - }), - prisma.post.count(), -]); -``` - -В `$transaction` допускается использование SQL: - -```js -const [userNames, updatedUser] = await prisma.$transaction([ - prisma.$queryRaw`SELECT 'user_name' FROM users`, - prisma.$executeRaw`UPDATE users SET user_name = 'Harry' WHERE id = 42`, -]); -``` - -## Интерактивные транзакции - -Интерактивные транзакции предоставляют разработчикам больший контроль над выполняемыми в контексте транзакции операциями. В данный момент они имеют статус экспериментальной возможности, которую можно включить следующим образом: - -``` -generator client { - provider = "prisma-client-js" - previewFeatures = ["interactiveTransactions"] -} -``` - -Рассмотрим пример совершения платежа. - -Предположим, что у Alice и Bob имеется по 100$ на счетах (account), и Alice хочет отправить Bob свои 100$. - -```js -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); - -async function transfer(from, to, amount) { - try { - await prisma.$transaction(async (prisma) => { - // 1. Уменьшаем баланс отправителя - const sender = await prisma.account.update({ - data: { - balance: { - decrement: amount, - }, - }, - where: { - email: from, - }, - }); - - // 2. Проверяем, что баланс отправителя после уменьшения >= 0 - if (sender.balance < 0) { - throw new Error( - `${from} имеет недостаточно средств для отправки ${amount}` - ); - } - - // 3. Увеличиваем баланс получателя - const recipient = await prisma.account.update({ - data: { - balance: { - increment: amount, - }, - }, - where: { - email: to, - }, - }); - - return recipient; - }); - } catch (e) { - // обрабатываем ошибку - } -} - -async function main() { - // эта транзакция разрешится - await transfer('alice@mail.com', 'bob@mail.com', 100); - // а эта провалится - await transfer('alice@mail.com', 'bob@mail.com', 100); -} - -main().finally(() => { - prisma.$disconnect(); -}); -``` - -Подробнее о транзакциях можно почитать [здесь](https://www.prisma.io/docs/concepts/components/prisma-client/transactions). diff --git a/docs/pwa.svg b/docs/pwa.svg new file mode 100644 index 0000000..fa482a4 --- /dev/null +++ b/docs/pwa.svg @@ -0,0 +1 @@ +pwa \ No newline at end of file diff --git a/docs/rn.svg b/docs/rn.svg new file mode 100644 index 0000000..6cd3635 --- /dev/null +++ b/docs/rn.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/solid.svg b/docs/solid.svg new file mode 100644 index 0000000..025aa30 --- /dev/null +++ b/docs/solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 187a97d..ddb2fe0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: Typescript site_url: https://scriptdev.ru/ -site_description: Справочник Typescript-разработчика с примерами кода +site_description: Справочник Typescript с примерами кода site_author: scriptdev.ru -copyright: (C) 2021-2023, Справочник разработчика Typescript с примерами кода +copyright: (☭) Народное достояние с 2021 года repo_name: 'Справочник на Github' repo_url: 'https://github.com/xsltdev/scriptdev.ru' @@ -10,59 +10,83 @@ repo_url: 'https://github.com/xsltdev/scriptdev.ru' theme: name: material custom_dir: overrides - # Don't include MkDocs' JavaScript - include_search_page: false - search_index_only: true - palette: + - media: '(prefers-color-scheme)' + primary: indigo + accent: indigo + toggle: + icon: material/brightness-auto + name: Переключиться на светлую тему - media: '(prefers-color-scheme: light)' scheme: default - primary: blue - accent: blue + primary: indigo + accent: indigo toggle: icon: material/toggle-switch-off-outline - name: Тёмная тема + name: Переключиться на тёмную тему - media: '(prefers-color-scheme: dark)' scheme: slate - primary: blue - accent: blue + primary: indigo + accent: indigo toggle: icon: material/toggle-switch - name: Светлая тема + name: Переключиться на системную тему language: ru features: + - navigation.prune - navigation.tabs - navigation.tracking - - navigation.instant + - navigation.top + - navigation.footer + - navigation.indexes + - navigation.path + - content.action.edit + - content.code.copy favicon: ts.svg logo: ts.svg extra: generator: false social: - - icon: fontawesome/brands/telegram - link: https://t.me/nodejsdev_ru - name: Группа в Telegram - -#google_analytics: -# - UA-34215161-1 -# - auto + - icon: fontawesome/brands/github + link: https://github.com/xsltdev + name: GitHub + - icon: fontawesome/regular/heart + link: https://boosty.to/bndby + name: Boosty + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/bndby/ + name: LinkedIn markdown_extensions: - admonition - - codehilite: - guess_lang: false - linenums: false - toc: permalink: true - pymdownx.extra + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde - pymdownx.details - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg - - pymdownx.tabbed - - pymdownx.superfences - - pymdownx.keys + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + pygments_lang_class: true + linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.arithmatex: + generic: true + - pymdownx.smartsymbols plugins: - search: @@ -70,8 +94,20 @@ plugins: - ru - en - awesome-pages - - section-index - social + - minify: + minify_html: true + htmlmin_opts: + remove_comments: true + cache_safe: true + - glightbox: + auto_caption: true + skip_classes: + - nolightbox extra_css: - - '/assets/stylesheets/styles.css' + - '/assets/stylesheets/styles.css?4' + +extra_javascript: + - path: '/assets/javascripts/xdev-browser.js' + type: module diff --git a/overrides/assets/javascripts/xdev-browser.js b/overrides/assets/javascripts/xdev-browser.js new file mode 100644 index 0000000..c9220b1 --- /dev/null +++ b/overrides/assets/javascripts/xdev-browser.js @@ -0,0 +1,23 @@ +const BROWSER = { + chrome: + '', + firefox: + '', + edge: + '', +}; + +class XDevBrowser extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + const browser = + this.getAttribute('browser') ?? 'chrome'; + const shadow = this.attachShadow({ mode: 'open' }); + shadow.innerHTML = `${BROWSER[browser]}`; + } +} + +customElements.define('xdev-browser', XDevBrowser); diff --git a/overrides/assets/stylesheets/styles.css b/overrides/assets/stylesheets/styles.css index 13e7396..c8fe470 100644 --- a/overrides/assets/stylesheets/styles.css +++ b/overrides/assets/stylesheets/styles.css @@ -102,14 +102,6 @@ h3 { opacity: 0.85; } -/* - CODE BLOCK -*/ -.md-typeset .codehilite, -.md-typeset .highlight { - border: 1px solid rgba(236, 236, 236, 1); -} - /* Google button */ .gsc-search-button-v2 svg { fill: #ffffff; @@ -155,3 +147,14 @@ h3 { .layout .cell h3 .headerlink { display: none; } + +.md-tabs__list li:last-child { + position: absolute; + right: 0; +} +.md-tabs__list li:last-child a { + font-weight: 700; + padding-left: 24px; + background: url('/https/github.com/heart.svg') left center / 16px 16px + no-repeat; +} diff --git a/overrides/partials/itunion.svg b/overrides/partials/itunion.svg new file mode 100644 index 0000000..2e68db7 --- /dev/null +++ b/overrides/partials/itunion.svg @@ -0,0 +1,46 @@ + + + + + + diff --git a/overrides/partials/social.html b/overrides/partials/social.html index a195459..382b877 100644 --- a/overrides/partials/social.html +++ b/overrides/partials/social.html @@ -1,13 +1,16 @@ - -{% if config.extra.social %} +
- {% for social in config.extra.social %} {% set title = - social.name %} {% if not title and "//" in social.link - %} {% set _,url = social.link.split("//") %} {% set - title = url.split("/")[0] %} {% endif %} - {% include ".icons/" ~ social.icon ~ ".svg" %} + {% include "./partials/itunion.svg" %} - {% endfor %} -
-{% endif %} + {% for social in config.extra.social %} - + + {% set title = social.name %} + {% if not title and "//" in social.link %} + {% set _, url = social.link.split("//") %} + {% set title = url.split("/")[0] %} + {% endif %} + + {% include ".icons/" ~ social.icon ~ ".svg" %} + + {% endfor %} + diff --git a/requirements.txt b/requirements.txt index 5c85ebf..c50982d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pymdown-extensions mkdocs-material mkdocs-awesome-pages-plugin mkdocs-minify-plugin -mkdocs-section-index +mkdocs-glightbox PyYAML six tornado diff --git a/runtime.txt b/runtime.txt index 0b2eb36..2c07333 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -3.7.2 +3.11