React基于Rxjs打造自己的状态管理库[Felix-Obs]

·  阅读 2641

前言/介绍

在我们使用传统的redux或mobx状态管理的过程中有很多副作用。实际项目过程中,我们往往需要的是一个简洁的轻量的状态管理工具。使用自带的Context或satae,功能又太过单一。对于事件处理或接口调用,rxjs可以很好的编排这些,让一些代码会看上去非常简洁。我个人是rxjs拥护者。我希望打造一款基于rxjs的单一源的状态管理库。但我不是重新开发它,我找到了一款很好的工具,Observable-Store。但这款库,并不能满足我全部需求,我们需要在此之上,做进一步的丰富。那么首先,我们先简单介绍一下Observable-Store这个库。

注意:看此文档前需要有一定的rxjs基础,我这里不讲rxjs相关知识。

Observable-Store 功能介绍

ObservableStore的原理很简单,借用他官方的一张图来说明一下。

ObservableStore.png 他有一个State源,分出很多个Service,都是操作这同一个State源。然后被多个组件调用和获取数据。

Feature:

  1. 可应用于目前流行的三大框架之中(angular,vue,react),并且支持TypeScript
  2. 状态数据是不可变的
  3. 单一数据流
  4. 可跟踪的状态变化历史
  5. 可简单易用的轻量级的应用
  6. 任何状态的改变都可以被订阅,还可以自定义一些订阅规则

Observable-Store是怎么实现单一数据源的呢

原理比较简单,从源码里面可看到,他有一个ObservableStoreBase的class,底层是基于Rx的BehaviorSubject去做数据响应式处理,而且在全局加载阶段就做了实例化。在ObservableStore这个核心类中可以看到,里面的setState和getState,最终都是调用的这个实例化的ObservableStoreBase里面的setStoreState和getStoreState. 而我们想做我们自己的自定义的State管理类,都需要继承于ObservableStore 。所以他可以轻易做到单一数据源。所以看起来如下图:

企业微信20210507063233.png

在React中如何使用Observable-Store

本人使用react较多,就拿React举例子吧,其他框架就不阐述了,大家自行看官方文档。

1 .首先我们要写一个Class继承于ObservableStore,然后要写构造方法。

class CustomersStore extends ObservableStore {
    constructor() {
        super({ trackStateHistory: true });
        //trackStateHistory为true代表可以跟踪状态的历史记录
    }
    
    fetchCustomers() {
        // 调用接口
        // 当然这里可以用第三方的(Axios, Ky, etc.)
        return fetch('/customers')
            .then(response => response.json())
            .then(customers => {
                this.setState({ customers }, 'GET_CUSTOMERS');
                return customers;
            });
    }

    getCustomers() {
        let state = this.getState();
        // pull from store cache
        if (state && state.customers) {
            return this.createPromise(null, state.customers);
        }
        // doesn't exist in store so fetch from server
        else {
            return this.fetchCustomers();
        }
    }

    getCustomer(id) {
        return this.getCustomers()
            .then(custs => {
                let filteredCusts = custs.filter(cust => cust.id === id);
                const customer = (filteredCusts && filteredCusts.length) ? filteredCusts[0] : null;                
                this.setState({ customer }, 'GET_CUSTOMER');
                return customer;
            });
    }

    createPromise(err, result) {
        return new Promise((resolve, reject) => {
            return err ? reject(err) : resolve(result);
        });
    }
}
export default new CustomersStore()
复制代码
  1. 然后在react组件中的useEffect中subscribe操作, stateChanged是rxjs的一个Observable对象。对于任何数据改变,只需要订阅stateChanged即可。下面代码就是关于在React的应用。
export const MyComponent = ()=>{
     const [customers,setCustomers] = useState([])
     useEffect(()=>{
         CustomersStore.getCustomers();//获取全量数据
         //订阅数据变化
         CustomersStore.stateChanged.subscribe(state=>setCustomers(state.customers))
     },[])
     return customers.map(customer=><div>{customer.name}</div>)
}

复制代码

在ObservableStore里面还有一个globalStateChanged, 这个是来自ObservableStoreBase的Observable对象,这个可以订阅全局的state改变。stateChanged只能订阅当前class所操作的state的改变。

ObservableStore的要求是每个功能组件都需要有相应的Store class(也可以叫Service)去支撑。 可以将一部分与数据打交道代码的业务逻辑放在里面。redux和mobx也都是类似的方式。

