DevGang
Авторизоваться

Шаблоны коммуникаций в Angular 

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

В этом посте я расскажу обо всех способах взаимодействия между компонентами. Вот темы, которые мы пройдем:

  • Пример проекта
  • Основы компонентов
  • Общение между родителями и потомками
  • Общение потомка с родителем
  • Связь одноуровневых компонентов с помощью сервисов
  • Связь с помощью EventEmitter
  • Связь с использованием @ViewChild декоратора
  • Общение с использованием контент-проекции
  • Связь с NGRX Store
  • Связь между модулями
  • Резюме
  • Заключение

Пример проекта

Вот пример проекта, поэтому вы можете увидеть все коммуникации в действии. Вы можете клонировать этот проект, чтобы протестировать его на своей машине.

git clone https://github.com/bbachi/angular-communication.git
cd angular-communication
npm install
npm start

Основы компонентов

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

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

Обычно у нас есть компоненты внутри компонентов, чтобы сформировать больший компонент, называемый составным. На следующей диаграмме у нас есть компоненты header, footer и dashboard внутри компонента app, который формирует модуль приложения.

Модуль

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

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   AppRoutingModule
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { }

Компонент

Каждый компонент имеет три важных файла: файл класса app.component.ts для логики, html-файл app.component.html для представления и файл CSS app.component.css для стилей.

import { Component } from '@angular/core';

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent {
 title = 'angular-communication';
}

Обратите внимание на selector в приведенный выше файле. Значение, app-root используется для размещения компонента в DOM следующим образом:

...
< body>
 
< /body>
...

Интерполирование

Интерполяция - это способ ввести какое-то значение в HTML. Мы внедряем title из класса компонента в HTML.

Welcome to {{ title }}!

Привязка свойств

Привязка свойств используется для отправки информации из класса компонента в представление в квадратных скобках []. В приведенном ниже примере мы передаем свойство title компоненту header.

Привязка событий

Привязка событий используется для отправки информации из класса представления в класс компонента с обычными скобками. (). В следующем примере мы передаем событие из компонента заголовка.

Разобравшись немного с основами, давайте погрузимся в модели коммуникации в Angular.

Общение родителей с потомками

Давайте посмотрим, как мы можем передавать информацию из родительского компонента в дочерний компонент. У нас есть заголовок, который будет передан из родительского компонента (app) в дочерний (header) компонента.
 

Мы определяем title в компоненте app, как показано ниже. Посмотрим, как мы передадим этот title компоненту header.

export class AppComponent {
  title = 'angular-communication';
}

У нас есть @Input() headerTitle в компоненте header, и мы используем привязку свойства [headerTitle] в компоненте app, чтобы передать title.

Welcome to App!
import { Component, OnInit, Input } from '@angular/core';

@Component({
 selector: 'app-header',
 templateUrl: './header.component.html',
 styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
 constructor() { }

 @Input() headerTitle: string;

 ngOnInit() {
 }

}

Итак, с помощью декоратора @Input() мы можем передавать данные дочернему компоненту.

Общение потомка с родителем

Давайте посмотрим, как мы передаем информацию из дочернего компонента в родительский.

Давайте добавим несколько ссылок в шапку (child). Когда вы нажмете на ссылку, название ссылки отображается в компоненте app (parent) под названием «Welcome to App» , Как показано ниже.

У нас есть декоратор @Output и класс EventEmitter для отправки информации из дочернего компонента в родительский.

Определим navOut и eventEmitter  - и при нажатии на ссылку мы отправляем значение.

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
 selector: 'app-header',
 templateUrl: './header.component.html',
 styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {

 constructor() { }
 @Input() headerTitle: string;
 @Output() navOut = new EventEmitter();

 onNavLink(linkName: string) {
    this.navOut.emit(linkName);
 }

 ngOnInit() {
 }
}

Когда событие генерируется, оно будет записано здесь в компоненте app, как показано ниже. Мы определяем метод onNavigation() в компоненте app для захвата испущенного значения и присваиваем его свойству linkName. Мы можем поместить это свойство linkName в файл HTML с интерполяцией {{ linkName }}.


export class AppComponent {
 title = 'angular-communication';
 linkName: string;

 onNavigation(navLink: string) {
   this.linkName = navLink;
 }
}

Связь одноуровневых компонентов с помощью сервисов

Компоненты на одном уровне называются родственными компонентами. В нашем примере Header и Footer являются родственными компонентами. В этом разделе давайте посмотрим, как мы можем общаться между одноуровневыми компонентами с помощью сервисов.

Если мы посмотрим на диаграмму ниже, мы увидим Subject в сервисах, которые получают данные от компонента Header. Компонент Footer подписывается на subject, определенный в сервисе, и получает данные, как только в Subject будет отправлены данные с помощью функции next.

Давайте посмотрим это в действии. У нас есть ссылки в Header и те же ссылки в Footer. Но у нас нет ссылки на dashboard в Footer, потому что пользователь не вошел в систему.

При нажатии на ссылку Login в заголовке пользователь входит в систему и ссылка Login меняется на Logout. Нам нужно связаться с Footer, чтобы он отображал ссылку на dashboard и Logout вместо Login. Ниже приведено состояние после нажатия на ссылку Login:

Вот пример нашего сервиса. Мы определяем параметр isUserLoggedIn и setUserLoggedIn метод:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
 providedIn: 'root'
})
export class AppService {
 constructor() { }
 public isUserLoggedIn = new Subject();

