From 6d59da76f2d5c8dddaaaedbb2002e650df6e5844 Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Sat, 23 Sep 2023 00:44:02 +0300 Subject: [PATCH] Section V finished --- .../01.md | 0 .../02.md | 0 .../03.md | 0 .../04.md | 0 .../05.md | 0 .../06.md | 0 .../07.md | 0 .../06-Section V. SDKs & UI Libraries/08.md | 90 +++++++++++ .../06-Section V. SDKs & UI Libraries/09.md | 7 + .../08.md | 1 - .../09.md | 1 - .../08.md | 1 - .../09.md | 1 - .../01.md | 0 .../02.md | 0 .../03.md | 2 +- .../04.md | 0 .../05.md | 0 .../06.md | 0 .../07.md | 0 .../06-Раздел V. SDK и UI-библиотеки/08.md | 90 +++++++++++ .../06-Раздел V. SDK и UI-библиотеки/09.md | 7 + .../06-Раздел V. SDK и UI-библиотеки/08.md | 152 ------------------ .../06-Раздел V. SDK и UI-библиотеки/09.md | 69 -------- .../06-Раздел V. SDK и UI-библиотеки/10.md | 9 -- 25 files changed, 195 insertions(+), 235 deletions(-) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/01.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/02.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/03.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/04.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/05.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/06.md (100%) rename src/en/clean-copy/{06-[Work in Progress] Section V. SDKs & UI Libraries => 06-Section V. SDKs & UI Libraries}/07.md (100%) create mode 100644 src/en/clean-copy/06-Section V. SDKs & UI Libraries/08.md create mode 100644 src/en/clean-copy/06-Section V. SDKs & UI Libraries/09.md delete mode 100644 src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/08.md delete mode 100644 src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/09.md delete mode 100644 src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/08.md delete mode 100644 src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/09.md rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/01.md (100%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/02.md (100%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/03.md (99%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/04.md (100%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/05.md (100%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/06.md (100%) rename src/ru/clean-copy/{06-[В разработке] Раздел V. SDK и UI-библиотеки => 06-Раздел V. SDK и UI-библиотеки}/07.md (100%) create mode 100644 src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/08.md create mode 100644 src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/09.md delete mode 100644 src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/08.md delete mode 100644 src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/09.md delete mode 100644 src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/10.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/01.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/01.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/01.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/01.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/02.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/02.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/02.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/02.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/03.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/03.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/03.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/03.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/04.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/04.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/04.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/04.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/05.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/05.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/05.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/05.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/06.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/06.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/06.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/06.md diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/07.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/07.md similarity index 100% rename from src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/07.md rename to src/en/clean-copy/06-Section V. SDKs & UI Libraries/07.md diff --git a/src/en/clean-copy/06-Section V. SDKs & UI Libraries/08.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/08.md new file mode 100644 index 0000000..b4e5d27 --- /dev/null +++ b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/08.md @@ -0,0 +1,90 @@ +### [Computed Properties][sdk-computed-properties] + +Let's revisit one of the problems we outlined in the “[Problems of Introducing UI Components](#sdk-ui-components)” chapter: the existence of multiple inheritance lines complicates customizing UI components as it implies that component properties could be inherited from any such line. + +For example, imagine we have a button that can borrow its `iconUrl` property from two sources — from data [in our case, from offer data originated in the offer search results] and the component's options: + +```typescript +class Button { + static DEFAULT_OPTIONS = { + … + iconUrl: + } + + constructor (data, options) { + this.data = data; + // Overriding default options + // with custom values + this.options = extend( + Button.DEFAULT_OPTIONS, + options + ) + } + + render() { + … + this.iconElement.src = + this.data.iconUrl || + this.options.iconUrl + } +} +``` + +It is also plausible to suggest that the `iconUrl` property in both hierarchies is inherited from some parent entities: + * The default option values could be defined in the base class which `Button` extends. + * The data for the button could be hierarchical (for example, if we decide to group offers in the same coffee shop chain, and the icon is to be taken from the parent group). + * To facilitate customizing the visual style of the components, we could allow overriding icons in all SDK buttons. + +In this situation, a question of priorities arises: if a property is defined in several hierarchies (let's say, in the offer data and in the default options), how should the priorities be set to select one of them? + +The straightforward approach to tackling this issue is to prohibit any inheritance and force developers to explicitly set the properties they need. In our example, it would mean that developers will need to write something like this: + +```typescript +const button = new Button(data); +if (data.createOrderButtonIconUrl) { + button.view.iconUrl = + data.createOrderButtonIconUrl; +} else if (data.parentCategory.iconUrl) { + button.view.iconUrl = + data.parentCategory.iconUrl; +} +``` + +The main advantage of this approach is obvious: developers implement the logic they need themselves. The disadvantages are also apparent: first, developers will need to write excessive and often copy-pasted code (“boilerplate”); second, they will soon become confused about which rules are in place and why. + +A slightly more complex solution is allowing inheritance but rigidly fixing priorities (let's say the value set in the component's options always takes precedence over the one set in the data, and they both are more important than any inherited value). However, for any complex API, the result will be the same: if developers need a different order of resolving priorities, they will write code similar to the one above. + +An alternative approach is to expose the possibility of defining rules for how exactly the icon is resolved for a specific button, either declaratively or imperatively: + +```json +// The declarative approach: the rules +// described in some data format +{ + "button.checkout.iconUrl": "@data.iconUrl" +} +``` + +```typescript +// The imperative approach: the value +// is calculated by the provided function +api.options.addRule( + 'button.checkout.iconUrl', + (data, options) => data.iconUrl +) +``` + +The most coherent implementation of this approach is the CSS technology.[ref CSS](https://www.w3.org/Style/CSS/) We are not actually proposing using a full CSS rule engine in component libraries (because of its overwhelming complexity and excessiveness for most cases), but we are cautiously drawing the reader's attention to the fact that supporting some subset of CSS-like rules could *significantly* simplify the task of customizing UI components. + +#### Calculated Values + +It is important not only to provide a mechanism for setting rules to determine how values are resolved but also to allow *obtaining* the value that was actually used. To achieve this, we need to distinguish between the concept of a set value and a computed value: + +```typescript +// Set a value as a percentage +button.view.width = '100%'; +// Retrieve the actual applied value +// in pixels +button.view.computedStyle.width; +``` + +It is also a good practice to provide an event for changes in the computed value of such a calculated property. \ No newline at end of file diff --git a/src/en/clean-copy/06-Section V. SDKs & UI Libraries/09.md b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/09.md new file mode 100644 index 0000000..3fb95e0 --- /dev/null +++ b/src/en/clean-copy/06-Section V. SDKs & UI Libraries/09.md @@ -0,0 +1,7 @@ +### [Conclusion][sdk-conclusion] + +In the previous eight chapters, we aimed to convey two important observations: + * Developing a high-quality UI library is a very complex engineering task. + * This task cannot be mechanically reduced to auto-generating SDKs based on a specification or data model. + +Looking back at what was written, we cannot confidently claim that we found the best examples and the clearest wording for such a complex subject area. However, we hope that we have helped make the reader's life and the lives of their users a bit easier. \ No newline at end of file diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/08.md b/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/08.md deleted file mode 100644 index 19382fd..0000000 --- a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/08.md +++ /dev/null @@ -1 +0,0 @@ -### Computed Properties \ No newline at end of file diff --git a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/09.md b/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/09.md deleted file mode 100644 index 52f697d..0000000 --- a/src/en/clean-copy/06-[Work in Progress] Section V. SDKs & UI Libraries/09.md +++ /dev/null @@ -1 +0,0 @@ -### Conclusion \ No newline at end of file diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/08.md b/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/08.md deleted file mode 100644 index 1912188..0000000 --- a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/08.md +++ /dev/null @@ -1 +0,0 @@ -### Вычисляемые свойства \ No newline at end of file diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/09.md b/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/09.md deleted file mode 100644 index 07d23e8..0000000 --- a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/09.md +++ /dev/null @@ -1 +0,0 @@ -### В заключение \ No newline at end of file diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/01.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/01.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/01.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/01.md diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/02.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/02.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/02.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/02.md diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/03.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/03.md similarity index 99% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/03.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/03.md index a9e3cfa..a31f040 100644 --- a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/03.md +++ b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/03.md @@ -73,7 +73,7 @@ let searchBox = new SearchBox({ }); return res; } -}) +}); ``` *Формально* этот подход корректен и никаких рекомендаций не нарушает. Но с точки зрения связности кода, его читабельности — это полная катастрофа, поскольку следующий разработчик, которого попросят заменить иконку *кнопке*, очень вряд ли пойдёт читать код *функции поиска предложений*. diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/04.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/04.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/04.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/04.md diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/05.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/05.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/05.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/05.md diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/06.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/06.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/06.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/06.md diff --git a/src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/07.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/07.md similarity index 100% rename from src/ru/clean-copy/06-[В разработке] Раздел V. SDK и UI-библиотеки/07.md rename to src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/07.md diff --git a/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/08.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/08.md new file mode 100644 index 0000000..c4f2319 --- /dev/null +++ b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/08.md @@ -0,0 +1,90 @@ +### [Вычисляемые свойства][sdk-computed-properties] + +Вернёмся к одной из проблем, описанных в главе «[Проблемы встраивания UI-компонентов](#sdk-ui-components)»: наличие множественных линий наследования усложняет кастомизация компонентов, поскольку подразумевает, что они могут наследовать важные свойства по любой из вертикалей. + +Пусть у нас имеется кнопка, которая получает одно и то же свойство `iconUrl` по двум вертикалям — из данных [т.е., в случае нашего примера, из результатов поиска предложений] и из настроек отображения: + +```typescript +class Button { + static DEFAULT_OPTIONS = { + … + iconUrl: <иконка по умолчанию> + } + + constructor (data, options) { + this.data = data; + // Разрешаем переопределять + // опции по умолчанию + this.options = extend( + Button.DEFAULT_OPTIONS, + options + ) + } + + render() { + … + this.iconElement.src = + this.data.iconUrl || + this.options.iconUrl + } +} +``` + +При этом мы можем легко представить себе, что по обеим иерархиям свойство `iconUrl` было получено от кого-то из родителей по любой из вертикалей: + * опции по умолчанию могут быть определены в базовом классе, от которого унаследован `Button`; + * данные, на которых строится кнопка, могут быть общими для группы кнопок или сами по себе быть иерархическими (например, если мы будем группировать предложения сети кофеен и наследовать иконку именно родительской группы); + * в целях облегчить кастомизацию визуального стиля компонент мы можем разрешить переопределять иконку вообще всем кнопкам через задание свойств по умолчанию для всего SDK. + +В этой ситуации у нас возникает вопрос: если значение определено сразу в нескольких иерархиях (например, и в данных предложения, и в опциях по умолчанию), каким образом задавать приоритеты, чтобы выбирать одно из них? + +Простой подход «в лоб» к этому вопросу — попросту запретить наследование и заставить разработчика копировать все нужные ему свойства. То есть в нашем примере сам разработчик должен написать что-то типа: + +```typescript +const button = new Button(data); +if (data.createOrderButtonIconUrl) { + button.view.iconUrl = + data.createOrderButtonIconUrl; +} else if (data.parentCategory.iconUrl) { + button.view.iconUrl = + data.parentCategory.iconUrl; +} +``` + +Достоинства простого решения очевидны — разработчик сам имплементирует ту логику, которая ему нужна. Недостатки тоже очевидны — во-первых, это лишний и зачастую дублирующийся код; во-вторых, разработчик быстро запутается в том, какие правила он реализовал и почему. + +Чуть более сложный подход к проблеме — разрешить наследование, но строго зафиксировать приоритеты (скажем, заданное в опциях отображения значение всегда важнее заданного в данных, и они оба всегда важнее любого унаследованного свойства). Однако в достаточно сложном API результат будет тот же самым: если разработчику необходим другой порядок приоритетов, ему придётся задавать нужные свойства вручную, т.е. в итоге писать код, подобный вышеприведённому. + +Альтернативный подход — это предоставить возможность задавать правила, каким образом для конкретной кнопки определяется её иконка, декларативно или императивно: + +```json +// Декларативный подход: +// описываем правила в каком-то формате +{ + "button.checkout.iconUrl": "@data.iconUrl" +} +``` + +```typescript +// Императивный подход — программно +// добавляем функцию вычисления значения +api.options.addRule( + 'button.checkout.iconUrl', + (data, options) => data.iconUrl +) +``` + +Наиболее последовательная реализация этого подхода — CSS[ref CSS](https://www.w3.org/Style/CSS/). Мы не то чтобы рекомендуем использовать CSS-подобные правила в библиотеках компонентов (в силу потрясающей сложности их имплементации и, в большинстве случаев, избыточности), но осторожно замечаем, что поддержка какого-то простого подмножества подобного рода правил *значительно* облегчает кастомизацию визуальных компонент для разработчиков. + +#### Вычисленные значения + +Очень важно не забыть предоставить разработчику не только способы задать приоритеты параметров, но и возможность узнать, какой же из вариантов значения был применён. Для этого мы должны разделить заданные и вычисленные значения: + +```typescript +// Задаём значение в процентах +button.view.width = '100%'; +// Получаем реально применённое +// значение в пикселях +button.view.computedStyle.width; +``` + +Хорошей практикой также будет предоставлять доступ не только к вычисляемым значениям, но и к событию изменения этого значения. \ No newline at end of file diff --git a/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/09.md b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/09.md new file mode 100644 index 0000000..ef0eaf4 --- /dev/null +++ b/src/ru/clean-copy/06-Раздел V. SDK и UI-библиотеки/09.md @@ -0,0 +1,7 @@ +### [Заключение][sdk-conclusion] + +Предыдущие восемь глав были написаны нами, чтобы раскрыть две очень важные мысли: + * разработка качественной UI-библиотеки — это отдельная и весьма непростая инженерная задача; + * и эта задача не сводится к автоматической генерации SDK по спецификации / модели данных. + +Оглядываясь на всё написанное, мы с трудом можем сказать, что нашли лучшие примеры и самые понятные слова для описания такой сложной предметной области. Мы, тем не менее, надеемся, что сделали вашу жизнь — и жизнь ваших пользователей — чуточку проще. \ No newline at end of file diff --git a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/08.md b/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/08.md deleted file mode 100644 index c811a73..0000000 --- a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/08.md +++ /dev/null @@ -1,152 +0,0 @@ -### Разделяемые ресурсы и асинхронные блокировки - -Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении разработчик решил открывать экран предложения с анимацией. Для этого он воспользуется объектом offerPanel, который мы реализуем в составе searchBox: - -``` -class OfferPanel { - constuctor(searchBox) { - searchBox.on( - 'selectOffer', - (event) => { - // Показываем выбранное предложение - // в панели, но размещаем её - // за границей экрана - this.render(event.offer, { - left: screenWidth - }); - // Анимируем положение панели - this.animate( - 'left', 0, '1s' - ); - } - ); - } -} -``` - -Возникает вопрос: а что должно произойти, если, например, пользователь пытается прокрутить список предложений, и сейчас происходит анимация панели? Логически мы должны эту операцию запретить, поскольку в ней нет никакого смысла — выезжающая панель всё равно не даст просмотреть новые элементы списка. Мы можем изменить *состояние* компонента, выставив флаг «происходит анимация»: - -``` -searchBox.on('selectOffer', (offer) { - searchBox.offerPanel.render(offer, { - left: screenWidth - }); - - searchBox.state.isAnimating = true; - - await searchBox.offerPanel - .view.animate('left', 0, '1s'); - - searchBox.state.isAnimating = false; -}); - -searchBox.on('scroll', (event) => { - // Если сейчас происходит анимация - if (searchBox.state.isAnimating) { - // Запретить действие - return false; - } -}); -``` - -Но этот код очень плох по множеству причин: - * непонятно, как его модифицировать, если у нас появятся разные виды анимации, причём некоторые из них будут требовать блокировки прокрутки, а некоторые — нет; - * этот код просто плохо читается: совершенно непонятно, почему флаг `isAnimating` влияет на обработку события `scroll`; - * сложно предсказать, что произойдёт, если каким-то образом (например, программно) будет открыто другое предложение — оба процесса будут «драться» за один флаг и один объект; - * если при выполнении анимации произойдёт какая-то ошибка, флаг `isAnimating` не будет сброшен, и прокрутка будет заблокирована навсегда. - -Корректное решение первых двух проблемы — это абстрагирование от самого факта анимации и переформулирование проблемы в высокоуровневых терминах. Почему мы запрещаем прокрутку во время анимации? Потому что появление панели предложения как бы «захватывает» эту область экрана. Пользователь не может работать с другими объектами в этой области во время анимации (или, скорее, нет разумного сценария использования, при котором пользователю может понадобиться это делать). Следовательно, именно такой флаг нам и надо объявить — признак «разделяемая область на экране заблокирована»: - -``` -searchBox.on('selectOffer', (offer) { - searchBox.offerPanel.render(offer, { - left: screenWidth - }); - - searchBox.state. - isInnerAreaLocked = true; - - await searchBox.offerPanel - .view.animate('left', 0, '1s'); - - searchBox.state. - isInnerAreaLocked = false; -}); - -searchBox.on('scroll', (event) => { - // Если сейчас происходит анимация - if (searchBox.state - .isInnerAreaLocked) { - // Запретить действие - return false; - } -}); -``` - -Такой подход улучшает семантику операций, но не помогает с проблемами параллельного доступа и ошибочно неснятых флагов. Чтобы решить их, нам нужно сделать ещё один шаг: не просто ввести флаг, но и процедуру его *захвата* (вполне классическим образом по аналогии с управлением разделяемыми ресурсами в системном программировании): - -``` -try { - const lock = await searchBox - .state.acquireLock('innerArea', '2s'); - - await searchBox.offerPanel - .view.animate('left', 0, '1s'); - - lock.release(); -} catch (e) { - // Какая-то логика обработки - // невозможности захвата ресурса - … -} -``` - -**NB**: вторым параметром в `acquireLock` мы передали время жизни блокировки — 2 секунды. Если в течение двух секунд блокировка не снята, она будет отменена автоматически. - -В таком подходе мы можем реализовать не только блокировки, но и программируемые прерывания и реакцию на них: - -``` -const lock = await searchBox - .state.acquireLock( - 'innerArea', - '2s', { - // Добавляем описание, - // кто и зачем пытается - // выполнить блокировку - reason: 'selectOffer', - offer - } - ); - -lock.on('lost', () => { - // Если у нас забрали блокировку, - // отменяем анимацию - searchBox.offerPanel.view - .cancelAnimation(); -}) - -// Если другой актор пытается -// перехватить блокировку -lock.on('tryLock', (sender) => { - // Если это другое предложение, - // разрешааем перехват и отменяем - // текущую блокировку - if (sender.reason == 'selectOffer') { - lock.release(); - } else { - // Иначе запрещаем перехват - return false; - } -}) - -await searchBox.offerPanel - .view.animate('left', 0, '1s'); - -lock.release(); -``` - -**NB**: хотя пример выше выглядит крайне переусложнённым, в нём не учтено ещё множество нюансов: - * `offerPanel` тоже должен стать разделяемым ресурсом, и его точно так же надо захватывать; - * при перехвате блокировки должна останавливаться не анимация вообще, а та конкретная операция, которая была запущена в рамках конкретной блокировки. - -Упражнение «найти все разделяемые ресурсы и дополнить пример корректной работой с ними» мы оставим читателю. \ No newline at end of file diff --git a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/09.md b/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/09.md deleted file mode 100644 index c25adad..0000000 --- a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/09.md +++ /dev/null @@ -1,69 +0,0 @@ -### Вычисляемые свойства - -Наличие множественных линий наследования усложняет кастомизация компонентов, поскольку подразумевает, что они могут наследовать важные свойства по любой из вертикалей (иконка кнопки может быть задана и в визуальных настройках компонента, и в данных, и в родительском классе). - -Вернёмся к проблеме, которую мы описали в предыдущей главе. Пусть у нас имеется кнопка, которая получает одно и то же свойство `iconUrl` по двум вертикалям — из данных и из настроек отображения: - -``` -const button = new Button({ - model: { - iconUrl: - } -); -button.view.options.iconUrl = ; -``` - -При этом мы можем легко представить себе, что по обеим иерархиям свойство `iconUrl` было получено от кого-то из родителей — например, данные могут быть сгруппированы по бренду, и иконка будет задана для всей группы. Также возможно, что мы разрешим переопределять иконку вообще всем кнопкам через задание свойств по умолчанию для всего SDK. - -В этой ситуации у нас возникает вопрос: каким образом задавать приоритеты, какой из возможных вариантов опции будет выбран. - -Современные графические SDK в зависимости от выбранного подхода делятся на две категории: построенные по образу и подобию CSS и все остальные. - -#### Приоритеты наследования - -Простой подход «в лоб» к этому вопросу — либо зафиксировать приоритеты в точности (скажем, заданное в опциях отображения значение всегда важнее заданного в данных, и они оба всегда важнее любого унаследованного свойства), либо попросту запретить наследование и заставить разработчика копировать все нужные ему свойства. То есть в нашем примере сам разработчик должен написать что-то типа: - -``` -const button = new Button(…); -if (button.data.checkoutButtonIconUrl) { - button.view.iconUrl = - button.data.checkoutButtonIconUrl; -} else if ( - button.data.parentCategory?.iconUrl -) { - button.view.iconUrl = - button.data.parentCategory.iconUrl; -} -``` - -(В достаточно сложном API оба этих подхода приведут к одинаковому результату. Если приоритеты фиксированы, то это рано или поздно приведёт к необходимости написать код, подобный вышеприведённому, так как разработчик не сможет добиться нужного результата иначе.) - -Достоинства простого решения очевидны — разработчик сам имплементирует ту логику, которая ему нужна. Недостатки тоже очевидны — во-первых, это лишний код; во-вторых, разработчик быстро запутается в том, какие правила он реализовал и почему. - -Альтернативный подход — это предоставить возможность декларативно задавать правила, каким образом для конкретной кнопки определяется её иконка, либо напрямую в виде CSS, либо предоставив какие-то похожие механизмы типа: - -``` -api.options.addRule( - // Читать примерно так: кнопки - // типа `checkout` значение `iconUrl` - // берут из поля `iconUrl` своей модели - 'button[@type=checkout].iconUrl', - 'model.iconUrl' -) -``` - -Думаем, излишне уточнять, что разработка своей CSS-подобной системы — огромное количество работы, и к тому же изобретение велосипеда. Использование настоящего CSS, если оно возможно — более разумный подход, однако и он зачастую совершенно избыточен, и только весьма ограниченный набор возможностей системы будет реально использоваться разработчиками. - -#### Вычисленные значения - -Очень важно не забыть предоставить разработчику не только способы задать приоритеты параметров, но и возможность узнать, какой же из вариантов значения был применён. Для этого мы должны разделить заданные и вычисленные значения: - -``` -// Задаём значение в процентах -button.view.width = '100%'; -// Получаем реально применённое -// значение в пикселях -button.view.computedStyle.width; -``` - -При этом необходимо предоставить доступ не только к вычисляемым значениям, но и к событию изменения этого значения. \ No newline at end of file diff --git a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/10.md b/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/10.md deleted file mode 100644 index 2e1d8f2..0000000 --- a/src/ru/drafts/06-Раздел V. SDK и UI-библиотеки/10.md +++ /dev/null @@ -1,9 +0,0 @@ -### В заключение - -Предыдущие восемь глав были написаны нами, чтобы раскрыть две очень важные мысли: - * разработка качественной UI-библиотеки — это отдельная и весьма непростая инженерная задача; - * и эта задача не сводится к автоматической генерации SDK по спецификации. - -Автор этой книги в течение 10 лет разрабатывал подобную сложную интерактивную визуальную систему, позволяющую кастомизировать компоненты вплоть до замены технологии рендеринга, и может уверенно констатировать: не знакомые с проблематикой разработки такого SDK программисты и менеджеры склонны считать проблемы UI несущественными и не заслуживающими большого внимания, и это само по себе быстро становится проблемой. (Впрочем, то же самое мы можем сказать и про разработку API в целом.) - -Оглядываясь на всё написанное, мы с трудом можем сказать, что нашли лучшие примеры и самые понятные слова для описания такой сложной предметной области. Мы, тем не менее, надеемся, что сделали вашу жизнь — и жизнь ваших пользователей — чуточку проще. Спасибо за ваше внимание! \ No newline at end of file