Action与Handler

Action是Model中的事件,Reducer与Effect是其Handler...

Aciton的定义

export interface Action {
  type: string; //通常由ModuleName.ActionName组成
  //通常无需设置,同时有多个handler时,可以特别指明处理顺序
  priority?: string[];
  payload?: any[];
}

创建并派发Action

先回忆一下,不管是redux还是vuex,派发action/mutation都类似:

dispatch({type:"user.login", payload:{username:"jimmy", password:"123456"}})

这种方法有个很大的弊端,就是派发体Action需要手写,TS又不能很好的验证与提示,很容易写错。比如type写成了user.updataUser,也验证不到,更容易出错的是参数部分,如果参数是一个复杂的结构体,盲写很容易写错。

Elux中改进了这种原始写法,自动生成派发体Action,并且配合TS类型提示,再也不用担心写错type和参数:

//自动生成{type:"xxx",args:xxx}
const action = userActions.login('jimmy','123456');
dispatch(action);

在Model中自动创建并派发Action:

import {Modules} from '@/Global';

export class Model extends BaseModel<ModuleState, APPState> {
    protected async test() {
        //本Model中可以用this.actions
        const logoutAction = this.actions.logout();
        console.log(logoutAction); //{type:'stage.logout'}
        await this.dispatch(logoutAction); //可以await该Action的执行
        //非本Model中可以用Modules.article.actions
        const searchAction = Modules.article.actions.search({keyword:'aaa'});
        this.dispatch(searchAction);
    }
}





 



 



在View中自动创建并派发Action:

const Component = ({curUser, notices, dispatch}) => {
    const onLogout = () => dispatch(Modules.stage.actions.logout());

    return <button onClick={onLogout}>退出</button>
}

使用GetActions()写法:

//当需要 dispatch 多个 module 的 action 时,例如:
dispatch(Modules.a.actions.a1())
dispatch(Modules.b.actions.b1())

//这种写法可以简化为:
const {a, b} = GetActions('a', 'b')
dispatch(a.a1())
dispatch(b.b1())

跟踪Action执行结果

可以将effect的执行情况注入moduleState中,比如:

export class Model extends BaseModel<ModuleState, APPState> {

  //定义一个effect/action,用来执行列表查询
  //将该effect的执行情况,注入this.state.listLoading中
   @effect(`this.listLoading`)
   public async fetchList(listSearch: ListSearch) {
     const {list} = await api.getList(listSearch);
     this.dispatch(this.privateActions.putList(listSearch, list));
   }
}





 






可以awiat一个action的所有handler执行完成,比如:

  • 在View中:

    // src/modules/stage/views/LoginForm.tsx
    
    const onSubmit = () => {
      const result = dispatch(Modules.stage.actions.login({username, password}));
      // 结果是一个Promise
      result.catch(({message}) => {
        setErrorMessage(message);
      });
    }
    
    
  • 在Model中:

    // src/modules/article/model.ts
    
    public async onMount(): Promise<void> {
      this.dispatch(this.privateActions._initState({currentView}));
      if (currentView === 'list') {
        await this.dispatch(this.actions.fetchList(listSearch));
      } else if (currentView && itemId) {
        await this.dispatch(this.actions.fetchItem(itemId));
      }
    }
    
    

内置特殊Action

框架内置了几个特殊的Action(以_前缀),它们在特定的时机会自动派发:

  • stage._error effect运行中出现任何错误,框架将自动派发该action,可以使用effect监听该action来统一处理错误。
  • stage._testRouteChange 路由准备跳转时会自动派发该action,可以使用effect监听该action,并阻止路由跳转。
  • stage._beforeRouteChange 路由准备前时会自动派发该action,可以使用effect监听该action,在跳转前保存某些有用的数据,如未提交的表单等。
  • stage._afterRouteChange 路由准备完成时会自动派发该action,可以使用effect监听该action,如获取最新的路由信息注入ModuleState中。
  • module._initState 用来注入初始的ModuleState,Model的onMount中必需派发该action。
  • module._updateState 用来简单的合并更新ModuleState。
  • module._loadingState 用来将执行过程作为loading状态注入ModuleState,effect的执行过程可以自动派发该action。

泛监听

