request/Requester.js

import {makeAssignableMethod} from '../operationKit';

/**
 * 请求管理器,负责对接口请求进行各种封装处理,详见{@tutorial 2.3-request}
 */
class Requester{
  _underlayRequest = null; //底层网络api,功能格式同wx.request
  _plugins = []; //插件列表

  /**
   * 构造函数
   * @param {Object} [configOptions] 配置参数,参见{@link Requester#config}
   */
  constructor(configOptions){
    configOptions && this.config(configOptions);
  }

  /**
   * 配置
   * @param {object} configOptions
   * @param {function} [configOptions.underlayRequest] 底层网络api,功能格式同[wx.request]{@link https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html}
   * @param {Array<BasePlugin>} [configOptions.plugins] 插件列表
   */
  config(configOptions){
    const defaultOpts = {
      underlayRequest: wx.request,
      plugins: [],
    };

    configOptions = Object.assign({}, defaultOpts, configOptions);

    this._underlayRequest = configOptions.underlayRequest;
    this._plugins = configOptions.plugins;
    
    for (let plugin of this._plugins) {
      plugin.mount({
        requester: this,
      });
    }
  }

  /**
   * 发送请求
   * @param {Requester~ReqOptions} reqOptions
   * @param {Requester~ManageOptions} [manageOptions]
   * @return {*|Requester~ReqRes} 成功时resolve接口数据,失败时reject完整请求结果
   * 
   * @example
   * let fetchData = requester.request({
   *   url: ''
   * });
   * 
   * fetchData.then(resp=>{ //成功时,返回结果直接是服务端返回的数据(wx.request success回调中的res.data)
   *   //resp数据格式,取决于服务端返回内容,和请求header中dataType等字段的设置
   *   console.log('server data:', resp);
   * });
   * fetchData.catch(res=>{ //失败时,返回结果是完整请求结果(wx.request fail回调中的整个res)
   *   console.log('errMsg:', res.errMsg); 
   * });
   */
  async request(reqOptions, manageOptions={}){
    //保存回调(兼容起见支持回调,但更建议以Promise形式使用)
    let {success, fail, complete} = reqOptions;
    delete reqOptions.success;
    delete reqOptions.fail;
    delete reqOptions.complete;
    
    //发出请求
    let reqRes = await this._request({
      reqOptions,
      manageOptions: {
        thisIssuer: manageOptions.thisIssuer,
      }
    });
    
    //处理回调
    if (reqRes.succeeded) {
      success && success(reqRes);
      complete && complete(reqRes);
    } else {
      fail && fail(reqRes);
      complete && complete(reqRes);
    }
    
    //返回结果
    return reqRes.succeeded ? Promise.resolve(reqRes.data) : Promise.reject(reqRes);
  }

  /**
   * 在requester对象上注册方法,用于提供便捷调用 
   * e.g.注册requestWithLogin方法便于直接进行需要登录态的接口调用
   * 要求新注册方法名不得与已有方法名冲突
   * @param {string} methodName 方法名
   * @param {Function} methodFunc 方法函数
   */
  registerToThis({methodName, methodFunc}){
    if (this[methodName]) {
      console.error(`[requester] registerToThis failed, method:"${methodName}" already exist`);
      return;
    }
    
    this[methodName] = methodFunc;
  }
  
  /**
   * 发出请求
   * @param {Requester~ReqOptions} reqOptions 请求参数
   * @param {Requester~ManageOptions} manageOptions 管理参数
   * @return {Requester~ReqRes} 请求结果
   * @private
   */
  async _request({reqOptions, manageOptions={}}){
    //参数处理
    const defaultManageOpts = {
      disableRetry: false,
    };
    manageOptions = Object.assign({}, defaultManageOpts, manageOptions);
    
    //执行各插件的beforeRequest/beforeRequestAsync钩子函数
    let beforeRes = await this._beforeRequest({reqOptions, manageOptions});
    switch (beforeRes.action) {
      case 'cancel': //取消接口请求
        let errMsg = `cancelled by plugin "${beforeRes.plugin.pluginName}" before request issued,reason: ${beforeRes.errMsg}`;
        console.warn('[Requester] 接口请求被取消,errMsg:', errMsg, 'url:', reqOptions.url);
        
        return {
          succeeded: false, 
          errMsg,
        };
      case 'continue': //继续默认流程
        break;
      case 'feed': //返回指定内容
        break;
      default:
        console.error('[Requester] dealing with beforeRes, unknown action:', beforeRes.action);
    } 
    
    //调用接口
    let reqRes = beforeRes.action==='feed' ? beforeRes.feedRes : await this._doRequest({reqOptions, manageOptions});
    
    //执行各插件的afterRequest/afterRequestAsync钩子函数
    let afterRes = await this._afterRequest({reqOptions, reqRes, manageOptions});
    switch (afterRes.action) {
      case 'retry':
        return manageOptions.disableRetry ? reqRes : this._request({
          reqOptions,
          manageOptions: {
            ...manageOptions,
            disableRetry: true, //只允许重试一次,避免死循环
          }
        });
      case 'override':
        return afterRes.overrideRes;
      case 'continue':
        return reqRes;
      default:
        console.error('[Requester] dealing with afterRes, unknown action:', afterRes.action);
        return reqRes;
    }
  }