 setUserLoggedIn(loggedIn: boolean) {
   this.isUserLoggedIn.next(loggedIn);
 }
}

Вот компоненты Footer и Header. Я удалил другой код для краткости. Мы внедрили сервис в обоих компонентах и ​​используем его в компоненте Header для отправки данных a в Footer для получения данных.

export class FooterComponent implements OnInit {

 constructor(private appService: AppService) { }
 isUserLoggedIn: boolean;

 ngOnInit() {
   this.appService.isUserLoggedIn.subscribe((userLoggedIn: boolean) => {
       this.isUserLoggedIn = userLoggedIn;
   });
 }

}
export class HeaderComponent implements OnInit {

 constructor(private appService: AppService) { }
 loginLabel = 'login';

 onNavLink(linkName: string) {
   if (linkName === 'logout') {
     this.loginLabel = 'login';
     this.appService.setUserLoggedIn(false);
   } else if (linkName === 'login') {
     this.loginLabel = 'logout';
     this.appService.setUserLoggedIn(true);
   }
   this.navOut.emit(linkName);
 }

}

Вот шаблон Footer:

Связь с помощью EventEmitter

В приведенном выше разделе мы увидели, как мы можем общаться между одноуровневыми компонентами с помощью сервисов. Давайте посмотрим, как мы можем сделать то же самое с EventEmitter.

Если мы посмотрим на диаграмму ниже, мы отправим данные с помощью EventEmitter в компонент App. Мы устанавливаем свойства app с данными, полученными из Header. Как только свойства app установлены, Footer получает данные в качестве входных данных.

Заголовок отправляет событие setLoggedInFlag и loginFlag передается в Footer:

export class AppComponent {
 title = 'angular-communication';
 linkName: string;

 loginFlag: boolean;

 setLoggedInUser(loggedIn: boolean) {
   this.loginFlag = loggedIn;
 }
}

Вот компонент Footer с геттерами и сеттерами для @Input, чтобы найти изменения:

export class FooterComponent implements OnInit {

  constructor() { }
  _isUserLoggedIn: boolean;

  get isUserLoggedIn(): boolean {
     return this._isUserLoggedIn;
  }

 @Input()
 set isUserLoggedIn(value: boolean) {
     this._isUserLoggedIn = value;
 }

}

Связь с использованием декоратора @ViewChild

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

У нас есть родительский компонент ParentComponent и дочерний компонент ChildComponent. Мы хотим установить свойство title дочернего компонента, но по некоторым причинам заголовок доступен только в родительском компоненте - например, после выполнения вызова API.

Давайте посмотрим, как мы можем сделать это с декоратором @ViewChild. Вот родительский компонент, где мы можем получить доступ к заголовку свойства дочернего компонента с помощью @ViewChild, и нам нужно использовать метод жизненного цикла ngAfterViewInit для инициализации заголовка дочернего компонента.

// parent html
I am a Parent
// parent component class. removed everything for brevity export class ParentComponent implements OnInit, AfterViewInit { constructor() { } @ViewChild('child') childComp: ChildComponent; ngAfterViewInit() { this.childComp.title = 'title coming from parent'; } }

Вот дочерний компонент:

// child html
{{title}}
// child component class export class ChildComponent implements OnInit { constructor() { } title = 'I am a child'; ngOnInit() { } }

Когда мы размещаем parent компонент в компоненте app:

Это один из вариантов использования декоратора @ViewChild .

Общение с использованием контент-проекции

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


 Welcome to APP!

Это код для app-welcome-message компонента HTML:

Мы можем спроектировать контент на компоненты с помощью ng-content:

Page: {{linkName}}

Вот вывод в браузере:

Связь с NGRX Store

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

Посмотрите на диаграмму ниже: есть умные и глупые компоненты. Умные компоненты - это те, которые знают о сторе, подписываются на него, получают данные и передают их глупые компонентам. С другой стороны, глупые компоненты - это компоненты представления, которые получают данные с помощью декоратора Input и отправляют данные с помощью EventEmitter.

