mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-03-29 20:51:01 +02:00
chapters 46-47, edits
This commit is contained in:
parent
a10681f8f0
commit
f5dcaec39e
@ -59,6 +59,6 @@ class SearchBox {
|
||||
|
||||
* сложившая практика такова, что разработка бэкенда и клиентского кода — разные специализации, требующие разной экспертизы и разных подходов к написанию кода.
|
||||
|
||||
**NB**: следует различать Backend-Driven UI и технологию серверного рендеринга (SSR). Второе подразумевает, что одно и то же состояние UI (как правило, в виде HTML-разметки или аналогичного декларативного описания) может быть сгенерировано как сервером, так и клиентом. В SSR, однако, подразумевается, что клиент может интерпретировать ответ сервера и может извлекать из него семантические данные.
|
||||
**NB**: следует различать Backend-Driven UI и технологию серверного рендеринга (SSR). Второе подразумевает, что одно и то же состояние UI (как правило, в виде HTML-разметки или аналогичного декларативного описания) может быть сгенерировано как сервером, так и клиентом. В SSR, однако, подразумевается, что клиент может интерпретировать ответ сервера и извлекать из него семантические данные.
|
||||
|
||||
С точки зрения предложения SDK сторонним разработчикам, Backend-driven UI фактически вынуждает разрабатывать гибридный код (поскольку практика предоставления партнёрам возможности встроиться в функции серверного рендеринга на стороне провайдера API выглядит весьма далёкой от жизнеспособности идеей), и, таким образом, будет страдать от той же проблемы непрозрачности — разработчик не может со своей стороны определить, в каком сейчас состоянии находится визуальный компонент. Как ни странно, этот недостаток одновременно является и достоинством, поскольку вендор API сохраняет возможность на своей стороне производить любые манипуляции с контентом без нарушения обратной совместимости (мы обсудим этот вопрос в главе «[Линейка сервисов API](#api-product-lineup)»). Таких клиентских API в мире существует довольно много (в первую очередь, разнообразные виджеты, рекламные API и т.д.), но их сложно назвать полноценными SDK.
|
@ -1 +1,152 @@
|
||||
### Разделяемые ресурсы и асинхронные блокировки
|
||||
### [Разделяемые ресурсы и асинхронные блокировки][sdk-shared-resources]
|
||||
|
||||
Другой важный паттерн, который мы должны рассмотреть — это доступ к общим ресурсам. Предположим, что в нашем учебном приложении разработчик решил открывать экран предложения с анимацией. Для этого он воспользуется объектом 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` тоже должен стать разделяемым ресурсом, и его точно так же надо захватывать;
|
||||
* при перехвате блокировки должна останавливаться не анимация вообще, а та конкретная операция, которая была запущена в рамках конкретной блокировки.
|
||||
|
||||
Упражнение «найти все разделяемые ресурсы и дополнить пример корректной работой с ними» мы оставим читателю.
|
@ -1,44 +0,0 @@
|
||||
### Backend-Driven UI
|
||||
|
||||
Другой способ обойти сложность «переброса мостов» между несколькими предметными областями, которые нам приходится сводить в рамках одного UI-компонента — это убрать одну из них. Как правило, речь идёт о бизнес-логике: мы можем разработать компоненты полностью абстрактными, и скрыть все трансляции UI-событий в полезные действия вне контроля разработчика.
|
||||
|
||||
В такой парадигме код открытия панели предложений (которая должна перестать быть панелью предложений и стать просто «панелью чего-то») выглядел бы так:
|
||||
|
||||
```
|
||||
class SearchBox {
|
||||
selectOffer: (offer) => {
|
||||
this.offerPanel.setContent(
|
||||
await api
|
||||
.getOfferPanelContent(item);
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Или даже так:
|
||||
|
||||
```
|
||||
class SearchBox {
|
||||
stateChange: (patch) => {
|
||||
// Получаем от сервера
|
||||
// список действий, которые
|
||||
// необходимо выполнить при
|
||||
// запрошенном изменении
|
||||
const actions = await api
|
||||
.getActions(
|
||||
this.model,
|
||||
patch
|
||||
);
|
||||
// Применяем действия
|
||||
…
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(Примером реализации этой идеи можно считать т.н. «Web 1.0» — сервер присылает готовый контент страницы, а вся интерактивность сводится к переходам по ссылкам.)
|
||||
|
||||
Этот подход, безусловно, является крайне привлекательным с двух сторон:
|
||||
* возможность с сервера регулировать поведение клиента, в том числе устранять возможные ошибки в реальном времени;
|
||||
* возможность сэкономить на разработке консистентной и читабельной номенклатуры сущностей публичного SDK, ограничившись минимальным набором доступной функциональности.
|
||||
|
||||
Тем не менее, мы не можем не отметить: при том, что любая крупная IT-компания проходит через эту фазу — разработки Backend-Driven UI (они же — «тонкие клиенты») для своих приложений или своих публичных SDK — мы не знаем ни одного заметного на рынке API, разработанного в этой парадигме (кроме протоколов удалённых терминалов), хотя во многих случаях возникающими сетевыми задержками вполне можно было бы пренебречь. Нам сложно выделить конкретные причины, почему так происходит, но мы рискнём предположить, что разработать серверный код управления UI ничуть не проще, нежели клиентский — даже если вы не должны документировать каждый метод и абстрагировать каждую сущность — и игра, в конечном счёте, не стоит свеч.
|
Loading…
x
Reference in New Issue
Block a user