Angular 中的状态管理

·  阅读 4437

Angular 的常见套路

大家知道前段框架一般都有自己的状态管理,ReactRedux, VueVuex。那么,Angular 或者 AngularJS 有没有自己的状态管理呢?当然你可以用 Ngrx/Store

其实个人认为,Angular/AngularJS 区别于其他框架的一个最大的不同就是:基于 DI 的 service。也就是,我们基本上,不实用任何状态管理工具,也能实现类似的功能。

在 Angular 的开发中,我们常见的套路是:

  • Service 封装 API (数据的获取和更新)
  • Component call service 负责获取和修改数据

这里其实只达成了两个共识,API 在 service 中,View 的逻辑在 Component 中。但是并没有像 React 一样,有明确的规定,数据逻辑层在哪里。往往就会堆叠在 Component 中。这样的缺点也是比较明显的,就是 Component 的重用变得困难,造成很多冗余代码。当然,另外一个副作用是让 Component 变得很大。

当然,我们所谓前端框架,现在是 MVVM 的结构,model 是数据,(servicecall API 返回的数据),view 是页面展示 HTML,view model 是驱动页面的数据,也就是 component class。这其实与我们常见的 MVC 结构不同,因为 Controlller 其实没有了,其实很大一部分原因是交给了状态管理工具处理了。

其实,很多项目都是把 service 误解了。包括我现在维护的项目,也是只把 service 用作一个 API 的包裹器,用来在 component 里面 call API 而已。而把逻辑都简单的堆叠到 Component 中。如果去看 Angular 对于 service 的介绍的话,就会发现大致有两种 service:data service 和 service,什么意思呢:

component -> data (service)  -> data-service (api)
复制代码

Component 通过 service 进行数据的获取和逻辑计算,service 通过 call data service 来修改更新 API。也就是说,component 是不应该直接 call api 来修改数据的。

中间这一层,可能是很多 Angular 项目都忽略的部分。而中间这个部分,其实就是我们所熟悉的所谓状态管理。而我们通过使用 Angular 默认的 RxJS 其实可以很容易实现一个简单的数据管理层。

通过 Service 实现一个数据管理层

为了区分,我们可以把数据管理层成为 state service,把 API 包裹层成为 data service,目录结构类似如下:

---- user.component.ts
---- user.component.html
---- user.state.service.ts
---- user.data.service.ts
---- user.state.module.ts
复制代码

当然,为了 user.state.service.ts 重用也可以抽离开来。受 Flex 的影响,我们可以选择做成类 Flex 的单向数据流:

  • State service 定义能够提供的 view state 结构
  • State service 提供可以修改 state 的方法,在外部无法直接修改 state (read only)
  • State service 提供 state 状态变化的通知

其实,可以很容易发现,这已经非常接近我们之前所说的状态管理了,细节就不讲了,这是我个人比较习惯的一种定义模式:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { User } from './type';

export interface UserState {
  users: User[];
}

@Injectable({
  providedIn: 'root'
})
export class UserStateService {
  userState$: Observable<UserState>;

  private _userState$ = new BehaviorSubject<UserState>(null);
  constructor() {
    this.userState$ = this._userState$.asObservable();
  }

  get userState(): UserState {
    return this._userState$.getValue();
  }

  addUser(user: User): void {
    const users = [
      ...this.userState.users,
      user
    ];

    this._userState$.next({
      users
    })
  }
}
复制代码

这里,当然还是有一些问题:

  • 如果 State 上有多个数据,实际上变成了任意一个数据的更新,都造成的整个 state 的更新,当然我们可以把 state 拆的尽可能小,而且,就算是整个 State 的更新,跟 Angular 的脏检查相比,也经济很多。
  • 如何优雅的处理异步
  • 虽然是一个套路的写法,但是常常需要自己定义很多变量,写重复的逻辑,有没有可能通过一个库来实现。

关于第一个问题,如何拆分 state?

熟悉 NgRx 的话应该只有,有个叫做 Selector 的东西,其实也很简单,比如说:

interface UserState {
  userNames: string[];
  foods: Food[];
}
复制代码

我只想关心,users 的变化,Foods 的变化我并不关心,其实也挺简单的:

userState$: Observable<UserState>;
//....
food$ = this.userState$.pipe(
  map(userState => userState.foods),
  distinctUntilChanges(),
)
//....

当然,你可以直接写个 method 会方便很多:
select(state: Observable<any>, selector: (state: any) => any): Observable<any> {
  return state.pipe(
    map(selector), 
    distinctUntilChanges()
  );
}

// 使用 select:
const foods$ = this.select(userState$, userState => userState.food);
复制代码

当然,这其实依托于我们的状态修改是,immutable,也就是每次状态的更新都不是修改状态本身,而是创建了一个新的状态,比如:

// Good to add new item:
state = {
  ...state,
  foods: [
    ...state.foods,
    new Food(),
  ]
}

// Bad to add new item
state.foods.push(new Food())
复制代码

如果你熟悉 Redux 的话应该对第一种写法不会太陌生,但是,说正的,真的很麻烦。当然你可以通过 immutable.js 或者 Immer 来避免用一堆 spread operator。

顺便提一下,得益于 Angular 的脏检查,其实很多时候,数据的变化我们其实也并不一定需要知道。。。当然,个人还是比较推崇能够通过 RxJS 尽可能的得知数据变化,从而我们可以减少对于脏检查的依赖。

第二个问题是如何处理异步?

最简单的思路当然是,随便你怎么处理都行。只需要记得,状态更新的时候,需要手动更新状态就行。这样,当然,是比较粗暴的。但是,但是,这已经是比大多数直接在 component 里面处理 call API 要优雅很多很多了。

如果你熟悉 Vuex 的话,它的处理其实要稍微进步一些,就是将方法分为,mutationsactionmutations 必须是纯同步方法,这样,你就可以把纯逻辑封装在 mutations 中,当需要使用异步的时候,才使用 action,而业务逻辑还是 call mutations

我们当然可以做类似的事情,区别只是,我们这样,只是一个口头的约定,没有办法通过框架来保证,也没有办法一眼看出来,哪些是同步方法,哪些是异步方法。(async await 当然是一种方法)

当然,讲了这么多,其实,这篇文章是为了引出我们接下来要介绍的内容:NgRx 的 componet store.

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改