请求中间件
请求中间件是一个异步函数,它提供了强大的,几乎能控制一个请求的所有行为的能力。如果你只是使用 alova,那你应该很可能不需要使用请求中间件,因为它主要用于完成自定义的请求策略,无论简单还是复杂的请求策略,可能你都会用上它,接下来我们看下它到底有什么神通。
中间件函数
请求中间件 是一个异步函数,你可以在useRequest、useWatcher、useFetcher中定义请求中间件。以下是一个简单的请求中间件,它在请求前和请求后分别打印了一些信息,没有改变任何请求行为。
useRequest(todoList, {
  async middleware(_, next) {
    console.log('before request');
    await next();
    console.log('after requeste');
  }
});
这里有几点你需要知道的,有关next函数调用的问题,这个函数也是一个异步函数,调用它可以继续发送请求,此时将会把 loading 状态设置为 true,然后发送请求。next 的返回值是带有响应数据的 Promise 实例,你可以在中间件函数中操纵返回值。
控制响应数据
中间件函数的返回值将作为本次请求的响应数据参与后续的处理,如果中间件没有返回任何数据但调用了 next,则会将本次请求的响应数据参与后续处理。
// 将会以修改后的result作为响应数据
useRequest(todoList, {
  async middleware(_, next) {
    const result = await next();
    result.code = 500;
    return result;
  }
});
// 将会以本次请求的响应数据参与后续处理
useRequest(todoList, {
  async middleware(_, next) {
    await next();
  }
});
// 将会以字符串abc作为响应数据
useRequest(todoList, {
  async middleware(_, next) {
    await next();
    return 'abc';
  }
});
这里还有一个特例,当既没有调用 next,又没有返回值时,将不再执行后续的处理,这表示onSuccess、onError、onComplete响应事件不会被触发。
useRequest(todoList, {
  async middleware() {}
});
更改请求
有时候你想要更改请求,此时可以在 next 中指定另一个 method 实例,在发送请求时就会将这个 method 中的信息进行请求,同时你还可以通过 next 设置是否强制请求来穿透缓存,这也很简单。
useRequest(todoList, {
  async middleware(_, next) {
    await next({
      // 更改请求的method实例
      method: newMethodInstance,
      // 本次是否强制请求
      force: true
    });
  }
});
控制错误
捕获错误
在中间件中,可以捕获 next 中产生的请求错误,捕获后,全局的onError钩子不再触发。
useRequest(todoList, {
  async middleware(_, next) {
    try {
      await next();
    } catch (e) {
      console.error('捕获到错误', e);
    }
  }
});
抛出错误
当然,也可以在中间件中抛出一个自定义错误,即使请求正常也将会进入请求错误的流程。
// 未发出请求,同时还会触发全局的以及请求级的onError,如果是通过`method.send`发送的请求将返回reject的promise实例
useRequest(todoList, {
  async middleware(_, next) {
    throw new Error('error on before request');
    await next();
  }
});
// 请求成功后,将触发全局的以及请求级的onError,如果是通过`method.send`发送的请求将返回reject的promise实例
useRequest(todoList, {
  async middleware(_, next) {
    await next();
    throw new Error('error on after request');
  }
});
控制响应延迟
在中间件中我们可以延迟响应,也可以提前响应,在提前的情况下,虽然获取不到响应数据,但可以返回一些其他的数据作为响应数据参与后续的处理。
// 延迟1秒响应
useRequest(todoList, {
  async middleware(_, next) {
    await new Promise(resolve => {
      setTimeout(resolve, 1000);
    });
    return next();
  }
});
// 立即响应,并使用字符串abc作为响应数据
useRequest(todoList, {
  async middleware(_, next) {
    return 'abc';
  }
});
不止于此
至此,我们所提及的都是中间件的第二个参数 next 的使用,那第一个参数是做什么的呢?
中间件第一个参数中包含了本次请求的一些信息,以及对loading、data和onSuccess等 useHook 中返 回的状态和事件的控制函数。我们接着往下看!
包含的请求信息
- front hooks
 - fetcher hook
 
以下为 useRequest 和 useWatcher 的中间件所包含的请求信息
async function alovaFrontMiddleware(context, next) {
  // 本次请求的method实例
  context.method;
  // send函数发送的参数数组,默认为[]
  context.sendArgs;
  // 本次请求命中的缓存数据
  context.cachedResponse;
  // useHook的配置集合
  context.config;
  // useHook返回的各项状态,包含以下属性
  // loading、data、error、downloading、uploading,以及通过managedStates管理的额外状态
  context.frontStates;
  // ...
}
以下为 useFetcher 的中间件所包含的请求信息
async function alovaFetcherMiddleware(context, next) {
  // 本 次请求的method实例
  context.method;
  // 由useFetcher的fetch传入的参数组,默认为[]
  context.fetchArgs;
  // 本次请求命中的缓存数据
  context.cachedResponse;
  // useHook的配置集合
  context.config;
  // useHook返回的各项状态,包含以下属性
  // fetching、error、downloading、uploading
  context.fetchStates;
  // ...
}
接下来,我们再来看看有哪些控制能力。
修改响应式数据
使用context.update修改响应式数据。
- front hooks
 - fetcher hook
 
