Что такое миксин? В соответствии с версией 2.2 TypeScript теперь поддерживает концепцию миксина - функции, которая может принимать класс, расширять его некоторыми функциями, а затем возвращать новый класс, позволяя другим классам расширяться от него, что позволяет классам смешивать и поделитесь функциональностью!

Как это работает?

Концепция довольно проста - если мы знакомы с наследованием, классами / функциями более высокого порядка и их синтаксисом, мы можем сразу перейти к ним. Вот пример из самой документации TypeScript:

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

Как это можно использовать?

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

Конечно, первое, что приходит в голову, это Guard. Поскольку наше приложение разработано хорошо, большинство (но не все!) Компонентов с формами в них имеют общее поле form, которое является экземпляром AbstractFormControl . Конечно, после этого мы можем проверить поле touched свойства form и выяснить, нужно ли отображать подсказку. Охранник может выглядеть так:

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

Конечно, это здорово, но для большинства компонентов (например, 90%) метод isFormTouched сводится именно к следующему:

Конечно, мы можем скопировать и вставить этот метод в каждый компонент, в котором он нам нужен, но само это действие пахнет подозрительно, не так ли? Причина, по которой нам не нужно это решение, заключается в том, что однажды может потребоваться также проверить, была ли форма уже отправлена ​​(используя свойство isSubmitted в том же классе). Для этого нам потребуется переписать много кода. И если мы пропустим один экземпляр - может пройти много времени, прежде чем кто-нибудь обнаружит такую ​​незначительную ошибку. Итак, естественно, нам нужно решение, которое позволит нам написать этот конкретный метод только один раз, но при этом поделиться им между всеми нашими компонентами, которые в нем нуждаются. Естественно, на ум приходит наследование, но у него есть три основных недостатка:

  1. Идиоматическое: наследование предназначено для представления отношения is-a (например, «лошадь - животное»), а не для совместного использования функций. Для этого используется внедрение зависимости или композиция объекта.
  2. Парадигматический: что вообще должен представлять этот класс? В некоторых языках ООП присутствует концепция trait, которая допускает такие вещи, но в JS у нас нет такой функции, поэтому наши классы должны представлять что-то, а не просто содержать один простой метод.
  3. Практический: что, если в будущем нам понадобится расширить наш компонент из другого базового класса? В этом случае наши руки будут связаны.

Миксины спешат на помощь

Взгляните на эту функцию:

Как видите, эта функция принимает простой класс (который также может быть компонентом Angular) и возвращает другой компонент, который расширяется от него, а также имеет реализованный на нем метод isFormTouched. Затем мы можем сделать эту простую вещь:

Вот как эта функция решает все три проблемы, упомянутые выше:

  1. Поскольку он назван WithFormTouchedCheck, теперь он представляет класс (который он может получить в качестве аргумента - или без него, обратите внимание на строку Base: Constructor<T> = (class {} as any), которая в основном означает, что если класс не указан, будет расширен пустой класс), который был расширен с помощью некоторые дополнительные функции.
  2. Эта функция не является классом сама по себе, а представляет только функциональность, которую она добавляет к существующей - при условии, что она реализует определенный интерфейс - вот почему она объявлена ​​как абстрактная).
  3. Поскольку он принимает другой класс в качестве аргумента, теперь он позволяет нам расширять наш компонент и из других классов. Фактически, пока мы сохраняем этот формат, у нас может быть столько классов, сколько мы захотим!

Смешивание вещей

Я уверен, что многие из нас слышали страшные истории о подписках на зомби и о том, как их избежать с помощью оператора takeUntil. Конечно, нам также потребуется создать для этого специальный Subject и отправить next уведомления внутри нашего ngOnDestroy метода. Собственно, вот код:

Внутри многих наших компонентов могут быть подписки, и все из них должны реализовывать ту же функцию, поэтому имеет смысл преобразовать ее в миксин:

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

Будьте осторожны: мы реализовали метод ngOnDestroy в нашем миксине: если вы собираетесь использовать его в классе, который также сам будет реализовывать метод ngOnDestroy, обязательно вызовите super.ngOnDestroy() внутри него!

Минусы

Конечно, у любого подхода к программированию могут быть свои недостатки. Большинство из них в нашем случае связаны либо с некоторыми проблемами компилятора Typescript, либо с проблемами сборки Angular. Здесь я представляю два из них, которые могут быть самыми неприятными.

1. Decorators are not valid here выпуск:

Если мы попробуем использовать декоратор внутри нашего миксина, например:

Мы получим сообщение об ошибке Decorators are not valid here. На самом деле это проблема компилятора Typescript. Вот обходной путь:

2. Проблема с угловыми входами:

Если мы включим декоратор в один из наших классов миксина, например, чтобы определить свойство Input в классе, он будет работать, как задумано, когда мы ng serve наше приложение, но выдаст ошибку во время производственной сборки. Это связано с этим вопросом. Обходной путь для этого немного сложнее:

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

Заключение

Примеси Typescript - отличный способ улучшить наше приложение Angular с помощью общих функций, не нарушая никакого потока. Хотя у этой технологии все еще есть небольшие недостатки, я лично считаю, что ее преимущества значительно перевешивают их.

Подпишитесь на меня в Medium и Twitter, чтобы узнать больше об Angular, Rxjs, React и Javascript в целом.