关于ObservableStore,我想改进的点。

  1. 既然有了历史追踪state记录, 但没有state回退和恢复操作(在编辑器应用中很方便)。
  2. 在react中,我们需要让useState操作用高阶函数封装起来。
  3. sate的缓存,我们希望能单独控制,并有一个缓存时间
  4. react的hooks很方便,我们可以开发一些强大的自定义的hooks,来使用state,替代原有的sate
  5. rxjs很强大了,我们是不是可以封装一个自己的EventEmit工具。
  6. 用了rxjs,我们是不是可以对调用接口做一些更丰富的封装,提供防抖或节流,接口出错,自动重试等功能

说干就干,下面我就讲讲我怎么实现以上功能的。

基于ObservableStore,打造属于自己的状态管理工具

先放我的Felix-Obs项目地址:https://github.com/tanghui315/felix-obs

1. state回退和恢复操作

如果开启了trackStateHistory为true,那么利用我的库就可以实现数据的撤销和恢复,具体怎么做?根据上面例子改造,将CustomersStore继承我库的FelixObservableStore, 请看下面代码演示

  class CustomersStore extends FelixObservableStore{
       //.....代码省略
  }
  const customerStore = new CustomersStore()
  customerStore.prevState() //将数据返回到上一个状态,如果到顶,什么都不做
  customerStore.nextState() //将数据恢复到下一个状态,如果到最后,什么都不做
复制代码

prevState 和 nextState 一样会触发subscribe的操作。

2. 利用高阶函数将useState封装起来

继承FelixObservableStore后,会有一个connect高阶函数,我们可以这样用这个函数将state数据注入到组件Props里面。将setSate操作不呈现给用户。具体代码是这样的。

 //高阶处理函数
 public connect(CMP: ComponentType<any>): FunctionComponent {
        return (props: unknown): JSX.Element => {
            const [state, setState] = useState(this.getState())
            useEffect(() => {
                const subject = this.stateChanged.subscribe(s => setState(s))
                return function () {
                    subject.unsubscribe()
                }
            }, [])
            return useMemo(() => <CMP {...props} state={{ ...state }} />, [state])
        }
    }


复制代码

比如上面第一个例子的组件,我们可以这样使用

export const MyComponent = CustomersStore.connect(({ state })=>{
     useEffect(()=>{
         CustomersStore.getCustomers();//获取全量数据
     },[])
     return state.customers.map(customer=><div>{state.customer.name}</div>)
})
复制代码

这样代码是不是简洁了很多。当然这个地方可以改进,传入对象的key值,选择性的带入进来。

3. sate的缓存,我们希望能单独控制,并有一个缓存时间

FelixObservableStore自己独立封装一个dispatch 和 dispatchWithTimerClean这两个修改数据的方法,dispatchWithTimerClean会对定时清楚数据的缓存。比如我们希望1分钟后清除缓存。那么就这么操作:

 //dispatchWithTimerClean(key: string, state: T, cleanTime: number)
 //key 表示对象key
 //state 表示要修改的数据
 //cleanTime 表示缓存过期的时间,单位秒
 CustomersStore.dispatchWithTimerClean("customer",state,60)
复制代码

4. 自定义useObservableStore hooks函数

对于一些轻量级的组件,我们希望像useState一样方便的使用我们的observable对象。并且同时保持唯一数据源,任何地方都可以获取和修改。应该怎么做呢,我这边先把代码贴出来再细讲。

/**
 * @description: 自定义的hook函数,可以基于Rx的stream的数据处理,也可用于状态管理
 * @param {T} initState 初始化的状态数据值
 * @param {obsFunc} additional 额外的observalbe可观察的对象方法,这里可以做使用rxjs操作方法,对数据进行细粒度的控制
 * @param {customKey} 自定义的key,可选参数,如果不穿,使用系统随机生成的key
 * @return {*} 返回一个数组,保持与useState操作的一致性
 */
function useObservableStore<T>(initState: T, additional?: obsFunc<T> | null, customKey?: string): [T, (state: T) => void, string] {
    const KEY = useConstant(() => customKey ? customKey : Math.random().toString(36).slice(-8))
    const [state, setState] = useState(initState)
    const store = useConstant(() => new FelixObservableStore(KEY, initState))
    const $input = new BehaviorSubject<T>(initState)
    useEffect(() => {
        let customSub: Subscription
        if (additional) {
            customSub = additional($input).subscribe(state => {
                state && store.dispatch(KEY, state)
            })
        }
        const subscription = store.stateChanged.subscribe(state => {
            state && setState(state[KEY])

        })
        return function () {
            subscription.unsubscribe()
            customSub && customSub.unsubscribe()
            $input.complete()
        }
    }, [])

    return [state, (state) => store.dispatch(KEY, state), KEY]
}