async function alovaFrontMiddleware(context, next) {
  context.update({
    // 提前修改loading状态为true
    loading: true,
    // 修改data值,如设置自定义的初始化数据
    data: {
      /* ... */
    }
  });
  // ...
}
async function alovaFetcherMiddleware(context, next) {
  context.update({
    // 提前修改fetching状态为true
    fetching: true,
    // 修改error的值
    error: new Error('custom midleware error')
  });
  // ...
}
装饰事件
你还可以在中间件中装饰onSuccess、onError、onComplete回调函数,让它们变得更丰富,例如改变回调函数的参数,又或者接收回调函数的返回值,实现更多的功能。
你可以使用decorateSuccess、decorateError、decorateComplete函数来装饰回调函数。下面将成功回调作为示例,它装饰了 3 处地方:
- 为 event 对象新增了
custom属性; - 为成功回调函数新增了第二个参数,值为
extra data; - 接收第二个成功回调函数的值,并打印它;
 
const { onSuccess } = useRequest(todoList, {
  // ...
  async middleware(context, next) {
    // 装饰成功回调函数,以下函数参数解释:
    // handler: 绑定的回调函数
    // event: 回调函数对应的事件对象
    // index: 回调函数下标,表示当前正在执行第几个回调函数
    // length: 回调函数绑定个数
    context.decorateSuccess((handler, event, index, length) => {
      event.custom = 1;
      const received = handler(event, 'extra data');
      if (index === 1) {
        console.log(`接收到第${index + 1}个回调函数的返回值:`, received);
        // [打印信息] 接收到第2个回调函数的返回值:I'm second handler
      }
    });
    // ...
  }
});
onSuccess((event, extra) => {
  console.log(event.custom); // 1
  console.log(extra); // extra data
});
onSuccess((event, extra) => {
  return "I'm second handler";
});
decorateError、decorateComplete的用法与decorateSuccess相同。
中断或重复发送请求
在中间件中还可以接收到 use hooks 返回的abort和send函数(useFetcher 中为fetch),你还可以在触发一次请求意图时,发送多个请求。
典型的使用例子是请求重试,发送一次请求后如果请求失败将自动按一定策略再次请求,重试成功后再触发onSuccess。以下为简单的请求重试示例代码。
- front hooks
 - fetcher hook
 
async function alovaFrontMiddleware(context, next) {
  return next().catch(error => {
    if (needRetry) {
      setTimeout(() => {
        context.send(...context.sendArgs);
      }, retryDelay);
    }
    return Promise.reject(error);
  });
}
async function alovaFetcherMiddleware(context, next) {
  return next().catch(error => {
    if (needRetry) {
      setTimeout(() => {
        context.fetch(context.method, ...context.fetchArgs);
      }, retryDelay);
    }
    return Promise.reject(error);
  });
}
如果需要在中间件内中断请求,可以调用context.abort()。
受控的加载状态
在上面内容中,我们知道了可以通过context.update自定义修改响应式数据,不过当你在修改加载状态值(loading或fetching)时将会有所阻碍,因为在正常情况下,加载状态值会在调用next时自动设置为 true,在响应流程中自动设置 false,这将覆盖通过context.update修改的加载状态值,此时我们可以开启受控的加载状态,开启后,在next函数和响应流程将不再修改加载状态值,而由我们完全控制。
我们还是以请求重试为例,我们希望在触发一次请求意图开始,经过请求重试直到请求结束为止,加载状态一直保持为 true。
- front hooks
 - fetcher hook
 
在 useRequest 和 useWatcher 的中间件中,使 用context.controlLoading开启自定义控制加载状态。
async function alovaFrontMiddleware(context, next) {
  context.controlLoading();
  // 请求开始时设置为true
  context.update({ loading: true });
  return next()
    .then(value => {
      // 请求成功后设置为false
      context.update({ loading: false });
      return value;
    })
    .catch(error => {
      if (needRetry) {
        setTimeout(() => {
          context.send(...context.sendArgs);
        }, retryDelay);
      } else {
        // 不再重试时也设置为false
        context.update({ loading: false });
      }
      return Promise.reject(error);
    });
}