如何优雅地处理loading

在业务代码中,为了判断异步操作是否在运行中,往往通过额外的标志量来区分;本文将从dva-loading源码出发,分析在React项目中如何统一管理loading状态,然后给出一种在Vue项目中管理loading的解决方案。

<!--more-->

参考

1. 业务场景

在业务代码中,我们往往通过一个额外的标志量来判断是否需要展示加载状态

function fetchList(){
  if(this.isFetchList) return // 避免重复请求
  this.isFetchList = true
  fetchListApi().then((res)=>{
    // do res
  }).finally(()=>{
    this.isFetchList = false // 重置状态
  })
}

当需要判断的操作过多时,需要花费大量的精力来维护这些状态。

在dva中可以使用dva-loading来管理对应的action,然后通过注入的loading.effects[actionType]可以很方便地获取某个action是否在运行中,下面将分析dva-loading的实现原理。

2. dav-loading源码分析

package/dva-loading目录下可以看见dva-loading插件的源码。

在之前的文章:dva源码分析中介绍了dva中的插件系统,这里我们来研究一下dva-loading插件的实现原理

const SHOW = '@@DVA_LOADING/SHOW';
const HIDE = '@@DVA_LOADING/HIDE';
const NAMESPACE = 'loading';

function createLoading(opts = {}) {
  const namespace = opts.namespace || NAMESPACE;

  const { only = [], except = [] } = opts;
  //只能配置only或except中的一个
  if (only.length > 0 && except.length > 0) {
    throw Error('It is ambiguous to configurate `only` and `except` items at the same time.');
  }

  const initialState = {
    global: false,
    models: {},
    effects: {},
  };

  const extraReducers = {
    [namespace](state = initialState, { type, payload }) {
      const { namespace, actionType } = payload || {};
      let ret;
      switch (type) {
        // 将actionType对应的loading状态设置为true,在页面上通过loading.effects[actionType]获取对应的effect loading状态
        case SHOW:
          ret = {
            ...state,
            global: true,
            models: { ...state.models, [namespace]: true },
            effects: { ...state.effects, [actionType]: true },
          };
          break;
        // 将actionType对应的loading状态设置为false
        case HIDE: {
          const effects = { ...state.effects, [actionType]: false };
          // 判断还存在effect loading为true的model
          const models = {
            ...state.models,
            [namespace]: Object.keys(effects).some(actionType => {
              const _namespace = actionType.split('/')[0];
              if (_namespace !== namespace) return false;
              return effects[actionType];
            }),
          };
          const global = Object.keys(models).some(namespace => {
            return models[namespace];
          });
          ret = {
            ...state,
            global,
            models,
            effects,
          };
          break;
        }
        default:
          ret = state;
          break;
      }
      return ret;
    },
  };

  function onEffect(effect, { put }, model, actionType) {
    const { namespace } = model;
    // 获取配置项中的only和except,判断actionType是否需要添加loading
    if (
      (only.length === 0 && except.length === 0) ||
      (only.length > 0 && only.indexOf(actionType) !== -1) ||
      (except.length > 0 && except.indexOf(actionType) === -1)
    ) {
      return function*(...args) {
        // 在执行effect之前将actionType对应的loading设置为true
        yield put({ type: SHOW, payload: { namespace, actionType } });
        yield effect(...args);
        // 在执行effect之后将actionType对应的loading设置为false
        yield put({ type: HIDE, payload: { namespace, actionType } });
      };
    } else {
      return effect;
    }
  }
  // dva插件格式,返回包含特定key的一个对象
  return {
    extraReducers, // 在创建store时调用
    onEffect,
  };
}

export default createLoading;

可以看见dva-loading主要实现了两个钩子

  • extraReducers中注册了一个处理loading的reducer,根据action.type区分是显示还是隐藏,根据action.payload.actionType判断当前正在进行loading状态切换的action
  • onEffect注册的方法会在dva.start时收集到saga中,最后在sagaMiddleware.run时,首先执行put({type:SHOW})将loading状态置为true,然后再执行原本的effect,最后执行put({type:HIDE})将loading状态置为false

3. 实现vue-loading

参考dva-loading,我实现了一个vue-loading,用于管理vuex中异步action的执行状态。

大致实现过程为,通过vuex插件劫持store._actions,并在action执行前后修改内部loading状态,整个过程分为两部分

3.1. 内置loading模块

这个模块用来保存所有action的loading状态,在视图中可以通过store.state.loading[actionName]的方式获取执行状态。

// loading模块
const createLoadingModule = () => {
    return {
        namespaced: true,
        state: {},
        mutations: {
            [LOADING_START](state, key) {
                Vue.set(state, key, true) // 动态添加state
            },
            [LOADING_END](state, key) {
                Vue.set(state, key, false)
            }
        },
    }
}

3.2. 劫持所有action

在注册插件时劫持所有的action,在调用前后提交loading状态

const defaultConfig = {
    // loading模块的名称,使用方式:this.$store.state[loadingModuleName]
    loadingModuleName: LOADING_NAME,
    // 当前触发的action名字,context与payload同原始的dispatch参数
    // 在当前action执行前调用
    before(key, context, payload) {},
    // 在当前action执行后调用
    after() {}
}

const createVuexLoading = (config) => (store) => {
    config = Object.assign(defaultConfig, config)

    // 注册loadingModule
    const loadingModule = createLoadingModule()
    const {loadingModuleName} = config
    store.registerModule(loadingModuleName, loadingModule)

    // 开始和结束的钩子
    const hooks = {
        before(key, context, payload) {
            config.before(key, context, payload)
            store.commit(`${loadingModuleName}/${LOADING_START}`, key)
        },
        after(key, context, payload) {
            store.commit(`${loadingModuleName}/${LOADING_END}`, key)
            config.after(key, context, payload)
        }
    }

    // 劫持所有的action,需要注意只有返回promise的action会正常触发loading监听
    const {_actions} = store
    Object.keys(_actions).forEach(key => {
        _actions[key] = _actions[key].map(action => {
            return async (context, payload) => {
                hooks.before(key, context, payload) // commit loading start
                try {
                    await action(context, payload)
                } catch (e) {
                    console.log(e)
                } finally {
                    hooks.after(key, context, payload) // commit loading end
                }
            }
        })
    })
}

此外,我们也可以效仿dva-loading,在config中增加对于onlyexcept配置的支持,只监听或排除某些特定的actionType。完整代码已放在github上。

4. 小结

本文从应用场景出发,分析了dva-loading的实现原理,然后给出了Vue项目中一种自动更新action的loading状态的方案,两种方案的实现原理基本类似

  • 通过插件实现一个内置的loading模块,用于获取每个action的loading状态
  • 在action执行前将actionType对应的loading更新为true,在action执行后将actionType对应的loading更新为false
    • dva-loading基于redux-saga,通过获取的effect,重新实现一个迭代器函数,并在effect前后执行对应loading state逻辑
    • vue-loading通过store._actions获取并劫持所有的action,在原始action执行前后执行对应loading state更新逻辑

当然,上述方案的使用依赖于使用全局状态管理的整个应用,对于局部组件的state,由于对应的状态更新并不会经过统一的store处理,因此无法统一监听并修改loading状态。这样可以算作是使用全局状态的一个优点吧...