От автора: в своем последнем посте я писал об Управлении состоянием в Angular. В конце я написал о Connect Mixin, который я использовал в то время. Ладно, если честно, я имел в виду компонент более высокого уровня, но в Angular это не так просто сделать. Итак, давайте по порядку. Сегодня говорим о том, как использовать в Angular.js миксины.
Цель Connect Mixin — подключиться к хранилищу NGRX и предоставить нам из него данные. Если вы не хотите читать весь пост, я бы посоветовал вам перейти к части Connect-Mixin. Или, по крайней мере, прочитайте последнюю часть: Плохие миксины.
1. Что такое миксины?
В программировании MixIn — это стиль разработки программного обеспечения, в котором функциональные единицы создаются в классе, а затем смешиваются с другими классами.
Класс mixin — это родительский класс, унаследованный от другого — но не как средство специализации. Как правило, миксин экспортирует сервисы в дочерний класс, но семантика не подразумевает, что дочерний класс «является своего рода» родителем.
По моему собственному определению: Миксин — это особый тип наследования классов. Они дают вам возможность множественного наследования и позволяют использовать код из разных классов. Но для чего это нужно?
2. Для чего используются миксины
Они позволяют нам распределить кросс-функциональное поведение. Скажем, у нас есть некоторые общие функции для подключения к хранилищу redux и некоторые общие функции, которые мы хотим совместно использовать для многих представлений. Примитивный подход может выглядеть следующим образом:
class Connect { connect(selectors, actions) { //.. } } class View { viewMode: 'create' | 'show' | 'edit'; } class CommitView extends View, Connect { // [ts] Classes can only extend a single class. [1174] }
Множественное наследование через расширение не допускается
Но компилятор не совсем доволен этим. Выдается ошибка: Classes can only extend a single class!
Как мы можем решить эту проблему? Миксин нам в помощь. Давайте сразу рассмотрим код. Чтобы достичь желаемого поведения, мы должны создать некоторые функции, которые смешивают (MixIn ^^) поведение. Такая функция Mixin ожидает по крайней мере один аргумент, а именно класс, который мы хотим расширить. И это именно то, что происходит внутри функции. Она возвращает выражение класса, которое выходит из данного класса и добавляет к нему дополнительное поведение:
class CommitView {} // Mixin Functions function mixinView<B extends Constructor>(Base: B) { return class extends Base { // every view has a viewMode viewMode: 'create' | 'show' | 'edit'; } } function mixinConnect<B extends Constructor>(Base: B) { return class extends Base { constructor(...args: any []) { super(...args); // Connect to a specific store implementation this.connect({}, {}); } connect(selectors, actions) { // TBD // 1. Read values with selectors // 2. Bind Actions to the Redux Dispatch function and the result to the Base class } } } // Usage: // Enhance the base class CommitView with multiple behaviours const ConnectedView = mixinConnect(mixinView(CommitView)); const view = new ConnectedView(); // access to the connect method provided by the Connect-Mixin and // access to the viewMode provided by the View-Mixin view.connect({}, {}); view.viewMode;
Образец миксинов
Теперь мы можем улучшить базовый класс CommitView с помощью поведения Connect-Mixin и поведения View-Mixin. По сути, функция Mixin берет определение класса и дополняет его. В моем примере выше View-Mixin добавляет поле viewMode, а Connect-Mixin подключается к конкретной реализации хранилища. Чтобы увидеть полную реализацию Connect-Mixin, пожалуйста, прокрутите вниз до пункта 4.
3. Миксины в Angular Material
Просматривая определенный источник Angular Material, я заметил, что там тоже используются миксины Это было своего рода сюрпризом, но в этом есть смысл. В Material есть много общих функций в разных директивах, таких как Color Mixin, Disabled Mixin, Tabindex Mixin, Error Sate Mixin или Initialized Mixin. Создание нескольких базовых классов со всеми различными вариантами поведения было бы просто слишком затратно и с трудом обслуживаемо. Я думаю, что это прекрасный пример правильного использования миксинов. Посмотрите на Color Mixin:
export function mixinColor<T extends Constructor<HasElementRef>>( base: T, defaultColor?: ThemePalette): CanColorCtor & T { return class extends base { private _color: ThemePalette; get color(): ThemePalette { return this._color; } set color(value: ThemePalette) { const colorPalette = value || defaultColor; if (colorPalette !== this._color) { if (this._color) { this._elementRef.nativeElement.classList.remove(`mat-${this._color}`); } if (colorPalette) { this._elementRef.nativeElement.classList.add(`mat-${colorPalette}`); } this._color = colorPalette; } } constructor(...args: any[]) { super(...args); // Set the default color that can be specified from the mixin. this.color = defaultColor; } }; }
До введения миксинов в Angular Material один и тот же код копировался снова и снова, вставлялся в несколько компонентов.
4. Connect Mixin
Как и было обещано выше, вот Connect Mixin. Его задача — работать аналогично компоненту высшего порядка Connect из React Redux. Это позволяет вам указать селекторы и действия, которые затем смешиваются с данным компонентом.
В этом мини-примере я создал и подключил Counter Counter к хранилищу NGRX.
Примитивный счетчик — я знаю
Давайте посмотрим на использование Connect Mixin. Обратите внимание, что это очень тонкая реализация компонента контейнера. Контейнер выполняет только одну задачу: подключение счетчика. Это другой, но чистый подход к компонентам контейнера. Чем он отличается? В Angular Applications я часто вижу очень большие компоненты контейнеров, которые соединяют все виды данных из хранилища и выполняют дополнительную бизнес-логику. Они таким образом не соответствуют принципу единичной ответственности.
export class CounterViewBase { constructor(public injector: Injector) {} } export const CounterViewMixins = mixinConnect(CounterViewBase, select => ({ counter: select(counterSelector) }), dispatch => ({ increment: (payload: number) => dispatch(incrementActionCreator(payload)), decrement: (payload: number) => dispatch(decrementActionCreator(payload)), reset: (payload: number) => dispatch(resetActionCreator(payload)) })); @Component({ selector: 'counter-view', templateUrl: 'counter.container.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterContainer extends CounterViewMixins { constructor(public injector: Injector) { super(injector); } }
Чтобы использовать включение зависимостей Angular, мы должны добавить между ними класс CounterViewBase и расширить контейнер из созданного класса Mixins. Классы Mixin должны иметь конструктор с единственным параметром типа any[], и поэтому мы не можем внедрить Injector непосредственно в Mixin.
В приведенном ниже шаблоне мы напрямую вызываем методы, привязанные к контейнеру через Connect-Mixin. Эти методы доступны через объект vm. Это ограничение, которое мы должны принять, чтобы оставаться совместимыми с AOT. Вы можете назвать это как угодно. Важно только то, чтобы переменная использовалась в шаблоне.
<!-- counter.container.html --> <ng-container *let="let count=counter from { counter: vm.counter | async }"> <counter [count]="count" (onIncrement)="vm.increment(count+1)" (onDecrement)="vm.decrement(count-1)" (onReset)="vm.reset(0)"> </counter> </ng-container>
Шаблон контейнера счетчика
Сам Connect-Mixin ожидает компонент, который содержит инжектор, поэтому мы создали интерфейс HasInjector. Нам нужен инжектор, чтобы получить во время выполнения сервис NGRX Store. Мы могли бы включить NGRX Store непосредственно в контейнер, но я хотел отделить контейнер от реализации хранилища, которую мы используем.
import {Injector} from '@angular/core'; import {Action, Store} from '@ngrx/store'; export function mixinConnect<T extends Constructor<HasInjector>, I, O>(base: T, inputs: Inputs<I>, outputs: Outputs<O>): T & Constructor<HasViewModel<I & O>> { return class extends base { vm: I & O; store: Store<any>; constructor(...args: any[]) { super(...args); this.store = this.injector.get(Store); // Bind inputs const selectedInputs: I = inputs(this.store.select.bind(this.store)); // Bind outputs const boundOutputs: O = outputs(this.store.dispatch.bind(this.store)); this.vm = Object.assign({}, boundOutputs, selectedInputs); } }; } /* ––––––––––––––––––––––––––––––– */ /* –– Types & Interfaces ––– */ /* ––––––––––––––––––––––––––––––– */ export type Constructor<T> = new(...args: any[]) => T; export interface HasInjector { injector: Injector; } export interface HasViewModel<T> { vm: T; } const store: Store<any> = null; type DispathFn = typeof store.dispatch; type SelectFn = typeof store.select; type Outputs<O> = (dispatch: DispathFn) => O; type Inputs<I> = (select: SelectFn) => I; /* ––––––––––––––––––––––––––––––– */ /* –– NGRX Typings ––– */ /* ––––––––––––––––––––––––––––––– */ export interface Payload<T> { payload: T; }; export type ActionCreator<T> = (payload: T) => Action & Payload<T>;
Хотя в этом примере я не использую объект vm в компоненте контейнера, типы Inputs <I> и Outputs <O> дают нам безопасную обработку типов.
5. Миксины плохие?
Я должен признать, что до написания этого исследования я был скорее критически настроен к наследованию. Я пытался избегать наследования, особенно множественного, любой ценой. Но мир не состоит только из черного и белого. И для библиотек компонентов, таких как Angular Material, использование множественного наследования через миксины имеет смысл.
Я бы не стал злоупотреблять этим. Если вам нужно гораздо более динамичное поведение в компонентах, я бы стремился к «Составлению вместо наследования». Составление допускает взаимозаменяемое поведение во время выполнения и придерживается принципа Open closed.
Оставайтесь на связи
Следующий пост будет на тему Миксины vs компонентов высшего порядка. Если я выдержу графики, он выйдет в июне 2019 года. Шучу Ознакомьтесь с полным исходным кодом примера на Github.
Автор: Christian Janker
Источник: https://codeburst.io/
Редакция: Команда webformyself.