前言
又是崭新的一周,怀着愉快的心情来到了公司,部门经理看到我就很愉快的告诉我有两个好消息。第一我们又开了一个新项目,还是交给你来造~。干,说好愉快的摸鱼呢。第二个就是这个项目技术框架定为React。再干,上上个项目是Angular,上一个是Vue,现在又变成了React,后面还会接着搞svelte。说好的不忘初心呢。
随着愉悦的心情中一阵狂撸~
我突然想到一个问题,因为每个项目都用到了数据管理,比如Angular一般用NgRx
或NGXS
,Vue的话会使用Vuex
或者pinia
,如果是React
则会使用Redux
或MboX
,那么有没有一个响应数据存储让三个框架都可以使用,而不用在不同框架的项目中来回切换呢.
那么问题来了,如果需要在各个框架中都可以正常使用,那么首先就是需要它本身不依赖于运行的框架,我发现基于RxJS
的响应式数据存储方案就是可以合适的选择,因为是基于RxJS
的实现,那么当然可以在Vue
,React
以及Angular
(这个自不必说)中良好的运行.
所以问题不大,功夫不负有心人,在一番查找后我发现Akita
就很好的满足了我的需求,那么现在就先来瞅瞅这只秋田犬
吧.
Akita
Akita是一个基于RxJS
的响应式数据存储工具,它主要分为Store
,Service
,Query
三部分.
Store
Store
部分是是来定义数据结构以及初始的默认值,类似Vuex
和redux
中的State
import { Store, StoreConfig } from '@datorama/akita';
export interface SessionState {
token: string;
name: string;
}
export function createInitialState(): SessionState {
return {
token: '',
name: ''
};
}
@StoreConfig({ name: 'session' })
export class SessionStore extends Store<SessionState> {
constructor() {
super(createInitialState());
}
}
Service
Service
部分是负责修改和操作数据的部分,通过对应公开的方法,来帮助我们实现对数据的修改和操作,也是类似于Vuex
中Action
和Mutation
的部分
import { SessionStore } from './session.store';
export class SessionService {
constructor(private sessionStore: SessionStore) {}
updateUserName(newName: string) {
this.sessionStore.update({ name: newName });
}
}
在Service
中通过访问Store
,使用update
函数实现对数据的修改.
Query
Query
部分是负责数据的获取和查询,在Query
我们可以通过this.select
来获取Observable
类型的对象,也可以通过getValue
方法来获取实时数据进行使用.
import { Query } from '@datorama/akita';
import { SessionState } from './session.store';
export class SessionQuery extends Query<SessionState> {
allState$ = this.select();
isLoggedIn$ = this.select(state => !!state.token);
selectName$ = this.select('name');
// Returns { name, age }
multiProps$ = this.select(['name', 'age']);
// Returns [name, age]
multiPropsCallback$ = this.select(
[state => state.name, state => state.age]
)
constructor(protected store: SessionStore) {
super(store);
}
}
不仅可以直接通过this.select
获取store中的数据,我们也可以通过RxJS
的Pipe
来组合我们需要的数据信息,或者直接使用TypeScript
的get
属性来获取数据.
import { Query } from '@datorama/akita';
import { SessionState } from './session.store';
export class SessionQuery extends Query<SessionState> {
constructor(protected store: SessionStore) {
super(store);
}
getTargetUser(){
return this.select(['age','name]).pipe(
// 目标选择操作
)
}
get isLoggedIn() {
return !!this.getValue().token;
}
}
Store
,Service
,Query
即是Akita
的主要组成部分,我们通过这些就是实现我们需要的数据存储以及访问功能,下面我们来看看我们如何来集成进各种前端框架之中。
集成Akita
Akita
的支持度很好,其实官方文档中就提供了React
以及Sevlte
的集成方法,我也是参考这个来进行实现的.
Akita
提供的响应式数据都是基于Observable
的,我们需要做的就是将Observable
转换成我们需要的类型.
本质的实现都是我们创建一个数据源,然后监听RxJS
来动态修改我们的数据源即可实现,至于其他的操作目的是为了我们实现的更优雅一些.
Angular
Angular
可是亲儿子,一切看文档即可,自不必多说了。
React
按我们之前的理解我们很好实现对React
的使用.
function UseStoreSelect<T>(
steam: Observable<T>,
defaultValue?: T
) {
const [value, setValue] = useState<T | undefined>(
defaultValue
)
useEffect(() => {
const subscription = steam.subscribe(newValue => {
setValue(newValue)
})
return () => subscription.unsubscribe()
}, [])
return value
}
我们需要传入Observable
对象以及默认值,通过useEffect
来进行监听Observable
进行修改数据,使用时直接使用返回的数据即可.
const user = UseStoreSelect(userQuery.getUser())
但是这个在需要获取默认值时不算友好,而且需要在Query
来添加一些同步的处理,所以我们来小小的优化一下写法.
function UseStoreQuery<T, R>(
query: Query<T>,
project: (store: T) => R
): R
function UseStoreQuery<
T,
R extends [(state: R) => any] | Array<(state: R) => any>
>(query: Query<T>, selectorFns: R): ReturnTypes<R>
function UseStoreQuery<T, R>(query: Query<T>): R
function UseStoreQuery<T, R>(
query: Query<T>,
project?: ((store: T) => R) | ((state: T) => any)[]
) {
let steam: Observable<R | T | any[]>
let state: R | T | any[]
if (isFunction(project)) {
steam = query.select(project)
state = project(query.getValue())
} else if (Array.isArray(project)) {
steam = query.select(project)
state = project.map(p => p(query.getValue()))
} else {
steam = query.select()
state = query.getValue()
}
const [value, setValue] = useState<R | T | any[]>(state)
useEffect(() => {
const subscription = steam.subscribe(newValue => {
setValue(newValue)
})
return () => subscription.unsubscribe()
}, [])
return value
}
现在我们就可以不在Query
中创建的查询操作,直接传递表达式来使用
const user = UseStoreQuery(userQuery,store=>store.user)
这样还会自动的从Query
中取出对应的默认值,免得我们自己查询一次.
Vue
Vue
相比与React
的Hooks
,Vue3
中的Ref
其实会受到更小的限制,所以采用类似的方案即可.
function UseStoreQuery<T, R>(
query: Query<T>,
project: (store: T) => R
): R
function UseStoreQuery<
T,
R extends [(state: R) => any] | Array<(state: R) => any>
>(query: Query<T>, selectorFns: R): ReturnTypes<R>
function UseStoreQuery<T, R>(query: Query<T>): R
function UseStoreQuery<T, R>(
query: Query<T>,
project?: ((store: T) => R) | ((state: T) => any)[]
) {
let steam: Observable<R | T | any[]>
let state: R | T | any[]
if (isFunction(project)) {
steam = query.select(project)
state = project(query.getValue())
} else if (Array.isArray(project)) {
steam = query.select(project)
state = project.map(p => p(query.getValue()))
} else {
steam = query.select()
state = query.getValue()
}
const target = ref<R | T | any[]>(state)
const subscription = steam.subscribe(newValue => {
target.value = newValue
})
return target
}
当然也可以使用vue-rx
库,来方便我们使用Observable
对象.
不过Vue
和React
中并没有依赖注入的模块,需要在Store
,Service
,Query
中来手动的实现实例化.
// user.store.ts
...
export const userStore = new UserStore()
// user.query.ts
...
export const userQuery = new UserQuery(userStore)
// user.service.ts
...
export const userService = new UserService(userStore)
Svelte
Svelte与其他不同自带了store
的支持,在页面中也可以直接通过$
来直接从store
中取值,不过我们依然可以用Akita
来适配Svelte
,只需要使用hooks
的方式将Observable
转换成writeable
即可。
function UseStoreSelect<T>(steam: Observable<T>, defaultValue?: T) {
const value = writable<T | undefined>(defaultValue)
steam.subscribe((newValue) => {
value.set(newValue)
})
return value
}
同样创建UseStoreQuery
函数可以简化我们的基本操作,一切操作大同小异。
function UseStoreQuery<T, R>(
query: Query<T>,
project: (store: T) => R
): Writable<R>
function UseStoreQuery<
T,
R extends [(state: R) => any] | Array<(state: R) => any>
>(query: Query<T>, selectorFns: R): Writable<R>
function UseStoreQuery<T, R>(query: Query<T>): Writable<R>
function UseStoreQuery<T, R>(
query: Query<T>,
project?: ((store: T) => R) | ((state: T) => any)[]
) {
let steam: Observable<R | T | any[]>
let state: R | T | any[]
if (isFunction(project)) {
steam = query.select(project)
state = project(query.getValue())
} else if (Array.isArray(project)) {
steam = query.select(project)
state = project.map((p) => p(query.getValue()))
} else {
steam = query.select()
state = query.getValue()
}
const value = writable<R | T | any[]>(state)
steam.subscribe((newValue) => {
value.set(newValue)
})
return value
}
使用示例
实现了以上的集成我们就可以在项目中使用Akita
了,使用起来也并不麻烦。
const ready = useStoreQuery(appQuery, (state) => state.ready)
以上就是从app
,这个store
中获取ready
这个数据,ready
会在各自的框架中返回相应的响应式数据。
同样可以值直接获取非响应式数据值
const {ready} = appQuery.getValues()
这样获取的就是值类型了。
而修改直接通过Action
来执行即可
appAction.setReady()
至于复杂的值类型,比如一个值通过多个store
中属性计算获得的话可以使用useStoreSelect
来获取,使用rxjs
的方式通过pipe来创建自定义的Observable
来计算值即可
// app.query.ts
public get userReady(){
return zip(this.select('a'),this.select('b')).pipe(...)
}
然后直接向useStoreSelect
传递这个Observable
对象即可
useStoreSelect(appQuery.userReady,false)
数据持久化
Akita
还支持一些扩展功能,对我们最常用的就是持久化存储了,在@datorama/akita
中提供了persistState
函数来帮助我们实现插件持久化,
persistState({ include: ['auth', 'todos'] });
为了控制存储的粒度,可以使用PersistStateSelectFn
来选择需要持久化的数据
export const userStore = new UserStore()
// 持久化存储
export const UserPersistState: PersistStateSelectFn<UserState> =
state => ({
access_token: state.access_token,
refresh_token: state.refresh_token
})
UserPersistState.storeName = 'user'
import { AppPersistState } from './app'
import { UserPersistState } from './user'
// 持久化存储
persistState({
select: [AppPersistState, UserPersistState]
})
至此即大功告成.
总结
各种框架存在也即合理,各种项目按自己的需要选择相应框架也自有道理,我们的目的是在这些框架中如何将Store
的使用实现相似或统一的用法,来减少工作中的差异性,当然如果习惯专用一套也自不必这么麻烦。
经过以上操作,我们基本就可以使用Akita
来支持原本框架中对应的响应式数据存储
的实现了,如果公司有多个不用框架的项目,我们可以基本保持一致的数据获取方式,起码在不同的框架中使用了一致的思路和API来操作存储数据,不用换来换去切文档了,至于剩下来的时间么,那当然是去愉快的摸鱼了.
如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛 github