项目作者: awaw00

项目描述 :
saga + mobx = sagax, have fun with your app state management.
高级语言: TypeScript
项目地址: git://github.com/awaw00/sagax.git
创建时间: 2018-03-19T09:27:28Z
项目社区:https://github.com/awaw00/sagax

开源协议:

下载


SagaX

State management with mobx and redux-saga.

Try sagax at SagaX playground.

Table of Contents

Setup

yarn install sagax mobx@^3.6.1 redux-saga@^0.16 axios@^0.18.0

mobx、redux-saga、axios是sagax中的peerDependencies,请注意安装版本。

redux-saga的1.0.0-beta版本有一些不支持sagax的变动,在sagax兼容之前请使用0.16版本。

Getting Started Guide

Concepts

将应用状态划分到三类Store中:

  • ServiceStore 服务Store
  • LogicStore 逻辑Store
  • UIStore 界面Store
  • UtilStore 工具Store(Optional)

其中,ServiceStore用于定义接口调用方法、接口相关的ActionType和接口调用状态。

LogicStore用于管理应用的逻辑过程和中间状态,比如,控制应用加载时的初始化流程(如调用初始化数据接口等)、控制页面的渲染时机。

UIStore用于管理应用界面渲染所涉及的状态数据、响应用户界面事件。

当然,上面的划分方式并不是强制性的,在某些场景下(逻辑并不复杂的场景)把LogicStore与UIStore合二为一也许会更加合适。

确保ServiceStore的独立性对中等及以上规模项目的可维护性和可扩展性来说,是非常重要的。

Basic Usage

定义服务Store:

  1. // /stores/serviceStores.ts
  2. import { BaseStore, apiTypeDef, AsyncType, api, getAsyncState } from 'sagax';
  3. import { observable } from 'mobx';
  4. export class UserService extends BaseStore {
  5. @apiTypeDef GET_USER_INFO: AsyncType;
  6. @observable userInfo = getAsyncState();
  7. @api('GET_USER_INFO', {bindState: 'userInfo'})
  8. getUserInfo () {
  9. return this.http.get('/userInfo');
  10. }
  11. }
  12. export class OrderService extends BaseStore {
  13. @apiTypeDef GET_ORDER_LIST_OF_USER: AsyncType;
  14. @observable orderListOfUser = getAsyncState();
  15. @api('GET_ORDER_LIST_OF_USER', {bindState: 'orderListOfUser'})
  16. getOrderListOfUser (params: any) {
  17. return this.http.get('/order/listOfUser', {params});
  18. }
  19. }

定义UIStore(这里因为逻辑比较简单,把逻辑也写到UIStore了):

  1. // /stores/uiStores.ts
  2. import { BaseStore, bind, runSaga, apiTypeDef, types, AsyncType, api } from 'sagax';
  3. import { put, call, take, takeLatest, fork } from 'redux-saga/effects';
  4. import { observable, computed } from 'mobx';
  5. import { UserService, OrderService } from './serviceStores';
  6. interface OrderUIConfig extends types.BaseStoreConfig {
  7. userService: UserService;
  8. }
  9. export class OrderUI extends BaseStore {
  10. userService: UserService;
  11. orderService: OrderService;
  12. @computed
  13. get loading () {
  14. return this.userService.userInfo.loading || this.orderService.orderListOfUser.loading;
  15. }
  16. @computed
  17. get orderList () {
  18. return this.orderService.orderListOfUser.data;
  19. }
  20. constructor (config: OrderUIConfig) {
  21. super(config);
  22. // 这里为什么从参数中获取userStore而不是重新new一个?
  23. // 因为用户信息这类数据,在大多数应用中都是唯一的(一个系统不会有两个登录用户)
  24. // 保持userStore的唯一性,可以避免无效和重复的接口调用、内存占用
  25. this.userService = config.userService;
  26. this.orderService = new OrderService();
  27. }
  28. @runSaga
  29. *sagaMain () {
  30. yield fork(this.initOrderList);
  31. }
  32. @bind
  33. *initOrderList () {
  34. const self: this = yield this;
  35. const {userInfo, GET_USER_INFO} = self.userService;
  36. const {GET_ORDER_LIST_OF_USER} = self.userService;
  37. if (userInfo.loading) {
  38. // 先检查用户信息是否在加载中,如果是,则等待加载成功
  39. yield take(GET_USER_INFO.END);
  40. } else if (!self.userService.userInfo.data) {
  41. // 再检查用户信息是否已存在,若不存在,则发起获取用户信息的请求,并等待请求成功
  42. yield call(self.userService.getUserInfo);
  43. }
  44. // 以用户id为参数,发起获取用户订单列表的请求
  45. yield put({type: GET_ORDER_LIST_OF_USER.START, payload: {userId: userInfo.id}});
  46. }
  47. }

