Рефакторинг Угловые компоненты от многих входов/выходов до одного объекта конфигурации

Мои компоненты часто начинаются с нескольких @Input и @Output. Когда я добавляю свойства, кажется более чистым переключиться на один объект конфигурации в качестве входных данных.

Например, здесь компонент с несколькими входами и выходами:

export class UsingEventEmitter implements OnInit {
    @Input() prop1: number;
    @Output() prop1Change = new EventEmitter<number>();
    @Input() prop2: number;
    @Output() prop2Change = new EventEmitter<number>();

    ngOnInit() {
        // Simulate something that changes prop1
        setTimeout(() => this.prop1Change.emit(this.prop1 + 1));
    }
}

И его использование:

export class AppComponent {
    prop1 = 1;

    onProp1Changed = () => {
        // prop1 has already been reassigned by using the [(prop1)]='prop1' syntax
    }

    prop2 = 2;

    onProp2Changed = () => {
        // prop2 has already been reassigned by using the [(prop2)]='prop2' syntax
    }
}

Шаблон:

<using-event-emitter 
    [(prop1)]='prop1'
    (prop1Change)='onProp1Changed()'
    [(prop2)]='prop2'
    (prop2Change)='onProp2Changed()'>
</using-event-emitter>

По мере роста количества свойств кажется, что переход на один объект конфигурации может быть более чистым. Например, здесь компонент, который принимает один объект конфигурации:

export class UsingConfig implements OnInit {
    @Input() config;

    ngOnInit() {
        // Simulate something that changes prop1
        setTimeout(() => this.config.onProp1Changed(this.config.prop1 + 1));
    }
}

И его использование:

export class AppComponent {
    config = {
        prop1: 1,

        onProp1Changed(val: number) {
            this.prop1 = val;
        },

        prop2: 2,

        onProp2Changed(val: number) {
            this.prop2 = val;
        }
    };
}

Шаблон:

<using-config [config]='config'></using-config>

Теперь я могу просто передать ссылку на объект конфигурации через несколько слоев вложенных компонентов. Компонент с использованием config будет вызывать обратные вызовы, такие как config.onProp1Changed(...), что заставляет объект конфигурации выполнять переназначение нового значения. Похоже, что мы все еще имеем односторонний поток данных. Плюс добавление и удаление свойств не требует изменений в промежуточных слоях.

Есть ли недостатки в наличии одного объекта конфигурации в качестве входа в компонент, вместо того, чтобы иметь несколько входов и выходов? Будет ли избежать @Output и EventEmitter как это вызывает проблемы, которые могут догнать мне позже?

Ответ 1

Я бы сказал, что можно использовать одиночные объекты конфигурации для Input но вы должны все время придерживаться Output. Input определяют, что ваш компонент требует извне, и некоторые из них могут быть необязательными. Тем не менее, Output являются полностью компонентом бизнеса и должны быть определены внутри. Если вы полагаетесь на то, что пользователи передают эти функции, вам нужно либо проверить undefined функции, либо просто вызывать функции, как будто они ВСЕГДА передаются в конфигурации, что может быть неудобно для использования вашего компонента, если слишком много событий определить, даже если пользователь не нуждается в них. Поэтому всегда определяйте свой Output в вашем компоненте и испускайте все, что вам нужно. Если пользователи не связывают функцию с этим событием, это нормально.

Кроме того, я думаю, что наличие единой config для Input не является лучшей практикой. Он скрывает реальные входные данные, и пользователям, возможно, придется заглянуть внутрь вашего кода или документов, чтобы выяснить, что они должны передать. Однако, если ваш Input определен отдельно, пользователи могут получить некоторую интеллектуальную значимость с помощью таких инструментов, как Language Service.

Кроме того, я думаю, что это может нарушить стратегию обнаружения изменений.

Давайте посмотрим на следующий пример

@Component({
    selector: 'my-comp',
    template: '
       <div *ngIf="config.a">
           {{config.b + config.c}}
       </div>
    '
})
export class MyComponent {
    @Input() config;
}

