Поиск…


Вступление

Ngrx - мощная библиотека, которую вы можете использовать с Angular2 . Идея состоит в том, чтобы объединить две концепции, которые хорошо сочетаются, чтобы иметь реактивное приложение с предсказуемым контейнером состояния : - [Redux] [1] - [RxJs] [2] Основные преимущества: - Совместное использование данных в приложении между вашими компонентами будет проще. Тестирование основной логики приложения состоит в проверке чистых функций без какой-либо зависимости от Angular2 (очень просто!) [1]: http://redux.js.org [2]: http: // reactivex. ю / rxjs

Полный пример: Вход / выход пользователя

Предпосылки

Эта тема не касается Redux и / или Ngrx:

  • Вы должны быть довольны Redux
  • По крайней мере, понять основы RxJs и наблюдаемого шаблона

Сначала давайте определим пример с самого начала и поиграем с некоторым кодом:

Как разработчик, я хочу:

  1. Имейте интерфейс IUser который определяет свойства User
  2. Объявите действия, которые мы будем использовать позже, чтобы манипулировать User в Store
  3. Определить начальное состояние UserReducer
  4. Создайте редуктор UserReducer
  5. Импортируйте наш UserReducer в наш основной модуль, чтобы создать Store
  6. Используйте данные из Store для отображения информации в нашем представлении

Предупреждение о спойлере : если вы хотите попробовать демо сразу или прочитать код, прежде чем мы начнем, вот Plunkr ( встроенный просмотр или просмотр ).

1) Определить интерфейс IUser

Мне нравится разделить мои интерфейсы на две части:

  • Свойства, которые мы получим с сервера
  • Свойства, которые мы определяем только для UI (например, при нажатии кнопки)

И вот интерфейс IUser мы будем использовать:

user.interface.ts

export interface IUser {
  // from server
  username: string;
  email: string;
  
  // for UI
  isConnecting: boolean;
  isConnected: boolean;
};

2) Объявить действия для манипулирования User

Теперь нам нужно подумать о том, какие действия должны выполнять наши редукторы .
Скажем здесь:

user.actions.ts

export const UserActions = {
  // when the user clicks on login button, before we launch the HTTP request
  // this will allow us to disable the login button during the request
  USR_IS_CONNECTING: 'USR_IS_CONNECTING',
  // this allows us to save the username and email of the user
  // we assume those data were fetched in the previous request
  USR_IS_CONNECTED: 'USR_IS_CONNECTED',

  // same pattern for disconnecting the user
  USR_IS_DISCONNECTING: 'USR_IS_DISCONNECTING',
  USR_IS_DISCONNECTED: 'USR_IS_DISCONNECTED'
};

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

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

  • Нажмите на кнопку
  • Компонент поймает событие и вызовет userService.login
  • Метод userService.login dispatch событие для обновления нашего свойства хранилища: user.isConnecting
  • Вызывается HTTP-вызов (мы будем использовать setTimeout в демо, чтобы имитировать поведение async )
  • Как только HTTP вызов будет завершен, мы отправим другое действие, чтобы предупредить наш магазин о том, что пользователь зарегистрирован

user.service.ts

@Injectable()
export class UserService {
  constructor(public store$: Store<AppState>) { }

  login(username: string) {
    // first, dispatch an action saying that the user's tyring to connect
    // so we can lock the button until the HTTP request finish
    this.store$.dispatch({ type: UserActions.USR_IS_CONNECTING });

    // simulate some delay like we would have with an HTTP request
    // by using a timeout
    setTimeout(() => {
      // some email (or data) that you'd have get as HTTP response
      let email = `${username}@email.com`;

      this.store$.dispatch({ type: UserActions.USR_IS_CONNECTED, payload: { username, email } });
    }, 2000);
  }

  logout() {
    // first, dispatch an action saying that the user's tyring to connect
    // so we can lock the button until the HTTP request finish
    this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTING });

    // simulate some delay like we would have with an HTTP request
    // by using a timeout
    setTimeout(() => {
      this.store$.dispatch({ type: UserActions.USR_IS_DISCONNECTED });
    }, 2000);
  }
}

3) Определите начальное состояние UserReducer

user.state.ts

export const UserFactory: IUser = () => {
  return {
    // from server
    username: null,
    email: null,

    // for UI
    isConnecting: false,
    isConnected: false,
    isDisconnecting: false
  };
};

4) Создайте редуктор UserReducer

Редуктор принимает 2 аргумента:

  • Текущее состояние
  • Action типа Action<{type: string, payload: any}>

Напоминание: редуктор должен быть инициализирован в какой-то момент

Поскольку мы определили состояние по умолчанию нашего редуктора в части 3), мы сможем использовать его так:

user.reducer.ts

export const UserReducer: ActionReducer<IUser> = (user: IUser, action: Action) => {
  if (user === null) {
    return userFactory();
  }
  
  // ...
}

