NgRx Component Store 尝试,真的很香

1,073 阅读2分钟

最近在开始尝试将 Angular 中的逻辑抽离到一个中间 service 层,让纯 service 彻底变成 data service 也就是 API 的 wrapper。

然后就在查资料的时候发现,NgRx 新出的 component store,使用了一把以后确实是真香。下面就简单的介绍一下。

首先,相信大家对 NgRx 的了解可能都是从 NgRx/Store 开始的,也就是 Angular 中对于 Redux 实现。对于 NgRx 的理解,官方的解释是:Angular Reactive Extension。之前我一直调侃,应该解释成:Angular Redux 更加贴切一些。

不过,通过后来的 NgRx/Component 和刚刚推出的 NgRx/Component-store 确实能够看出来,NgRx 正在帮助 Angular 往更加 Reactive 的路上,越走越远。

其实 Component Store 跟我们之前聊过的 state service 是非常类似的。大家知道,NgRx 本身是提供了 NgRx/Store 这种状态管理模式的。

但是一个显著的问题是,Redux 是更加推荐全局状态管理的,虽然我们也能实现局部的状态管理,但是依然是 feature module 层面的,对项目的侵入性是比较大的。这对于一个项目或者大的 feature module 来说,是比较大的 desision。比如项目一开始就是就没有引入状态管理,项目负责的人员也无意于引入。

作为一个小程序猿,我们只想在新写的 feature 中,局部性的引入状态管理,把之前的业务逻辑从 component 中抽离开来,我们之前讲的 state service 会是一个比较不错的选择。而 component-store 相当于,帮助你提供了更加明确的规范和工具,让你更加简单直白的做局部状态管理。然而其本质上其实还是一个 service。

还记得我们之前设计的 state service,其实存在两个显著的问题:

  • 如何对 observable 进行拆分和组合
  • 如何优雅的处理异步

其实基本的思路是一致的,就是 service 统一暴露用户不能直接修改的 observable,和能够修改数据的 methods。

class StoreService {
  users$: Observable<User[]>;
  
  addUser(user) {
    //....
  }
}

这样,只需要永远监听 state 就可以了。component-store 采用了更加通用的方法,就是,直接定义一个 State 接口,数据都包裹在 State 中:

export interface UserState {
  users: User[];
  currentUserId: string;
}
class StoreService {
  userState$: Observable<UserState>;
  
  addUser(user) {
    //....
  }
}

那么,我们如何拆分 state 呢,比如,我只想得到一个关于 users 的 observable,当然直接用 map 也很简单:

readonly users$ = this.userState$.pipe(
  map(userState => userState.users), 
  distinctUntilChanges()
);
readonly currentUserId$ = this.userState$.pipe(
  map(userState => userState.currentUserId), 
  distinctUntilChanges()
);

相同的代码我们就可以定义一个 method 来处理:

select (state$, selectFun) {
  return this.userState$.pipe(
    map(userState => userState.currentUserId), 
    distinctUntilChanges(),
  )
}

readonly users$ = this.select(this.state$, (state) => state.users);
readonly currentUserId$ = this.select(this.state$, (state) => state.currentUserId);

当然,因为传入的参数是 observable 和一个 map 函数,其实也可以传入 select 后的结果,拓展一下 select 方法的话,不难实现如下的功能:

readonly currentUser$ = this.select(this.users$, this.currentUserId$, (users, currentUserId) => {
  return users.find(u => u.id === currentUserId);
})

其实,引入了一个简单实现的 select 函数,就简化了不少的 pipe, map, 逻辑上也变得更加容易理解。

接下来我们进入到第二个问题,component-store 是如何优雅的处理异步的呢?

我们之前聊过,method 本质上可以处理同步和异步的所有问题,那么我们该怎么处理同步和异步呢?其实这里很简单,method 永远只处理同步问题。而异步,交给 effect 处理。可以这么简单的说:

  • Method 会同步的修改 store 中某些数据的状态。
  • Store 会在 Effects 监听需要监听的数据状态,做异步处理,然后修改新的状态。 这样,我们本质上,讲跟 method 相关的异步操作,修改为跟数据状态相关的异步状态,method 仅仅成为了状态修改的触发器。(其实如果逻辑比较负责,我们甚至可以抽离到 reducer 来处理数据的逻辑状态变化)
addUser(user) {
  this.setState(this.state$, (state) => ({ 
    users: { ...this.state.users, user } 
  }));
}