神马😎,Vue,React,Angular统统都能用它来搞定-Akita响应式数据存储

1,838 阅读5分钟

前言

又是崭新的一周,怀着愉快的心情来到了公司,部门经理看到我就很愉快的告诉我有两个好消息。第一我们又开了一个新项目,还是交给你来造~。干,说好愉快的摸鱼呢。第二个就是这个项目技术框架定为React。再干,上上个项目是Angular,上一个是Vue,现在又变成了React,后面还会接着搞svelte。说好的不忘初心呢。

image.png

随着愉悦的心情中一阵狂撸~

image.png

我突然想到一个问题,因为每个项目都用到了数据管理,比如Angular一般用NgRxNGXS,Vue的话会使用Vuex或者pinia,如果是React则会使用ReduxMboX,那么有没有一个响应数据存储让三个框架都可以使用,而不用在不同框架的项目中来回切换呢.

Frame 1.png

那么问题来了,如果需要在各个框架中都可以正常使用,那么首先就是需要它本身不依赖于运行的框架,我发现基于RxJS的响应式数据存储方案就是可以合适的选择,因为是基于RxJS的实现,那么当然可以在Vue,React以及Angular(这个自不必说)中良好的运行.

所以问题不大,功夫不负有心人,在一番查找后我发现Akita就很好的满足了我的需求,那么现在就先来瞅瞅这只秋田犬吧.

Akita

image.png

Akita是一个基于RxJS的响应式数据存储工具,它主要分为Store,Service,Query三部分.

1638166973(1).png

Store

Store部分是是来定义数据结构以及初始的默认值,类似Vuexredux中的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部分是负责修改和操作数据的部分,通过对应公开的方法,来帮助我们实现对数据的修改和操作,也是类似于VuexActionMutation的部分

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中的数据,我们也可以通过RxJSPipe来组合我们需要的数据信息,或者直接使用TypeScriptget属性来获取数据.

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相比与ReactHooks,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对象.

不过VueReact中并没有依赖注入的模块,需要在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