Надеюсь, есть более простой способ написать, что, используя нашу factory функцию для возврата объекта и внутри редуктора, используйте значение параметров по умолчанию (ES6):

export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
  // ...
}

Затем нам нужно обработать все действия в нашем редукторе: СОВЕТ : используйте функцию ES6 Object.assign чтобы сохранить наше состояние неизменным

export const UserReducer: ActionReducer<IUser> = (user: IUser = UserFactory(), action: Action) => {
  switch (action.type) {
    case UserActions.USR_IS_CONNECTING:
      return Object.assign({}, user, { isConnecting: true });

    case UserActions.USR_IS_CONNECTED:
      return Object.assign({}, user, { isConnecting: false, isConnected: true, username: action.payload.username });

    case UserActions.USR_IS_DISCONNECTING:
      return Object.assign({}, user, { isDisconnecting: true });

    case UserActions.USR_IS_DISCONNECTED:
      return Object.assign({}, user, { isDisconnecting: false, isConnected: false });

    default:
      return user;
  }
};

5) Импортируйте наш UserReducer в наш основной модуль, чтобы создать Store

app.module.ts

@NgModule({
    declarations: [
    AppComponent
    ],
    imports: [
    // angular modules
    // ...

    // declare your store by providing your reducers
    // (every reducer should return a default state)
    StoreModule.provideStore({
        user: UserReducer,
        // of course, you can put as many reducers here as you want
        // ...
    }),

    // other modules to import
    // ...
    ]
});

6) Используйте данные из Store для отображения информации на наш взгляд

Теперь все готово с логической стороны, и нам просто нужно отобразить то, что мы хотим, в двух компонентах:

  • UserComponent : [Dumb component] Мы просто передадим объект пользователя из магазина с использованием свойства @Input и async pipe. Таким образом, компонент получит пользователя только после его доступности (и user будет иметь тип IUser а не тип Observable<IUser> !)
  • LoginComponent [Интеллектуальный компонент] Мы будем непосредственно вводить Store в этот компонент и работать только с user в качестве Observable .

user.component.ts

@Component({
  selector: 'user',
  styles: [
    '.table { max-width: 250px; }',
    '.truthy { color: green; font-weight: bold; }',
    '.falsy { color: red; }'
  ],
  template: `
    <h2>User information :</h2>

    <table class="table">
      <tr>
        <th>Property</th>
        <th>Value</th>
      </tr>

      <tr>
        <td>username</td>
        <td [class.truthy]="user.username" [class.falsy]="!user.username">
          {{ user.username ? user.username : 'null' }}
        </td>
      </tr>

      <tr>
        <td>email</td>
        <td [class.truthy]="user.email" [class.falsy]="!user.email">
          {{ user.email ? user.email : 'null' }}
        </td>
      </tr>

      <tr>
        <td>isConnecting</td>
        <td [class.truthy]="user.isConnecting" [class.falsy]="!user.isConnecting">
          {{ user.isConnecting }}
        </td>
      </tr>

      <tr>
        <td>isConnected</td>
        <td [class.truthy]="user.isConnected" [class.falsy]="!user.isConnected">
          {{ user.isConnected }}
        </td>
      </tr>

      <tr>
        <td>isDisconnecting</td>
        <td [class.truthy]="user.isDisconnecting" [class.falsy]="!user.isDisconnecting">
          {{ user.isDisconnecting }}
        </td>
      </tr>
    </table>
  `
})
export class UserComponent {
  @Input() user;

  constructor() { }
}

login.component.ts

@Component({
  selector: 'login',
  template: `
    <form
      *ngIf="!(user | async).isConnected"
      #loginForm="ngForm"
      (ngSubmit)="login(loginForm.value.username)"
    >
      <input
        type="text"
        name="username"
        placeholder="Username"
        [disabled]="(user | async).isConnecting"
        ngModel
      >
 
      <button
        type="submit"
        [disabled]="(user | async).isConnecting || (user | async).isConnected"
      >Log me in</button>
    </form>
 
    <button
      *ngIf="(user | async).isConnected"
      (click)="logout()"
      [disabled]="(user | async).isDisconnecting"
    >Log me out</button>
  `
})
export class LoginComponent {
  public user: Observable<IUser>;
 
  constructor(public store$: Store<AppState>, private userService: UserService) {
      this.user = store$.select('user');
  }
 
  login(username: string) {
    this.userService.login(username);
  }
 
  logout() {
    this.userService.logout();
  }
}

Поскольку Ngrx является слиянием концепций Redux и RxJs , RxJs может быть трудно понять входы. Но это мощный шаблон, который позволяет вам, как мы видели в этом примере, иметь реактивное приложение, и вы можете легко поделиться своими данными. Не забывайте, что есть Plunkr, и вы можете разветвить его, чтобы сделать свои собственные тесты!

Надеюсь, это было полезно, даже если тема довольно длинная, ура!



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow