完成示例 Loginlogout 使用者

先決條件

這個主題不是關於終極版和/或 NGRX:

  • 你需要對 Redux 感到滿意
  • 至少要了解 RxJs 和 Observable 模式的基礎知識

首先,讓我們從一開始就定義一個示例並使用一些程式碼:

作為開發人員,我想:

  1. 有一個 IUser 介面,用於定義 User 的屬性
  2. 宣告我們稍後將用於操作 Store 中的 User 的動作
  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 來模擬非同步行為
  • 一旦 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

reducer 有兩個引數:

  • 目前的狀態
  • Action<{type: string, payload: any}>Action

提醒: 需要在某個時刻初始化 reducer

當我們在第 3 部分中定義了 reducer 的預設狀態時,我們將能夠像這樣使用它:

user.reducer.ts

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

希望有一種更簡單的方法來編寫它,通過使用我們的 factory 函式返回一個物件,在 reducer 中使用(ES6) 預設引數值

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

然後,我們需要處理 reducer 中的每個動作: 提示 :使用 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[啞元件] 我們將使用 @Input 屬性和 async 管道從商店傳遞使用者物件。這樣,元件只有在可用時才會收到使用者(而 user 的型別為 IUser 而不是 Observable<IUser>!)
  • LoginComponent [智慧元件] 我們將 Store 直接注入此元件,僅作為 Observable 使用 user

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();
  }
}

由於 NgrxReduxRxJs 概念的合併,因此在開始時很難理解這些問題。但這是一個強大的模式,允許你像我們在此示例中看到的那樣擁有一個*被動應用程式,*並且你可以輕鬆地共享你的資料。不要忘記有一個可用的 Plunkr ,你可以將其分叉以進行自己的測試!

我希望即使這個話題很長,也很有幫助!