完成示例 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 ,你可以将其分叉以进行自己的测试!

我希望即使这个话题很长,也很有帮助!