  /**
   * 处理 请求发起前 的各种扩展逻辑
   * @param {Requester~ReqOptions} reqOptions 请求参数
   * @param {Requester~ManageOptions} manageOptions 管理参数
   * @return {Requester~BeforeRequestRes} 处理结果
   * @private
   */
  async _beforeRequest({reqOptions, manageOptions}){
    let finalRes = {action: 'continue'};

    for (let plugin of this._plugins) {
      //调用钩子函数
      let pluginRes = await this._execPluginHook({
        plugin,
        hook: 'beforeRequest',
        args: {reqOptions},
        defaultRes: {action: 'continue'},
        manageOptions,
      });

      //格式检查&规整
      switch (pluginRes.action) {
        case 'feed': //返回指定内容
          let checkRes = formatCheckReqRes(pluginRes.feedRes); //检查内容格式是否正确
          if (!checkRes.pass) { //若格式不正确,则无视该处理指令
            console.error(
              `[Requester] feedRes格式不正确:${checkRes.errMsg},该处理已被忽略。`,
              'pluginName:', plugin.pluginName,
              'pluginRes:', pluginRes,
              'reqOptions:', reqOptions
            );
            pluginRes.action = 'continue';
          }
          break;
        default:
      }
      
      //处理返回结果
      switch (pluginRes.action) {
        case 'continue': //继续默认流程
          break;
        case 'cancel': //取消接口调用
          finalRes = {action: 'cancel', plugin, errMsg: pluginRes.errMsg};
          return finalRes;
        case 'feed': //返回指定内容
          finalRes = {action: 'feed', plugin, errMsg: pluginRes.errMsg, feedRes: pluginRes.feedRes};
          return finalRes;
        default:
          console.error('[Requester] beforeRequest/beforeRequestAsync, unknown action:', pluginRes.action, 'pluginName:', plugin.pluginName);
      }
    }

    return finalRes;
  }

  /**
   * 调用接口
   * @param {Requester~ReqOptions} reqOptions 请求参数
   * @param {Requester~ManageOptions} manageOptions 管理参数
   * @return {Requester~ReqRes} 请求结果
   * @private
   */
  async _doRequest({reqOptions, manageOptions}){
    return await new Promise((resolve)=>{
      this._underlayRequest({
        ...reqOptions,
        success(res){
          resolve(Object.assign({succeeded: true}, res))
        },
        fail(res){
          resolve(Object.assign({succeeded: false}, res))
        },
        complete: null,
      });
    });
  }

  /**
   * 处理 请求返回后 的各种扩展逻辑
   * @param {Requester~ReqOptions} reqOptions 请求参数
   * @param {Requester~ReqRes} 请求结果
   * @param {Requester~ManageOptions} manageOptions 管理参数
   * @return {Requester~AfterRequestRes} 处理结果
   * @private
   */
  async _afterRequest({reqOptions, reqRes, manageOptions}){
    let finalRes = {action: 'continue'};
    
    for (let plugin of this._plugins) {
      //调用钩子函数
      let pluginRes = await this._execPluginHook({
        plugin,
        hook: 'afterRequest',
        args: {reqOptions, reqRes},
        defaultRes: {action: 'continue'},
        manageOptions,
      });

      //格式检查&规整
      switch (pluginRes.action) {
        case 'override': 
          let checkRes = formatCheckReqRes(pluginRes.overrideRes); //检查内容格式是否正确
          if (!checkRes.pass) { //若格式不正确,则无视该处理指令
            console.error(
              `[Requester] overrideRes格式不正确:${checkRes.errMsg},该处理已被忽略。`,
              'pluginName:', plugin.pluginName,
              'pluginRes:', pluginRes,
              'reqOptions:', reqOptions
            );
            pluginRes.action = 'continue';
          }
          break;
        default:
      }
      
      //处理返回结果
      switch (pluginRes.action) {
        case 'continue':
          break;
        case 'override':
          finalRes = {
            action: 'override',
            overrideRes: pluginRes.overrideRes
          };
          reqRes = finalRes.overrideRes;
          break;
        case 'retry':
          finalRes = {action: 'retry'};
          return finalRes;
        default:
          console.error('[Requester] afterRequest/afterRequestAsync, unknown action:', pluginRes.action, 'pluginName:', plugin.pluginName);
      }
    }
    
    return finalRes;
  }