Связь с NGRX Store

Связь с NGRX Store в деталях

Давайте рассмотрим наиболее важные аспекты того, как данные на самом деле передаются в NGRX Store.

Умные компоненты - это те, которые подписываются на хранилище, отправляют action, получают новое состояние. Как вы можете видеть на этой диаграмме:

Поток данных с NGRX Store

Action - это просто объекты JavaScript, содержащие type и payload. Вот пример:

{
 "type":"LOGIN",
 "payload": data
}

Action Creators - это функции javascript, которые принимают действие в качестве входных данных и выполняют соответствующее действие, такое как вызов reducers или вызов API через NGRX effects.

import { Action } from '@ngrx/store';

export const LOGIN = '[User] Login'
export const LOGIN_SUCCESS = '[User] Login Success'
export const LOGIN_FAILURE = '[User] Login Failure'

export class Login implements Action {
   readonly type = LOGIN;
   constructor(public payload:any) {}
}

export class LoginSuccess implements Action {
   readonly type = LOGIN_SUCCESS;
   constructor(public payload:any) {}
}

export class LoginFailure implements Action {
   readonly type = LOGIN_FAILURE;
   constructor(public payload:any) {}
}

export type Actions =
  | Login
  | LoginSuccess
  | LoginFailure

Reducers - это чистые функции, которые принимают action и payload в качестве аргументов и возвращают новое состояние.

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

import * as loginTypes from '../actions/login';

export interface State {
   isLoading: boolean;
   isLoadingSuccess: boolean;
   login: any;
}

const initialState: State = {
   isLoading: false,
   isLoadingSuccess: false,
   login: undefined,
};

export function reducer(state= initialState, action: loginTypes.Actions): State {

   switch(action.type) {

       case formTypesLOGIN: return {...state,isLoading: true,isLoadingSuccess: false,login:undefined};
       case formTypes.LOGIN_SUCCESS: return {...state,isLoading: false,isLoadingSuccess: true,login: action.payload};
       case formTypes.LOGIN_FAILURE: return {...state,isLoading: false,isLoadingSuccess: true,login: action.payload};

       default: return state
   }
}

export const getLogin = (state: State) => {
   return {isLoading:state.isLoading, isLoadingSuccess:state.isLoadingSuccess, login:state.login}
}

Вот как мы подписываемся на хранилище из компонентов - мы должны внедрить хранилище в конструктор компонента. Мы можем отправить действие с помощью метода this.store.dispatch и получить данные из хранилища с помощью селекторов, this.store.select.

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { Store } from '@ngrx/store';
import * as fromRoot from '../app-state/reducers';
import * as loginTypes from '../app-state/actions/login';

@Component({
   templateUrl: './login.component.html',
   styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
   constructor(private store: Store) {
       this.downloadRestUrl = this.appConfig.readEnvironment('downloadRestUrl');
   }

   ngOnInit() {
       this.store.dispatch(new loginTypes.Login({user:this.userId}))
       this.forms$ = this.store.select(fromRoot.getFormsList);
   }
}

Связь между модулями

Все приложение Angular разделено на модули, основанные на функциях приложения. Иногда нам также необходимо общение между модулями.

Есть несколько способов сделать это.

NGRX Store

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

Посмотрите на диаграмму ниже. Мы можем отправить действие из компонента A в Модуль A с данными, чтобы внести изменения в состояние NGRX. Компонент B из модуля B подписывается на хранилище и получает эти данные по мере изменения состояния.

Связь между модулями с NGRX Store

Общий модуль

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

Связь между модулями с помощью общего модуля

Резюме

  • Используйте декоратор @Input() для общения между родителями и потомками.
  • Используйте декоратор @Output() и EventEmitter для общения между потомками и родителями.
  • Сервис и EventEmitter два способа, которыми компоненты на одном уровне могут общаться.
  • Мы можем получить доступ к дочернему компоненту, запросив шаблон с @ViewChild()
  • Мы можем получить доступ к нескольким дочерним компонентам с @ViewChildren()
  • Всегда рекомендуется использовать управление состоянием NGRX для однонаправленного потока данных, так как проект со временем становится все больше и больше.
  • Мы можем создать общий модуль для облегчения связи между модулями.
  • Кроме того, мы можем использовать хранилище NGRX для связи между модулями.

Заключение

Поток данных важен в одностраничных приложениях. Понимание этих шаблонов коммуникации имеет решающее значение для успеха вашего проекта. Без этих базовых концепций вы получите «спагетти код»: его трудно отлаживать, читать и расширять.

Перевод статьи: Communication Patterns in Angular

#Angular #Начинающим
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Освой перспективную онлайн профессию!

Получить скидку