复制代码

为什么要有key的概念, 第一次看到也许有疑惑, 这样要说明一些。ObservableStore本质上是构建了一个大的对象树。相当一个map对象,通过key才能找到对应的state。 但每次修改和保存的数据都是维持的不可变性。但也可以不必这样,数据是响应式的。只要调了dispatch 都会触发数据的subscribe操作。 不需要对象之间的diff操作。 看下面使用例子:

import { from } from 'rxjs'
import { map, switchMap, delay } from 'rxjs/operators'

export const MyComponent = ()=>{
     //将useState换为useObservableStore
     //使用第二参数传递自己rx处理
     const [customers,setCustomers,key] = useObservableStore([],($obs) => $obs.pipe(
        delay(2000),// 延迟2秒输出,为了凸显loading效果
        switchMap(() => from(fech("get/customers")).pipe( //直接处理接口操作
          map(result => result.data)  // 提取接口data数据,传递到store中
        ))
      ))
   
     return customers.map(customer=><div>{customer.name}</div>)
}

复制代码

对于轻量级组件来说,我们不需构建独立的Store。 直接使用useObservableStore可以解决相应的数据问题。 而且第二参数,rxjs的observable对象函数很好用, 可以直接调用接口,会自动将数据绑定在key上,或者在这样也可以处理dom事件。有这个入口,会让hooks的功能变得非常灵活。

如果我有另外一个组件,可以通过key调用dispatch 或 使用setCustomers去修改数据。如果我们使用自定义的Key。那么可以不用传递setCustomers。 直接调用dispatch ,从而实现全场景的跨组件通信 。

5. 附加一个EventEmiter工具

发布订阅,在面临大型应用中是必不可少的功能。利用rxjs可以轻松实现这一点。FelixObservableStore库里面顺便提供了一个。使用示例如下:

FelixObservableStore.getEmitter.listen("event name",(data)=>{})  //监听某个事件
FelixObservableStore.getEmitter.emit("event name",data) //发送某个事件
FelixObservableStore.getEmitter.removeListen("event name") //删除单个监听事件
FelixObservableStore.getEmitter.dispose() //删除所有监听事件
复制代码

6. 对接口更加细粒度的控制

往往接口操作是使用状态管理场景最多的情况。我们需要独立一个封装,将一些接口常用的功能定义好,只需要传递响应的配置即可,不需要带给用户复杂感,轻量化的解决问题,同时给用户最大的自由度。

//定义配置类型
interface AjaxSetting {
    initData?: any,// 给接口数据一个初始化的值 
    debounceTimes?: number, //防抖配置  单位毫秒
    throllteTimes?: number,//节流配置 单位毫秒
    retryCount?: number, // 接口出错,重试的次数
    initialDelayTimes?: number, // 重试间隔延时, 单位毫秒
    fetchCacheTimes?: number //接口数据缓存时间,单位秒
}
//调用接口
//key 表示数据的键
//handler 一个返回promise的函数 
//isAuto  为true 表示交给FelixObservableStore处理subscribe,为false表示手动处理subscribe,返回的是一个observable对象
//setting 表示传递请求配置
public fetchData(key: string, handler : ajaxFunc<any>, isAuto: boolean = true, setting?: AjaxSetting) 

复制代码

以上就是定义fetchData的所需参数的基本配置和函数的定义,下面我们用上面例子试用一下:

//将上面例子改造一下
export const MyComponent =  CustomersStore.connect(({ state })=>{
      useEffect(()=>{
          CustomersStore.fetchData("customer",fech("get/customers"),true,{
              initData:[],
              debounceTimes:3000,
              retryCount:3,
              initialDelayTimes:1000,
              fetchCacheTimes:60
          })
          /* 
            //自己控制subscribe
            CustomersStore.fetchData("customer",fech("get/customers"),false,{
              initData:[],
              debounceTimes:3000,
              retryCount:3,
              initialDelayTimes:1000,
             }).subscribe(data=>{...})
          */
      },[])
   
     return state.customers.map(customer=><div>{state.customer.name}</div>)
}
复制代码

这样请求接口的功能是不是丰富很多。当然如果第3个参数为false,可以自己控制subscribe,可以非常灵活。

结语

虽然我已经将工具应用在实际项目中,但我太忙,平时没有时间维护这个工具。感兴趣的小伙伴们,可以一起维护,一起完善,欢迎大家积极的提交PR,我很容通过的,哈哈。 最后再次奉上Felix-Obs项目地址:https://github.com/tanghui315/felix-obs

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