  /**
   * 执行插件的钩子函数
   * @param {BasePlugin} plugin 插件
   * @param {string} hook 钩子
   * @param {object} args 传给钩子函数的参数
   * @param {object} defaultRes 钩子函数默认返回值
   * @param {Requester~ManageOptions} manageOptions 管理选项
   * @return {object} 钩子函数最终返回值
   * @private
   */
  async _execPluginHook({plugin, hook, args, defaultRes, manageOptions}){
    //补充公共参数
    args = {
      ...args,
      thisIssuer: manageOptions.thisIssuer,
    };
    
    //执行插件钩子
    let syncRes = null, asyncRes = null;
    try {
      syncRes = plugin[hook] ? plugin[hook](args) : null;
      asyncRes = plugin[`${hook}Async`] ? await plugin[`${hook}Async`](args) : null;
    } catch (e) {
      console.error(`[Requester] ${hook}/${hook}Async, caught error:`, e, 'pluginName:', plugin.pluginName, 'args:', args);
      syncRes = null;
      asyncRes = null;
    }
    
    //返回执行结果
    return Object.assign({}, defaultRes, syncRes, asyncRes);
  }

  /**
   * 将方法封装为通用函数,使之可以在任意this对象上执行
   * @param {string} methodName 方法名
   * @return {function} 封装后的函数
   */
  makeAssignableMethod(methodName){
    return makeAssignableMethod({
      instance: this,
      method: methodName,
      rcvThis: {
        argIdx: 1,
        argProp: 'thisIssuer'
      }
    })
  }
}

/**
 * @typedef {object} Requester~ReqOptions 接口请求参数,格式同[wx.request]{@link https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html}
 * @property {string} url 开发者服务器接口地址
 * @property {string|object|ArrayBuffer} [data] 请求的参数
 * @property {object} [header] 设置请求的 header,header 中不能设置 Referer
 * @property {string} [method='GET'] HTTP 请求方法
 * @property {string} [dataType='json'] 返回的数据格式
 * @property {string} [responseType='text'] 响应的数据类型
 * @property {function} [success] 兼容起见支持回调,但更建议以Promise形式使用
 * @property {function} [fail] 兼容起见支持回调,但更建议以Promise形式使用
 * @property {function} [complete] 兼容起见支持回调,但更建议以Promise形式使用
 */

/**
 * @typedef {object} Requester~ManageOptions 接口请求管理选项
 * @property {object} thisIssuer 发起接口请求的this对象
 * @property {boolean} disableRetry 是否禁止重试(避免无限次重试接口调用导致死循环)
 */

/**
 * @typedef {object} Requester~ReqRes 接口请求结果,除标注了“模块补充”的字段外,格式同[wx.request]{@link https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html}
 * @property {boolean} succeeded 模块补充字段,请求是否成功(服务器返回即算成功,包括404/500等,网络异常等导致请求未正常返回才算失败)
 * @property {string|Object|ArrayBuffer} [data]	(成功时)开发者服务器返回的数据
 * @property {number} [statusCode] (成功时)开发者服务器返回的 HTTP 状态码
 * @property {Object} [header]	(成功时)开发者服务器返回的 HTTP Response Header
 * @property {string} [errMsg] (失败时)错误信息
 */
/**
 * 格式检查,判断传入的数据是否符合Requester~ReqRes格式要求
 * @param {*} res 待判断的数据
 * @return {{pass: boolean, errMsg: string}}
 * @ignore
 */
function formatCheckReqRes(res) {
  if (!res) {
    return {
      pass: false,
      errMsg: '应为对象格式'
    }
  }
  
  if (typeof res.succeeded !== 'boolean') {
    return {
      pass: false,
      errMsg: '“succeeded”字段应为boolean类型'
    }
  }
  
  if (res.succeeded && !("data" in res)) {
    return {
      pass: false,
      errMsg: '“data”字段缺失'
    }
  }
  
  return {
    pass: true,
    errMsg: 'ok'
  }
}

/**
 * @typedef {object} Requester~BeforeRequestRes 请求发起前的各种扩展逻辑处理结果
 * @property {string} action 期望的后续处理:'continue'-继续 | 'cancel'-终止该请求 | 'feed'-返回指定内容(如接口缓存、mock数据等)
 * @property {string} errMsg 错误信息
 * @property {Requester~ReqRes} feedRes (action==='feed'时)指定的返回内容
 * @property {BasePlugin} plugin 决定该处理方式的插件(该字段会自动添加,插件钩子函数中无需返回)
 */

/**
 * @typedef {object} Requester~AfterRequestRes 请求返回后的各种扩展逻辑处理结果
 * @property {string} action 期望的后续处理:'continue'-继续 | 'override'-以指定内容作为请求结果返回 | 'retry'-重新发送请求,并以重试结果作为本次请求结果返回
 * @property {Requester~ReqRes} [overrideRes] action==='override'时,作为请求结果的指定内容
 */
export default Requester;