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:
import { BaseStore, apiTypeDef, AsyncType, api, getAsyncState } from 'sagax';
import { observable } from 'mobx';
export class UserService extends BaseStore {
@apiTypeDef GET_USER_INFO: AsyncType;
@observable userInfo = getAsyncState();
@api('GET_USER_INFO', {bindState: 'userInfo'})
getUserInfo () {
return this.http.get('/userInfo');
}
}
export class OrderService extends BaseStore {
@apiTypeDef GET_ORDER_LIST_OF_USER: AsyncType;
@observable orderListOfUser = getAsyncState();
@api('GET_ORDER_LIST_OF_USER', {bindState: 'orderListOfUser'})
getOrderListOfUser (params: any) {
return this.http.get('/order/listOfUser', {params});
}
}
定义UIStore(这里因为逻辑比较简单,把逻辑也写到UIStore了):
import { BaseStore, bind, runSaga, apiTypeDef, types, AsyncType, api } from 'sagax';
import { put, call, take, takeLatest, fork } from 'redux-saga/effects';
import { observable, computed } from 'mobx';
import { UserService, OrderService } from './serviceStores';
interface OrderUIConfig extends types.BaseStoreConfig {
userService: UserService;
}
export class OrderUI extends BaseStore {
userService: UserService;
orderService: OrderService;
@computed
get loading () {
return this.userService.userInfo.loading || this.orderService.orderListOfUser.loading;
}
@computed
get orderList () {
return this.orderService.orderListOfUser.data;
}
constructor (config: OrderUIConfig) {
super(config);
this.userService = config.userService;
this.orderService = new OrderService();
}
@runSaga
*sagaMain () {
yield fork(this.initOrderList);
}
@bind
*initOrderList () {
const self: this = yield this;
const {userInfo, GET_USER_INFO} = self.userService;
const {GET_ORDER_LIST_OF_USER} = self.userService;
if (userInfo.loading) {
yield take(GET_USER_INFO.END);
} else if (!self.userService.userInfo.data) {
yield call(self.userService.getUserInfo);
}
yield put({type: GET_ORDER_LIST_OF_USER.START, payload: {userId: userInfo.id}});
}
}
写一个React组件(为了简单没有使用mobx-react的Provider和inject等工具):
import React from 'react';
import { render } from 'react-dom';
import { observer } from 'mobx-react';
import { UserService } from 'stores/serviceStores';
import { OrderUI } from 'stores/uiStores';
import OrderList from 'components/OrderList';
const userUserService = new UserService();
const orderUI = new OrderUI({userService});
@observer
class App extends React.Component {
render () {
return (
<div>
{orderUIStore.loading
? 'loading...'
: (
<OrderList dataSource={orderUI.orderList}/>
)
}
</div>
);
}
}
render(<App/>, document.getElementById('root'));
更多详细用法可查阅测试代码
Document
Core
BaseStore
class BaseStore {
static initialized: boolean = false;
static sagaRunner: SagaRunner;
static http: AxiosInstance;
key: string;
http: AxiosInstance;
sagaRunner: SagaRunner;
static init: (baseStoreConfig: BaseStoreStaticConfig = {}) => void;
static reset: () => void;
constructor (baseStoreConfig: BaseStoreConfig = {});
dispatch: (action: Action) => Action;
runSaga: (saga: Saga, ...args: any[]) => Task;
SagaRunner
提供一个Saga运行环境。
**不同的SagaRunner实例之间运行的saga互相隔离,无法通信。**在初始化BaseStore实例的时候,可以传入一个新的SagaRunner实例,store中的saga便会运行在一个隔离的“沙箱”中。
class SagaRunner<T extends Action = Action> {
constructor (private sagaOptions: SagaOptions = {});
dispatch: (action: T) => action;
getState: () => {[p: string]: any};
runSaga: (saga: Saga, ...args: any[]) => Task;
registerStore: (key: string, store: any) => void;
unRegisterStore: (key: string) => void;
useSagaMiddleware: (middleware: SagaMiddleware<any>) => void;
}
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
function getAsyncState<T> (initialValue: T = null): AsyncState<T>;
Interfaces & Types
BaseStoreStaticConfig
BaseStore静态配置:
export interface BaseStoreStaticConfig {
axiosConfig?: AxiosRequestConfig;
sagaOptions?: SagaOptions;
}
BaseStoreConfig
BaseStore配置:
export interface BaseStoreConfig {
key?: string;
sagaRunner?: SagaRunner;
apiResToState?: (apiRes?: any) => any;
bindState?: boolean;
}
ActionType
export type ActionType<T = any> = string;
AsyncState
异步状态:
export interface AsyncState<T = any> {
loading: boolean;
error: null | Error;
data: null | T;
}
AsyncType
异步类型:
export interface AsyncType<R = any, S = any, F = any> {
START: ActionType<R>;
END: ActionType<S>;
ERROR: ActionType<F>;
}
ApiConfig
api装饰器配置
export interface ApiConfig {
asyncTypeName?: string;
defaultParams?: any;
bindState?: string;
axiosApi?: boolean;
}
Best Practice
项目结构
个人推荐的项目结构(仅供参考)
.
├── src
│ ├── index.ts
│ ├── App.ts
│ ├── routes
│ │ ├── index.ts
│ │ ├── Home
│ │ │ ├── index.ts
│ │ │ ├── components
│ │ └── Product
│ │ ├── index.ts
│ │ ├── components
│ │ └── stores
│ │ ├── ProductLogic.ts
│ │ └── ProductUI.ts
│ └── stores
│ ├── index.ts
│ ├── logic
│ │ └── AppLogic.ts
│ └── service
│ ├── UserApi.ts
│ ├── ProductApi.ts
│ └── OrderApi.ts
└── package.json
与redux的全局store不同,sagax的store更灵活,可以有全局store也可以有局部store。
可以在src/stores/index.ts
文件中初始化全局store:
import AppLogic from './logic/AppLogic';
import UserApi from './service/UserApi';
export const user = new UserApi({key: 'user'});
export const appLogic = new AppLogic({key: 'appLogic', user});
...
然后在src/routes/Product/index.ts
中使用全局store:
import { user } from '../../stores';
import ProductUI from '../stores/ProductUI';
const productUI = new ProductUI({user});
...
DO NOT: 给ActionType加命名空间
sagax在处理ActionType的时候(包括AsyncType),会自动加上命名空间避免重复。
具体命名空间的值可以参考测试代码:
test('asyncType和typeDef自动赋值', () => {
class TypeTest extends BaseStore {
@typeDef TYPE_B: string;
@asyncTypeDef TYPE_API_B: AsyncType;
}
const typeTest = new TypeTest({key: 'test'});
expect(typeTest.TYPE_B).toBe('TypeTest<test>/TYPE_B');
expect(typeTest.TYPE_API_B).toEqual({
START: 'TypeTest<test>/TYPE_API_B/START',
END: 'TypeTest<test>/TYPE_API_B/END',
ERROR: 'TypeTest<test>/TYPE_API_B/ERROR'
});
const randomKeyTypeTest = new TypeTest();
const key = randomKeyTypeTest.key;
expect(key).toMatch(/^[a-zA-Z]{6}$/);
expect(randomKeyTypeTest.TYPE_B).toBe(`TypeTest<${key}>/TYPE_B`);
expect(randomKeyTypeTest.TYPE_API_B).toEqual({
START: `TypeTest<${key}>/TYPE_API_B/START`,
END: `TypeTest<${key}>/TYPE_API_B/END`,
ERROR: `TypeTest<${key}>/TYPE_API_B/ERROR`
});
});
DO NOT: 通过Action去执行方法
在配合redux使用saga的时候,一般会通过派发一个Action的方式来执行saga方法,例如:
saga:
function* getData ({payload}) {
...
}
yield takeLatest(types.GET_DATA, getData);
react component:
@connect(
state => ({}),
dispatch => ({
onGetData () {
dispatch({
type: types.GET_DATA,
payload: {...}
});
}
})
)
class Comp extends React.Component {
render () {
return (
<button onClick={this.props.onGetData}>Click</button>
);
}
}
在sagax中,也可以像上面这样去实现,但是不推荐这么做,原因如下:
- 方法参数通过payload传递,将无法享受开发时的编辑器的智能提示和类型安全检查
- 通过Action来调用方法,容易让应用的Action变得混乱(有的Action既可用来主动调用方法,又可用来作为事件被动监听)
最好的做法是,所有的Action都只作为被动的事件通知,是向模块外部暴露的钩子,以便外部使用者在某个事件触发时做特定的处理。