Давай использовать это

@Component({
    selector: 'your-comp',
    template: '
       <my-comp [config]="config"></my-comp>
    '
})
export class YourComponent {
    config = {
        a: 1, b: 2, c: 3
    };
}

И для отдельных входов

@Component({
    selector: 'my-comp',
    template: '
       <div *ngIf="a">
           {{b + c}}
       </div>
    '
})
export class MyComponent {
    @Input() a;
    @Input() b;
    @Input() c;
}

И давайте использовать этот

@Component({
    selector: 'your-comp',
    template: '
       <my-comp 
          [a]="1"
          [b]="2"
          [c]="3">
       </my-comp>
    '
})
export class YourComponent {}

Как я уже говорил выше, вы должны взглянуть на код YourComponent чтобы увидеть, какие значения вы передаете. Кроме того, вы должны везде вводить config чтобы использовать эти Input. С другой стороны, вы можете лучше видеть, какие значения передаются во втором примере лучше. Вы даже можете получить немного intellisense, если вы используете Language Service

Другое дело, что второй пример будет лучше масштабировать. Если вам нужно добавить больше Input, вы должны все время редактировать config которая может нарушить работу вашего компонента. Тем не менее, во втором примере легко добавить другой Input и вам не нужно трогать рабочий код.

И последнее, но не менее важное: вы не можете обеспечить двустороннюю привязку своим способом. Вы, вероятно, знаете, что если у вас во Input data а в Output data dataChange, потребители вашего компонента могут использовать синтаксис двустороннего связывания и простой тип

<your-comp [(data)]="value">

Это обновит value родительского компонента, когда вы отправляете событие, используя

this.dataChange.emit(someValue)

Надеюсь, что это проясняет мое мнение об одном Input

редактировать

Я думаю, что есть правильный случай для одного Input который также имеет определенную function внутри. Если вы разрабатываете что-то вроде компонента диаграммы, который часто требует сложных опций/конфигов, на самом деле лучше иметь один Input. Это потому, что входные данные устанавливаются один раз и никогда не меняются, и лучше иметь параметры вашей диаграммы в одном месте. Кроме того, пользователь может передать некоторые функции, которые помогут вам нарисовать легенды, всплывающие подсказки, надписи по оси X, надписи по оси Y и т.д. Например, для этого случая было бы лучше иметь такой ввод

export interface ChartConfig {
    width: number;
    height: number;
    legend: {
       position: string,
       label: (x, y) => string
    };
    tooltip: (x, y) => string;
}

...

@Input() config: ChartConfig;

Ответ 2

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

1- Труднее узнать, что должно быть внутри входов и выходов, например: (рассмотрим компонент с html входным элементом и метками)

представьте, если у вас есть только 3 из этого компонента, и вы должны вернуться к работе над этим проектом через 1 или 2 месяца, или кто-то еще будет сотрудничать с вами или использовать ваш код !. это действительно трудно понять ваш код.

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

3- сложнее отслеживать переменные и отлаживать. Сам по себе angular содержит запутанные ошибки, которые трудно отладить, зачем мне это усложнять. Отслеживание и исправление любого неправильного ввода или вывода по одному для меня легче, чем делать это в одной переменной конфигурации, в которой находится куча данных.

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

Обновление: я использую этот метод для однократного ввода и без изменений данных (например, метка)

@Component({
selector: 'icon-component',
templateUrl: './icon.component.html',
styleUrls: ['./icon.component.scss'],
inputs: ['name', 'color']
});

export class IconComponent implements OnInit {
 name: any;
 color: any;

 ngOnInit() {
 }
}

Html:

<icon-component name="fa fa-trash " color="white"></icon-component>

с помощью этого метода угловые не будут отслеживать любые изменения внутри вашего компонента или снаружи. но с помощью метода @input, если ваша переменная изменится в родительском компоненте, вы также получите изменение внутри компонента.

