Обертывание императивных API в Angular


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


Правило прогрессивной реактивности №3

Оберните императивные API декларативными.

Императивные API лучше, чем отсутствие API, и они, как правило, предшествуют декларативным API. Почему так происходит, и что мы можем с этим сделать?

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

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

Поэтому мы не должны удивляться или расстраиваться, что в Angular есть множество императивных API. AngularJS был ранним SPA-фреймворком и решал сложные и новые проблемы. Фактически, AngularJS привнес реактивность в обновления DOM с обнаружением изменений, и именно этот механизм создал проблемы, которые в итоге были решены с помощью императивных API. А затем Angular попытался сохранить некоторую преемственность с AngularJS, поэтому он унаследовал большую часть императивного стиля.

Angular незаслуженно игнорируется многими разработчиками, которые перешли на React или другой фреймворк (да, фреймворк) после AngularJS и не имеют ни малейшего представления о том, как выглядит современный Angular. Однако другие современные фреймворки добились прогресса, которого не смог добиться Angular. Хотя они в основном не знают о преимуществах RxJS, у них гораздо больше декларативных API, чем у Angular, и это иногда заставляет меня завидовать.

Модалы

Мой любимый пример — модалы. В экосистеме Angular кажется само собой разумеющимся, что диалоги нужно открывать с помощью императивной команды .open(). Но это не обязательно должно быть так. Буквально каждая другая библиотека компонентов в буквально каждом другом современном front-end фреймворке имеет декларативные диалоги, которые реагируют на состояние, вместо того, чтобы зависеть от внеконтекстных императивных команд для их открытия. Не верите мне? Ну, даже если верите, я хочу вам это показать. Давайте рассмотрим Vue, React, Svelte, Preact, Ember, Lit, Alpine и SolidJS. Не стесняйтесь переходить к Angular. Это длинный список.

Vue.js

Лучшие библиотеки компонентов Vue

Vuetify

Quasar

Bootstrap Vue

React

Лучшие библиотеки компонентов React

Material UI

Ant Design

React Bootstrap

Svelte

Лучшие библиотеки компонентов Svelte

Svelte Material UI

SvelteStrap

Smelte

Preact

Честно говоря, было трудно найти библиотеки компонентов для Preact. Я включил единственную, которую нашел, с документацией, которую было легко найти.

Preact Material

Я считаю, что простой рендеринг элемента Dialog открывает его, так что это декларативно.

Ember

Лучшие библиотеки компонентов Ember

Ember Paper

Ember Frontile

SL Ember Components

Lit

Lit предназначен для создания веб-компонентов, поэтому в этой статье я просто рассмотрю библиотеки веб-компонентов.

PolymerElements Paper Dialog

Веб-компоненты Vaadin

Wired Elements

Alpine

Я нашел только этот пример:

SolidJS

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

Я нашел эту неофициальную библиотеку компонентов для Headless UI:

Angular

И наконец, Angular. Лучшие библиотеки компонентов Angular

Angular Material

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

Итак, вызывается метод. Это нарушает наше правило 2. Что делает этот метод?

Это первая библиотека компонентов из 20+ для 7+ фреймворков, которые я видел, которая открывает диалоги императивно.

Вторая и третья библиотеки также императивны.

ngx-bootstrap

ng-bootstrap


Подведем итоги,

Фреймворк Библиотека 1 Библиотека 2 Библиотека 3
Vue ✅ Декларативный ✅ Декларативный ✅ Декларативный
React ✅ Декларативный ✅ Декларативный ✅ Декларативный
Svelte ✅ Декларативный ✅ Декларативный ✅ Декларативный
Preact ✅ Декларативный ✅ Декларатив ✅ Декларативный
Ember ✅ Декларативный ✅ Декларативный ✅ Декларативный
Lit ✅ Декларативный ✅ Декларативный ✅ Декларативный
SolidJS ✅ Декларативный ✅ Декларативный
Alpine ✅ Декларатив
Угловой ❌ Императив ❌ Императив ❌ Императив

Но вам не придется страдать.

Опять же, не стоит удивляться или расстраиваться, что в Angular много императивных API. AngularJS был ранним SPA-фреймворком и решал сложные и новые задачи.

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

Поэтому я создал обертку для диалогового компонента Angular Material, которую вы можете использовать следующим образом:

<app-dialog 
  [component]="AnyComponent" 
  [open]="open$ | async"
></app-dialog>
Войти в полноэкранный режим Выход из полноэкранного режима

ПЕРЕЙДИТЕ К ЭТОМУ GIST И СКОПИРУЙТЕ ЕГО В СВОЮ КОДОВУЮ БАЗУ ПРЯМО СЕЙЧАС.

Перестаньте жить в боли. Наслаждайтесь декларативными диалогами.

Вы должны быть проактивны и обернуть ВСЕ императивные API в декларативные API.

Другие императивные API в Angular

Диалоги — не единственное место, где в Angular есть императивные API. Нам все еще приходится писать императивный код для крючков жизненного цикла компонентов. Angular Reactive Forms следует называть Angular Imperative Forms. Есть и другие. В прошлом я писал о том, как работать с этими другими императивными API Angular. Осторожно, это премиум-статья на Medium. Вот ссылка.

Боковые эффекты

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

Диалоги являются примерами API, которые в конечном итоге выводят что-то пользователю, но что насчет более закулисных API, таких как localStorage?

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

Вместо того чтобы вызывать setItem в функции обратного вызова, мы хотели бы, чтобы localStorage сам объявлял свое состояние с течением времени. Что-то вроде этого было бы неплохо:

this.localStorageService.connect('key', this.state$);
Войти в полноэкранный режим Выход из полноэкранного режима

Но что подписывается? Что отписывается? А что если state$ отпочковывается от http$ observable? Хотим ли мы немедленно вызвать его, подписавшись? Очевидно, что локальное хранилище не должно быть основным подписчиком на то, что оно наблюдает. Но RxJS не поддерживает «вторичных» подписчиков или пассивное прослушивание любого рода. Итак, я вижу 2 возможных решения:

  1. Добавить tap к объявлению state$. Таким образом, все, что подписывается на

    state$ = defineStateSomehow().pipe(
      tap(s => localStorage.setItem('s', JSON.stringify(s))),
    );
    

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

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

    <app-local-storage
      key="key"
      [item]="state$ | async"
    ></app-local-storage>
    

    Это странно? Отчасти да. Но это так удобно. И если мы хотим, мы можем обернуть этот элемент в *ngIf, который контролирует, когда app-local-storage подписывается.

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

Другие императивные API могут возвращать наблюдаемые данные, поэтому их гораздо проще выразить реактивно. Например, POST-запрос может быть выполнен следующим образом:

submit$ = new Subject<void>();

submissionSuccessful$ = this.submit$.pipe(
  withLatestFrom(this.form.valueChanges),
  concatMap(([, data]) => this.apiService.submit(data)),
);
Войти в полноэкранный режим Выйти из полноэкранного режима

Большинство из вас, вероятно, привыкли, что вместо этого есть метод submit. Но это императивный метод, когда он может быть реактивным. Как вы думаете, почему $http.post возвращает наблюдаемую? Потому что POST-запросы возвращают значения, и не только для того, чтобы они могли затеряться в глубинах нашего приложения. Вероятно, мы должны иметь обертку для компонента тоста, чтобы мы могли показать пользователю, что его отправка прошла успешно:

<app-toast
  [message]="submissionSuccessful$ | async"
  duration="3000"
></app-toast>
Войти в полноэкранный режим Выход из полноэкранного режима

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

Резюме

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

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


Оцените статью
devanswers.ru
Добавить комментарий