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



Здесь я собираюсь дать еще четыре совета по повышению качества компонентов.

5. Уничтожить входные данные объекта.

Обертывание нескольких аргументов в объект - это практика классического JavaScript. Тем не менее, это антипаттерн для компонента Angular.

export interface Configuration {
  enableFilter: boolean;
  enableSort: boolean;
  ...
}
@Component({
  selector: 'custom-grid',
  ...
})
export class CustomGridComponent {
  @Input() configuration: Configuration;
...
}

Основная проблема заключается в том, что это создает дополнительную сложность и требует поддержки кода. Это также затрудняет установку значения по умолчанию. Кроме того, его неудобно использовать, поскольку разработчикам приходится объявлять дополнительные переменные в своих представлениях.

@Component({
  template: `
    <custom-grid [configuration]="gridConfiguration"></custom-grid>
  `,
  ...
})
export class TestComponent {
  gridConfiguration: Configuration = {
    enableFilter: true,
    enableSort: true
  }
  ...
}

Настоятельно рекомендуется всегда стараться деструктурировать входные данные объекта как можно более плоскими.

@Component({
  selector: 'custom-grid',
  ...
})
export class CustomGridComponent {
  @Input() enableFilter: boolean;
  @Input() enableSort: boolean;
...
}

Теперь разработчики могут:

<custom-grid 
  [enableFilter]="true" 
  [enableSort]="true"></custom-grid>

Это не только делает его намного проще, но также позволяет нам указывать значение по умолчанию для каждого атрибута отдельно, как то, что я упоминал ранее в советах №1.

6. Замените входную мутацию на выходную привязку.

Если вы ненавидите компанию, в которой работаете, и хотите облажаться перед уходом, одна из лучших ловушек, которую вы можете сделать, - это обновить связанный объект @Input():

@Component({
  selector: 'app-parent',
  template: `
    <div>Parent: {{ counterState | json }}</div>
    <app-child [state]="counterState"></app-child>
  `
})
export class ParentComponent {
  counterState = { counter: 0 };
}

@Component({
  selector: 'app-child',
  template: `
    <div>Child: {{ state | json }}</div>
    <button (click)="onUpdateCounter()">Update Counter</button>
  `
})
export class ChildComponent {
  @Input() state: { counter: number };

  onUpdateCounter() {
    this.state.counter++;
  }
}

Проблема обновления связанного объекта заключается в том, что это также повлияет на родительский объект. Однако нет никакой информации для других разработчиков, чтобы понять такое поведение при использовании компонента. Это может очень легко вызвать проблему, а также очень сложно отладить, поскольку логика обновления скрыта внутри компонента. Разработчики должны копаться в компонентах, которые они использовали, один за другим, прежде чем они наконец смогут узнать, где происходит обновление. Лучший подход - всегда использовать двустороннюю привязку всякий раз, когда есть обновление, которое выходит из компонента:

export interface CounterState {
  counter: number;
}

@Component({
  selector: 'app-parent',
  template: `
    <div>Parent: {{ counterState | json }}</div>
    <app-child [(state)]="counterState"></app-child>
  `
})
export class ParentComponent {
  counterState: CounterState = { counter: 0 };
}

@Component({
  selector: 'app-child',
  template: `
    <div>Child: {{ state | json }}</div>
    <button (click)="onUpdateCounter()">Update Counter</button>
  `
})
export class ChildComponent {
  @Input() state: CounterState;
  @Output() stateChange = new EventEmitter<CounterState>();

  onUpdateCounter() {
    this.stateChange.emit({
      ...this.state,
      counter: this.state.counter + 1
    });
  }
}

Для других разработчиков гораздо яснее, что компонент обновит связанную переменную, поскольку теперь это явно двусторонняя привязка (он заблокирует обновление, если они попытаются выполнить одностороннюю привязку с <app-child [state]=”counterState”></app-child>).

Кстати, гораздо безопаснее обновлять примитивный ввод, поскольку он не будет отражен в родительском элементе (@Input counter: number;). Тем не менее, по-прежнему настоятельно рекомендуется использовать двустороннюю привязку для всех случаев входной мутации для обеспечения согласованности данных и удобства обслуживания.

7. Извлечь вызовы API в компонент оболочки.

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

@Component({
  template: `
    <div class="grid">
      <div class="row">
        <div *ngFor="let product of products"
          class="col-6 col-md-4 mb-2">
          <div class="card bg-white" (click)="navigateTo(product)">
            <div class="card-header">
              <img [src]="product.img">
            </div> 
            <div class="card-body">
              <strong>{{product.name}}</strong> 
              <div>Brand: {{product.brand}}</div> 
              <div>Color: {{product.color}}</div> 
              <div>Style: {{product.style}}</div>
            </div> 
            <div class="card-footer text-center">
              <span class="price">${{product.price}}</span> 
            </div> 
          </div>
        </div>
      </div>
    </div>
  `,
  ...
})
export class ProductOverviewComponent {
  products: Product[] = [];
  
  constructor(productService: ProductService) {
    productService.getAllProducts()
      .subscribe(products => this.products = products);
  }
  
  ...
}

Мы можем извлечь вызовы API в компонент-оболочку, чтобы внутренний компонент можно было сохранить презентационным.

@Component({
  selector: 'product-grid',
  template: `
    <div class="grid">
      <div class="row">
        <div *ngFor="let product of products"
          class="col-6 col-md-4 mb-2">
          <div class="card bg-white" (click)="navigateTo(product)">
            <div class="card-header">
              <img [src]="product.img">
            </div>
            <div class="card-body">
              <strong>{{product.name}}</strong>
              <div>Brand: {{product.brand}}</div>
              <div>Color: {{product.color}}</div>
              <div>Style: {{product.style}}</div>
            </div>
            <div class="card-footer text-center">
              <span class="price">${{product.price}}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  `,
  ...
})
export class ProductGridComponent {
  @Input() products: Product[] = [];
  
  ...
}

@Component({
  template: `
    <product-grid [products]="products"></product-grid>
  `,
  ...
})
export class ProductOverviewComponent {
  products: Product[] = [];

  constructor(productService: ProductService) {
    productService.getAllProducts()
      .subscribe(products => this.products = products);
  }
}

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

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



8. Используйте подход одного файла для простых компонентов

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

@Component({
  template: `
    <div class="card">
      <div class="card-header">
        {{displayText}}
      </div>
    </div>
  `,
  styles: [`
    .card-header {
      color: green;
    }
  `]
})
export class CardComponent {
  ...
}

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

Спасибо за прочтение! Надеюсь, эта статья окажется для вас полезной. Будем очень признательны за любые комментарии. : D