Ответ 3

  • Смысл Input помимо его очевидной функциональности, состоит в том, чтобы сделать ваш компонент декларативным и легким для понимания.

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

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

  • Вы идете в обратном направлении и думаете, как работали плагины jQuery, когда вы вызываете функцию с именем init а затем вы предоставляете целую кучу конфигурации, которую вы даже не помните, должны ли вы предоставить или нет, и затем вы продолжаете копировать, вставляя этот неизвестный и постоянно растущий объект в ваши компоненты, где они, вероятно, даже не нуждаются в них

  • Создание по умолчанию является extremley легко и ясно, с простым Input, тогда он становится немного грязным с объектами в созданном по умолчанию.

Если у вас слишком много похожих Input, Output s, вы можете рассмотреть их ниже:

1- Вы можете создать Base класс и поместить все ваши Input/Output, которые похожи, а затем расширить все ваши компоненты из него.

export class Base{
    @Input() prop1: number;
    @Output() prop1Change = new EventEmitter<number>();
    @Input() prop2: number;
    @Output() prop2Change = new EventEmitter<number>();
}

@Component({})
export class MyComponent extends from Base{
      constructor(){super()}
}

2- Если вам это не нравится, вы можете использовать композицию и создать многократно используемый mixin и применять все ваши Input/Output.

Ниже приведен пример функции, которую можно использовать для применения миксинов. ПРИМЕЧАНИЕ не обязательно может быть именно тем, что вы хотите, и вам нужно настроить его под свои нужды.

export function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      derivedCtor.prototype[name] = baseCtor.prototype[name];
    });
  });
}

А затем создайте свои миксины:

export class MyMixin{
    @Input() prop1: number;
    @Output() prop1Change = new EventEmitter<number>();
    @Input() prop2: number;
    @Output() prop2Change = new EventEmitter<number>();
}

applyMixins(MyComponent, [MyMixin]);

3- Вы можете иметь свойства по умолчанию для входных данных, поэтому вы можете переопределить их, только если вам нужно:

export class MyComponent{
    @Input() prop1: number = 10; // default 
}

Ответ 4

Есть ли недостатки в том, чтобы иметь один объект конфигурации в качестве входа для компонента, вместо того, чтобы иметь несколько входов и выходов?

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

Будет ли предотвращение @Output и EventEmitter, как это, вызвать какие-либо проблемы, которые могут настигнуть меня позже?

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

Обновление: после того, как вы снова взглянули на пример в своем посте, похоже, вы не имели в виду, что хотите напрямую работать с входной моделью, а передавать излучатели событий напрямую через объект конфигурации. Передача обратных вызовов через @input (что вы неявно делаете) также имеет свои недостатки, такие как:

  • Ваш компонент становится труднее понять и рассуждать (каковы его входные данные по сравнению с его выходными данными?)
  • больше не может использовать синтаксис банановых ящиков

Ответ 5

Если вы хотите связать входные параметры как объект, я бы предложил сделать это следующим образом:

export class UsingConfig implements OnInit {
    @Input() config: any;
    @Output() configChange = new EventEmitter<any>();


    ngOnInit() {
        // Simulate something that changes prop1
        setTimeout(() => 
          this.configChange.emit({
              ...this.config, 
              prop1: this.config.prop1 + 1
          });
        );
    }
}
  • Вы создаете новый объект конфигурации при изменении свойства.
  • Вы используете Output-Event для создания измененного объекта конфигурации.

Обе точки гарантируют, что ChangeDetection будет работать правильно (при условии, что вы используете более эффективную стратегию OnPush). Плюс проще следовать логике в случае отладки.

Изменить: здесь очевидная часть внутри родительского компонента.

Шаблон:

<using-config [config]="config" (configChange)="onConfigChange($event)"></using-config>

Код:

export class AppComponent {
    config = {prop1: 1};

    onConfigChange(newConfig: any){
      // if for some reason you need to handle specific changes 
      // you could check for those here, e.g.:
      // if (this.config.prop1 !== newConfig.prop1){...

      this.config = newConfig;
    }
  }