可以使用一个Hander监听多个Action:

  • 使用,符号分隔多个actionType
  • 使用*符号作为moduleName的通配符
  • 使用this可以指代本模块名
// src/modules/moduleB/model.ts
class Model extends BaseModel 

  @effect()
  //同时监听2个模块的'_initState' Action
  async ['moduleA._initState, moduleA._initState'](){
    console.log('moduleA/moduleB inited');
  }

  @effect()
  //同时监听所有模块的'_initState' Action
  async ['*._initState'](){
    console.log('all inited');
  }
}

多个Handler的执行顺序

既然Action是Model中的事件,dispatch一个action可以触发多个reducer和effect,那么它们是同步还是异步?执行顺序是怎样的?

假设有3个模块都监听了Stage.logout事件:

// src/modules/stage/model.ts

//监听本模块自己的Action
class Model extends BaseModel {
  @reducer
  logout(){
    console.log('stage.reducer');
    return {...this.state, curUser: null}
  }

  @effect()
  async ['this.logout'](){
    console.log('stage.effect');
  }
}






 





 


// src/modules/moduleA/model.ts

//监听其它模块的Action
class Model extends BaseModel {
  @reducer
  ['stage.logout'](){
    console.log('moduleA.reducer');
    return {...this.state, editable: false}
  }

  @effect()
  //注意后面的小尾巴[,]这是一种hack写法
  async ['stage.logout,'](){
    console.log('moduleA.effect');
  }
}






 






 


// src/modules/moduleB/model.ts

//监听其它模块的Action
class Model extends BaseModel {
  @reducer
  ['stage.logout'](){
    console.log('moduleB.reducer');
    return {...this.state, editable: false}
  }

  @effect()
  //注意后面的小尾巴[,]这是一种hack写法
  async ['stage.logout,'](){
    console.log('moduleB.effect');
  }
}






 






 


现在dispatch(Modules.stage.actions.logout()),我们可以看到:

stage.reducer
moduleA.reducer
moduleB.reducer
stage.effect
moduleA.effect
moduleB.effect
  • 所有reducer被最先同步执行,而此action宿主模块的reducer第一个执行(stage模块是此action的宿主模块,其它都是被动监听模块)。
  • 所有effect都在reducer执行完成之后才执行,同样action宿主模块的effect第一个执行。

同一个Model对同一个Action保持一份监听(reducer和effect各一个)

为什么?因为没必要多份,你完全可以在 reducer 和 effect 中调用其它方法。

改变执行顺序

可以使用Action的priority属性来强制更改不同Handler的执行顺序,如:

dispatch({type: 'stage.logout', priority: ['moduleB']})

这样会把 moduleB 的 reducer 和 effect 优先执行,但是不推荐这么做,保持模块之间的松散性,可以改为:

  • ModuleB 执行完后,dispatch一个新的action
  • MouldeA 监听这个新的action

错误与处理

ActionHandler相当于一条执行链,执行过程中若出现任何错误,框架将自动dispatch一个type为stage._error的Action(通常actionName为_表示是框架内置的Action)

可以通过effect来监听这个ErrorAction,然后决定是消化错误还是继续抛出。如果继续抛出,则该ActionHandler的执行链将就此中断

中间件与日志记录

中间件可以在Action和Store中间建立一条管道,类似于Redux的Middleware,它的类型定义如下:

export type StoreMiddleware = (api: {getStore: () => IStore; dispatch: Dispatch}) => (next: Dispatch) => (action: Action) => void | Promise<void>;

日志记录是一个回调函数,每一次dispatch都会调用该回调函数,它的类型定义如下:

export type storeLoggerInfo = {
  id: number;
  isActive: boolean;
  actionName: string;
  payload: any[];
  priority: string[];
  handers: string[];
  state: any;
  effect: boolean;
};

export type StoreLogger = (info: storeLoggerInfo) => void;

使用中间件和日志记录:

// src/Project.ts
//Elux全局设置,参见 https://eluxjs.com/api/react-web.setconfig.html
export const appConfig: AppConfig = setConfig({
  StoreLogger: (info) => console.log(info),
  StoreMiddlewares: [...],
});

DevTools

Elux兼容Redux的devTools,并改进了它的显示信息,参见/guide/dev-tools