写一个React组件(为了简单没有使用mobx-react的Provider和inject等工具):

  1. // /App.tsx
  2. import React from 'react';
  3. import { render } from 'react-dom';
  4. import { observer } from 'mobx-react';
  5. import { UserService } from 'stores/serviceStores';
  6. import { OrderUI } from 'stores/uiStores';
  7. import OrderList from 'components/OrderList'; // 实现忽略
  8. const userUserService = new UserService();
  9. const orderUI = new OrderUI({userService});
  10. @observer
  11. class App extends React.Component {
  12. render () {
  13. return (
  14. <div>
  15. {orderUIStore.loading
  16. ? 'loading...'
  17. : (
  18. <OrderList dataSource={orderUI.orderList}></OrderList>
  19. )
  20. }
  21. </div>
  22. );
  23. }
  24. }
  25. render(<App></App>, document.getElementById('root'));

更多详细用法可查阅测试代码

Document

Core

BaseStore

  1. class BaseStore {
  2. /**
  3. * 静态对象是否已进行初始化
  4. * @type {boolean}
  5. */
  6. static initialized: boolean = false;
  7. /**
  8. * 默认的sagaRunner对象,在init静态方法中创建
  9. */
  10. static sagaRunner: SagaRunner;
  11. /**
  12. * 默认的axios对象,在init静态方法中创建
  13. */
  14. static http: AxiosInstance;
  15. /**
  16. * 见BaseStoreConfig.key
  17. */
  18. key: string;
  19. /**
  20. * 同BaseStore.http
  21. */
  22. http: AxiosInstance;
  23. /**
  24. * baseStoreConfig.sagaRunner 或 BaseStore.sagaRunner
  25. */
  26. sagaRunner: SagaRunner;
  27. /**
  28. * 初始化静态字段
  29. * @param {BaseStoreStaticConfig} baseStoreConfig
  30. */
  31. static init: (baseStoreConfig: BaseStoreStaticConfig = {}) => void;
  32. /**
  33. * 重置静态字段
  34. */
  35. static reset: () => void;
  36. constructor (baseStoreConfig: BaseStoreConfig = {});
  37. /**
  38. * 派发一个action
  39. * @param {Action} action
  40. * @returns {Action}
  41. */
  42. dispatch: (action: Action) => Action;
  43. /**
  44. * 执行Saga方法
  45. * @param {Saga} saga 要执行的saga方法
  46. * @param args saga方法的参数列表
  47. * @returns {Task} sagaTask
  48. */
  49. runSaga: (saga: Saga, ...args: any[]) => Task;

SagaRunner

提供一个Saga运行环境。

不同的SagaRunner实例之间运行的saga互相隔离,无法通信。在初始化BaseStore实例的时候,可以传入一个新的SagaRunner实例,store中的saga便会运行在一个隔离的“沙箱”中。

  1. class SagaRunner<T extends Action = Action> {
  2. constructor (private sagaOptions: SagaOptions = {});
  3. /**
  4. * 派发一个action
  5. * @param {T} action
  6. * @returns {T}
  7. */
  8. dispatch: (action: T) => action;
  9. /**
  10. * 非SagaMiddleware连接模式下,select副作用会使用这个方法
  11. * @returns {{[p: string]: any}}
  12. */
  13. getState: () => {[p: string]: any};
  14. /**
  15. * 执行saga方法
  16. * @param {Saga} saga方法
  17. * @param args saga参数列表
  18. * @returns {Task}
  19. */
  20. runSaga: (saga: Saga, ...args: any[]) => Task;
  21. /**
  22. * 注册store
  23. * @param {string} key store的key
  24. * @param store store对象
  25. */
  26. registerStore: (key: string, store: any) => void;
  27. /**
  28. * 根据key注销store
  29. * @param {string} key
  30. */
  31. unRegisterStore: (key: string) => void;
  32. /**
  33. * 将sagaRunner与SagaMiddleware连接
  34. * 注意:连接后无法通过select副作用获取store
  35. * 注意:请跟在createSagaMiddleware之后使用此方法(晚了容易丢失action或造成action派发失败的问题)
  36. * @param {SagaMiddleware<any>} middleware
  37. */
  38. useSagaMiddleware: (middleware: SagaMiddleware<any>) => void;
  39. }

Decorators

api

api (asyncTypeName: string, config: ApiConfig = {}): MethodDecorator

接口方法装饰器工厂方法。

当调用使用api装饰器装饰的方法时,会在调用接口前派发一个this[asyncTypeName].START的action。

调用成功后,派发一个this[asyncTypeName].END的action,并在payload中带上调用结果。

当调用失败时,会派发一个this[asyncTypeName].ERROR的action,并在payload中带上错误对象。

bind

bind: MethodDecorator

绑定方法执行上下文为this的方法装饰器

typeDef

typeDef: PropertyDecorator

ActionType定义属性装饰器。

使用该装饰器的字段会被自动赋值为${ClassName}<${key}>/${ActionType}

asyncTypeDef

asyncTypeDef: PropertyDecorator

AsyncType定义属性装饰器。

AsyncType是由三个ActionType组成的对象: START、END、ERROR,分别代表“接口请求开始”、“接口请求完成”、“接口请求失败”四种action。

runSaga

runSaga: MethodDecorator

saga方法自动执行方法装饰器。

标记该装饰器的方法,会在实例初始化时使用this.runSaga方法执行该saga方法。

一般在saga入口方法中使用该装饰器。

Utils

getAsyncState

获取异步状态的初始值(用于初始化异步状态字段),返回AsyncState

  1. function getAsyncState<T> (initialValue: T = null): AsyncState<T>;

Interfaces & Types

BaseStoreStaticConfig

BaseStore静态配置:

  1. export interface BaseStoreStaticConfig {
  2. /**
  3. * axios实例的配置参数对象
  4. */
  5. axiosConfig?: AxiosRequestConfig;
  6. /**
  7. * 默认sagaRunner的配置参数对象
  8. */
  9. sagaOptions?: SagaOptions;
  10. }

BaseStoreConfig

BaseStore配置:

  1. export interface BaseStoreConfig {
  2. /**
  3. * store的key
  4. * 当构建store的时候,若传入了key(BaseStoreConfig.key),会在sagaRunner以此key中注册该store
  5. * 被注册的store可以在select副作用获取到该store对象,一般在这个store是全局唯一的通用store时使用该配置
  6. * 如:yield select(stores => stores[key])
  7. *
  8. * 如果没有在构建store的时候传入key,将不会在sagaRunner中注册,并且会用一个随机字符串填充该key值充当action type的命名空间前缀的一部分
  9. */
  10. key?: string;
  11. /**
  12. * 设置一个另外的sagaRunner对象,这个store中的saga将会在这个sagaRunner中执行
  13. * 并且无法take到其它sagaRunner中的action
  14. */
  15. sagaRunner?: SagaRunner;
  16. /**
  17. * 接口返回结果转换方法,在api调用成功后,会通过本方法转换后再赋值给相应的的state
  18. * @default void
  19. * @param apiRes 普通接口调用的结果对象
  20. * @returns {any}
  21. */
  22. apiResToState?: (apiRes?: any) => any;
  23. /**
  24. * api调用过程中是否自动更新绑定的state
  25. * @default true
  26. */
  27. bindState?: boolean;
  28. }

ActionType

  1. /**
  2. * T: 约定触发该Action会带的payload属性的类型
  3. */
  4. export type ActionType<T = any> = string;

AsyncState

异步状态:

  1. export interface AsyncState<T = any> {
  2. loading: boolean;
  3. error: null | Error;
  4. data: null | T;
  5. }

AsyncType

异步类型:

  1. /**
  2. * R: 约定触发START时会带的payload属性类型
  3. * S: 约定触发END时会带的payload属性类型
  4. * F: 约定触发ERROR时会带的payload属性类型
  5. */
  6. export interface AsyncType<R = any, S = any, F = any> {
  7. START: ActionType<R>;
  8. END: ActionType<S>;
  9. ERROR: ActionType<F>;
  10. }

ApiConfig

api装饰器配置

  1. export interface ApiConfig {
  2. /**
  3. * 异步action type名称
  4. */
  5. asyncTypeName?: string;
  6. /**
  7. * 默认参数对象
  8. * @default void
  9. */
  10. defaultParams?: any;
  11. /**
  12. * 接口状态绑定state的名称
  13. * @default void
  14. */
  15. bindState?: string;
  16. /**
  17. * 是否为标准的axios接口(接口方法是否返回AxiosPromise)
  18. * @default true
  19. */
  20. axiosApi?: boolean;
  21. }

Best Practice

项目结构

个人推荐的项目结构(仅供参考)

  1. .
  2. ├── src
  3. ├── index.ts
  4. ├── App.ts
  5. ├── routes
  6. ├── index.ts
  7. ├── Home
  8. ├── index.ts
  9. ├── components
  10. └── Product
  11. ├── index.ts
  12. ├── components
  13. └── stores
  14. ├── ProductLogic.ts
  15. └── ProductUI.ts
  16. └── stores
  17. ├── index.ts
  18. ├── logic
  19. └── AppLogic.ts
  20. └── service
  21. ├── UserApi.ts
  22. ├── ProductApi.ts
  23. └── OrderApi.ts
  24. └── package.json

与redux的全局store不同,sagax的store更灵活,可以有全局store也可以有局部store。

可以在src/stores/index.ts文件中初始化全局store:

  1. import AppLogic from './logic/AppLogic';
  2. import UserApi from './service/UserApi';
  3. export const user = new UserApi({key: 'user'});
  4. export const appLogic = new AppLogic({key: 'appLogic', user});
  5. ...

然后在src/routes/Product/index.ts中使用全局store:

  1. import { user } from '../../stores';
  2. import ProductUI from '../stores/ProductUI';
  3. const productUI = new ProductUI({user});
  4. ...

DO NOT: 给ActionType加命名空间

sagax在处理ActionType的时候(包括AsyncType),会自动加上命名空间避免重复。

具体命名空间的值可以参考测试代码:

  1. test('asyncType和typeDef自动赋值', () => {
  2. class TypeTest extends BaseStore {
  3. @typeDef TYPE_B: string;
  4. @asyncTypeDef TYPE_API_B: AsyncType;
  5. }
  6. const typeTest = new TypeTest({key: 'test'});
  7. expect(typeTest.TYPE_B).toBe('TypeTest<test>/TYPE_B');
  8. expect(typeTest.TYPE_API_B).toEqual({
  9. START: 'TypeTest<test>/TYPE_API_B/START',
  10. END: 'TypeTest<test>/TYPE_API_B/END',
  11. ERROR: 'TypeTest<test>/TYPE_API_B/ERROR'
  12. });
  13. const randomKeyTypeTest = new TypeTest();
  14. const key = randomKeyTypeTest.key;
  15. expect(key).toMatch(/^[a-zA-Z]{6}$/);
  16. expect(randomKeyTypeTest.TYPE_B).toBe(`TypeTest<${key}>/TYPE_B`);
  17. expect(randomKeyTypeTest.TYPE_API_B).toEqual({
  18. START: `TypeTest<${key}>/TYPE_API_B/START`,
  19. END: `TypeTest<${key}>/TYPE_API_B/END`,
  20. ERROR: `TypeTest<${key}>/TYPE_API_B/ERROR`
  21. });
  22. });

DO NOT: 通过Action去执行方法

在配合redux使用saga的时候,一般会通过派发一个Action的方式来执行saga方法,例如:

saga:

  1. function* getData ({payload}) {
  2. ...
  3. }
  4. yield takeLatest(types.GET_DATA, getData);

react component:

  1. @connect(
  2. state => ({}),
  3. dispatch => ({
  4. onGetData () {
  5. dispatch({
  6. type: types.GET_DATA,
  7. payload: {...}
  8. });
  9. }
  10. })
  11. )
  12. class Comp extends React.Component {
  13. render () {
  14. return (
  15. <button onClick={this.props.onGetData}>Click</button>
  16. );
  17. }
  18. }

在sagax中,也可以像上面这样去实现,但是不推荐这么做,原因如下:

  • 方法参数通过payload传递,将无法享受开发时的编辑器的智能提示和类型安全检查
  • 通过Action来调用方法,容易让应用的Action变得混乱(有的Action既可用来主动调用方法,又可用来作为事件被动监听)

最好的做法是,所有的Action都只作为被动的事件通知,是向模块外部暴露的钩子,以便外部使用者在某个事件触发时做特定的处理。