前言/介绍
在我们使用传统的redux或mobx状态管理的过程中有很多副作用。实际项目过程中,我们往往需要的是一个简洁的轻量的状态管理工具。使用自带的Context或satae,功能又太过单一。对于事件处理或接口调用,rxjs可以很好的编排这些,让一些代码会看上去非常简洁。我个人是rxjs拥护者。我希望打造一款基于rxjs的单一源的状态管理库。但我不是重新开发它,我找到了一款很好的工具,Observable-Store。但这款库,并不能满足我全部需求,我们需要在此之上,做进一步的丰富。那么首先,我们先简单介绍一下Observable-Store这个库。
注意:看此文档前需要有一定的rxjs基础,我这里不讲rxjs相关知识。
Observable-Store 功能介绍
ObservableStore的原理很简单,借用他官方的一张图来说明一下。
他有一个State源,分出很多个Service,都是操作这同一个State源。然后被多个组件调用和获取数据。
Feature:
- 可应用于目前流行的三大框架之中(angular,vue,react),并且支持TypeScript
- 状态数据是不可变的
- 单一数据流
- 可跟踪的状态变化历史
- 可简单易用的轻量级的应用
- 任何状态的改变都可以被订阅,还可以自定义一些订阅规则
Observable-Store是怎么实现单一数据源的呢
原理比较简单,从源码里面可看到,他有一个ObservableStoreBase的class,底层是基于Rx的BehaviorSubject去做数据响应式处理,而且在全局加载阶段就做了实例化。在ObservableStore这个核心类中可以看到,里面的setState和getState,最终都是调用的这个实例化的ObservableStoreBase里面的setStoreState和getStoreState. 而我们想做我们自己的自定义的State管理类,都需要继承于ObservableStore 。所以他可以轻易做到单一数据源。所以看起来如下图:
在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()
- 然后在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,我想改进的点。
- 既然有了历史追踪state记录, 但没有state回退和恢复操作(在编辑器应用中很方便)。
- 在react中,我们需要让useState操作用高阶函数封装起来。
- sate的缓存,我们希望能单独控制,并有一个缓存时间
- react的hooks很方便,我们可以开发一些强大的自定义的hooks,来使用state,替代原有的sate
- rxjs很强大了,我们是不是可以封装一个自己的EventEmit工具。
- 用了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