npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

sagax

v0.1.9

Published

play mobx with saga

Downloads

46

Readme

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:

// /stores/serviceStores.ts
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了):

// /stores/uiStores.ts
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);
    
    // 这里为什么从参数中获取userStore而不是重新new一个?
    // 因为用户信息这类数据,在大多数应用中都是唯一的(一个系统不会有两个登录用户)
    // 保持userStore的唯一性,可以避免无效和重复的接口调用、内存占用
    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);
    }
    // 以用户id为参数,发起获取用户订单列表的请求
    yield put({type: GET_ORDER_LIST_OF_USER.START, payload: {userId: userInfo.id}});
  }
}

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

// /App.tsx
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 {
  /**
   * 静态对象是否已进行初始化
   * @type {boolean}
   */
  static initialized: boolean = false;
  /**
   * 默认的sagaRunner对象,在init静态方法中创建
   */
  static sagaRunner: SagaRunner;
  /**
   * 默认的axios对象,在init静态方法中创建
   */
  static http: AxiosInstance;

  /**
   * 见BaseStoreConfig.key
   */
  key: string;
  /**
   * 同BaseStore.http
   */
  http: AxiosInstance;
  /**
   * baseStoreConfig.sagaRunner 或 BaseStore.sagaRunner
   */
  sagaRunner: SagaRunner;

  /**
   * 初始化静态字段
   * @param {BaseStoreStaticConfig} baseStoreConfig
   */
  static init: (baseStoreConfig: BaseStoreStaticConfig = {}) => void;

  /**
   * 重置静态字段
   */
  static reset: () => void;

  constructor (baseStoreConfig: BaseStoreConfig = {});

  /**
   * 派发一个action
   * @param {Action} action
   * @returns {Action}
   */
  dispatch: (action: Action) => Action;

  /**
   * 执行Saga方法
   * @param {Saga} saga 要执行的saga方法
   * @param args        saga方法的参数列表
   * @returns {Task}    sagaTask
   */
  runSaga: (saga: Saga, ...args: any[]) => Task;

SagaRunner

提供一个Saga运行环境。

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


class SagaRunner<T extends Action = Action> {
  constructor (private sagaOptions: SagaOptions = {});

  /**
   * 派发一个action
   * @param {T} action
   * @returns {T}
   */
  dispatch: (action: T) => action;

  /**
   * 非SagaMiddleware连接模式下,select副作用会使用这个方法
   * @returns {{[p: string]: any}}
   */
  getState: () => {[p: string]: any};

  /**
   * 执行saga方法
   * @param {Saga}    saga方法
   * @param args      saga参数列表
   * @returns {Task}
   */
  runSaga: (saga: Saga, ...args: any[]) => Task;

  /**
   * 注册store
   * @param {string} key  store的key
   * @param store         store对象
   */
  registerStore: (key: string, store: any) => void;
  
  /**
   * 根据key注销store
   * @param {string} key
   */
  unRegisterStore: (key: string) => void;

  /**
   * 将sagaRunner与SagaMiddleware连接
   * 注意:连接后无法通过select副作用获取store
   * 注意:请跟在createSagaMiddleware之后使用此方法(晚了容易丢失action或造成action派发失败的问题)
   * @param {SagaMiddleware<any>} middleware
   */
  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 {
  /**
   * axios实例的配置参数对象
   */
  axiosConfig?: AxiosRequestConfig;
  /**
   * 默认sagaRunner的配置参数对象
   */
  sagaOptions?: SagaOptions;
}

BaseStoreConfig

BaseStore配置:


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

ActionType

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

AsyncState

异步状态:

export interface AsyncState<T = any> {
  loading: boolean;
  error: null | Error;
  data: null | T;
}

AsyncType

异步类型:


/**
 * R: 约定触发START时会带的payload属性类型
 * S: 约定触发END时会带的payload属性类型
 * F: 约定触发ERROR时会带的payload属性类型
 */
export interface AsyncType<R = any, S = any, F = any> {
  START: ActionType<R>;
  END: ActionType<S>;
  ERROR: ActionType<F>;
}

ApiConfig

api装饰器配置


export interface ApiConfig {
  /**
   * 异步action type名称
   */
  asyncTypeName?: string;
  /**
   * 默认参数对象
   * @default void
   */
  defaultParams?: any;
  /**
   * 接口状态绑定state的名称
   * @default void
   */
  bindState?: string;
  /**
   * 是否为标准的axios接口(接口方法是否返回AxiosPromise)
   * @default true
   */
  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都只作为被动的事件通知,是向模块外部暴露的钩子,以便外部使用者在某个事件触发时做特定的处理。