Что такое миксин? В соответствии с версией 2.2 TypeScript теперь поддерживает концепцию миксина - функции, которая может принимать класс, расширять его некоторыми функциями, а затем возвращать новый класс, позволяя другим классам расширяться от него, что позволяет классам смешивать и поделитесь функциональностью!
Как это работает?
Концепция довольно проста - если мы знакомы с наследованием, классами / функциями более высокого порядка и их синтаксисом, мы можем сразу перейти к ним. Вот пример из самой документации TypeScript:
Как мы видим, здесь мы используем функцию для создания расширенной версии другого класса, которую можно использовать как для создания экземпляров новых объектов, так и для расширения других классов. В некотором смысле теперь это допускает множественное наследование - если некоторые из наших классов нужны только для совместной работы (абстрактный класс), то мы можем записать его внутри функции, чтобы его можно было смешивать с другими классы для дальнейшего сочинения.
Как это можно использовать?
Представьте себе обычное приложение Angular с разными страницами, на некоторых из которых есть формы. Все хорошо, и в один прекрасный день мы решаем, что отныне, если пользователь касается формы, но пытается покинуть страницу, не сохраняя ее, должно отображаться окно с просьбой подтвердить, действительно ли он хочет покинуть форму. страница (очень стандартная функция).
Конечно, первое, что приходит в голову, это Guard
. Поскольку наше приложение разработано хорошо, большинство (но не все!) Компонентов с формами в них имеют общее поле form
, которое является экземпляром AbstractFormControl
. Конечно, после этого мы можем проверить поле touched
свойства form
и выяснить, нужно ли отображать подсказку. Охранник может выглядеть так:
Конечно, на бумаге это выглядит хорошо, но на самом деле, как вкратце упоминалось выше, не каждая страница работает так - некоторые страницы имеют несколько форм, некоторые страницы имеют формы внутри своих дочерних компонентов, некоторые имеют и то, и другое. Конечно, мы хотим, чтобы охрана работала одинаково для каждого компонента, и мы также хотим быть очень точными и последовательными. Итак, вот мысль: с этого момента каждый компонент, который должен иметь эту защиту, должен реализовывать специальный метод, называемый isFormTouched
, который будет возвращать boolean
, чтобы сообщить охраннику, должен ли он отображать подсказку или нет. Вот пример более сложного компонента, который имеет форму не внутри себя, а внутри своего дочернего компонента, и того, как он реализует метод:
Конечно, это здорово, но для большинства компонентов (например, 90%) метод isFormTouched
сводится именно к следующему:
Конечно, мы можем скопировать и вставить этот метод в каждый компонент, в котором он нам нужен, но само это действие пахнет подозрительно, не так ли? Причина, по которой нам не нужно это решение, заключается в том, что однажды может потребоваться также проверить, была ли форма уже отправлена (используя свойство isSubmitted
в том же классе). Для этого нам потребуется переписать много кода. И если мы пропустим один экземпляр - может пройти много времени, прежде чем кто-нибудь обнаружит такую незначительную ошибку. Итак, естественно, нам нужно решение, которое позволит нам написать этот конкретный метод только один раз, но при этом поделиться им между всеми нашими компонентами, которые в нем нуждаются. Естественно, на ум приходит наследование, но у него есть три основных недостатка:
- Идиоматическое: наследование предназначено для представления отношения is-a (например, «лошадь - животное»), а не для совместного использования функций. Для этого используется внедрение зависимости или композиция объекта.
- Парадигматический: что вообще должен представлять этот класс? В некоторых языках ООП присутствует концепция
trait
, которая допускает такие вещи, но в JS у нас нет такой функции, поэтому наши классы должны представлять что-то, а не просто содержать один простой метод. - Практический: что, если в будущем нам понадобится расширить наш компонент из другого базового класса? В этом случае наши руки будут связаны.
Миксины спешат на помощь
Взгляните на эту функцию:
Как видите, эта функция принимает простой класс (который также может быть компонентом Angular) и возвращает другой компонент, который расширяется от него, а также имеет реализованный на нем метод isFormTouched
. Затем мы можем сделать эту простую вещь:
Вот как эта функция решает все три проблемы, упомянутые выше:
- Поскольку он назван
WithFormTouchedCheck
, теперь он представляет класс (который он может получить в качестве аргумента - или без него, обратите внимание на строкуBase: Constructor<T> = (class {} as any)
, которая в основном означает, что если класс не указан, будет расширен пустой класс), который был расширен с помощью некоторые дополнительные функции. - Эта функция не является классом сама по себе, а представляет только функциональность, которую она добавляет к существующей - при условии, что она реализует определенный интерфейс - вот почему она объявлена как абстрактная).
- Поскольку он принимает другой класс в качестве аргумента, теперь он позволяет нам расширять наш компонент и из других классов. Фактически, пока мы сохраняем этот формат, у нас может быть столько классов, сколько мы захотим!
Смешивание вещей
Я уверен, что многие из нас слышали страшные истории о подписках на зомби и о том, как их избежать с помощью оператора 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 в целом.