request/plugin/CloudFuncPlugin.js

import BasePlugin from './BasePlugin';
import Cookie from '../../Cookie';

/**
 * 请求管理-云函数插件
 * 将云函数封装成http接口形式使用,便于:
 * 1. 使用requester提供的各种逻辑扩展能力
 * 2. 后续在云函数和后端服务器之间进行各种业务的相互迁移
 * 
 * 详见{@tutorial 2.3-request}
 * @extends BasePlugin
 */
class CloudFuncPlugin extends BasePlugin{
  _fakeDomain = '';
  _fakeRootPath = '';

  /**
   * 构造函数
   * @param {string} [pluginName='CloudFuncPlugin'] 插件名称
   * @param {string} [fakeDomain='cloud.function'] 虚拟域名
   * @param {string} [fakeRootPath='/'] 虚拟根路径
   * @example 使用默认配置
   * let requester = new Requester({
   *   plugins: [
   *     new CloudFuncPlugin() //使用默认配置
   *   ]
   * });
   * 
   * //则调用接口 
   * let res = await requester.request({
   *   url: 'https://cloud.function/xxx?a=1&b=2'
   * });
   * //等价于调用云函数 
   * let res = await wx.cloud.callFunction({
   *   name: 'xxx',
   *   data: {
   *     a: "1",
   *     b: "2",
   *   }
   * })
   *
   * @example 自定义虚拟域名和虚拟路径
   * let requester = new Requester({
   *   plugins: [
   *     new CloudFuncPlugin({ //自定义虚拟域名和虚拟路径
   *       fakeDomain: 'fancy.com',
   *       fakeRootPath: '/demos/cloud/'
   *     })
   *   ]
   * });
   *
   * //则调用指定虚拟域名虚拟路径下的接口
   * let res = await requester.request({
   *   url: 'https://fancy.com/demos/cloud/xxx?a=1&b=2'
   * });
   * //等价于调用对应云函数
   * let res = await wx.cloud.callFunction({
   *   name: 'xxx',
   *   data: {
   *     a: "1",
   *     b: "2",
   *   }
   * })
   * 
   * @example 云函数实现
   *  exports.main = async (event, context) => {
        //云函数格式约定:
        
        let {a, b} = event; //调用方传入的参数可以通过event获取
        a = Number(a); //参数类型统一为string
        b = Number(b);
      
        //此外,还会额外拼入一些http相关参数
        let {reqHeader} = event; //http请求header信息
        console.log(reqHeader.cookie); //header中的cookie字段会解析成对象格式,形如:{uid: 'xxx'}
      
        //处理结果正常返回
        let result = { sum: a+b };
        
        //此外,有一些保留字段可以用于设置http相关参数
        result.resStatusCode = 200; //设置http状态码
        result.resHeader = {}; //设置http返回结果中的header信息
        result.resHeader['Set-Cookie'] = [ //同一header有多条记录时,以数组形式设置
          'uid=xxx;expires=111',
          'sessionKey=yyy;expires=222'
        ] 
        
        return result;
      }

   */
  constructor({pluginName='CloudFuncPlugin', fakeDomain='cloud.function', fakeRootPath='/'}={}){
    super({
      pluginName
    });
    
    //参数处理
    //在路径前后补充斜杠
    fakeRootPath = /\/$/.test(fakeRootPath) ? fakeRootPath : fakeRootPath+'/';
    fakeRootPath = /^\//.test(fakeRootPath) ? fakeRootPath : '/'+fakeRootPath;
    
    //参数配置
    this._fakeDomain = fakeDomain;
    this._fakeRootPath = fakeRootPath;
  }
  
  async beforeRequestAsync({reqOptions}){
    //将http请求解析成云函数调用
    let {hit, funcName, funcParams} = this._parseReq({reqOptions});
    if (!hit) //不是云函数调用,不作处理
      return;
    
    //调用云函数
    let cloudRes = await this._execCloudFunc({funcName, funcParams});
    
    //将云函数返回结果解析成http请求结果
    let reqRes = this._parseRes({cloudRes});
    
    //返回结果
    return {
      action: 'feed',
      feedRes: reqRes,
    }
  }

  /**
   * 解析请求,将http请求解析成云函数调用
   * @param {Requester~ReqOptions} reqOptions
   * @return {{hit: boolean, funcName: string, funcParams: object}} 解析结果,格式形如:{
   *   hit: true,  //是否为云函数调用
   *   funcName: 'xxx', //云函数函数名
   *   funcParams: { //云函数入参
   *    a: "1",
   *    b: "2",
   *    reqHeader: {}
   *   }
   * }
   * @protected
   */
  _parseReq({reqOptions}){
    //判断是否云函数调用
    if (reqOptions.url.indexOf(`https://${this._fakeDomain}${this._fakeRootPath}`) !== 0)
      return {hit: false};

    //参数解析
    let [path, queryStr=''] = reqOptions.url.split('?');
    let queryObj = {};
    queryStr.split('&').forEach(paramStr=>{
      let [name, value] = paramStr.split('=');
      if (name && value!==undefined)
        queryObj[name] = value;
    });

    let funcName = path.substring(`https://${this._fakeDomain}${this._fakeRootPath}`.length);
    let funcParams = Object.assign(queryObj, reqOptions.data);

    //参数处理

    // 调用网络请求时,不管参数原本是什么类型,传到后端时都会被转为string
    // 因而,在使用网络请求时调用方很可能不会特别关注实际类型,也不会主动做类型转换
    // 故,此处也统一进行类型规整,避免产生类型歧义
    for (let name in funcParams)
      funcParams[name] = String(funcParams[name]);
    
    //header解析
    let reqHeader = Object.assign({}, reqOptions.header);
    reqHeader.cookie = Cookie.cookieStrToObj(reqHeader.cookie);
    
    //header写入
    funcParams.reqHeader = reqHeader;
    
    //返回结果
    return {
      hit: true,
      funcName,
      funcParams,
    }
  }

  /**
   * 执行云函数
   * @param {string} funcName 函数名
   * @param {object} funcParams 函数入参
   * @return {{succeeded: boolean, result: object}} 云函数执行结果
   * @protected
   */
  async _execCloudFunc({funcName, funcParams}){
    try {
      let cloudRes = await wx.cloud.callFunction({
        name: funcName,
        data: funcParams
      });
      
      return {
        succeeded: true,
        ...cloudRes,
      };
    } catch (e) {
      console.error('[CloudFuncPlugin] failed to exec cloud func:',funcName, 'err:', e);
      return {
        succeeded: false,
        errMsg: 'failed to exec cloud func:'+funcName
      }
    }
  }

  /**
   * 解析返回结果,将云函数返回结果解析成http请求结果
   * @param {{succeeded: boolean, result: object}} cloudRes 云函数执行结果
   * @return {Requester~ReqRes} 对应的http请求结果
   * @protected
   */
  _parseRes({cloudRes}){
    //云函数执行失败
    if (!cloudRes.succeeded) {
      return {
        succeeded: false,
        errMsg: cloudRes.errMsg,
      }
    }
    
    //云函数执行成功
    
    //解析返回结果
    let result = cloudRes.result;
    
    //约定的保留字段,用于进行http相关设置
    let httpFieldMap = { //key: http字段  value:对应的保留字段
      statusCode: 'resStatusCode', //状态码
      header: 'resHeader', //头部
    };
    
    //提取保留字段,并从结果中删除
    let httpField = {};
    for (let [httpName, cloudName] of Object.entries(httpFieldMap)) {
      httpField[httpName] = result[cloudName];
      delete result[cloudName];
    }
    
    //http字段处理
    //状态码
    httpField.statusCode = httpField.statusCode ? Number(httpField.statusCode) : 200;
    
    //头部
    httpField.header = httpField.header || {};
    for (let name in httpField.header)
      httpField.header[name.toLowerCase()] = httpField.header[name];
    
    //cookie(返回头部中有'set-cookie'时,wx.request会额外返回一个cookies字段,此处予以相同处理)
    let cookies = Array.isArray(httpField.header['set-cookie']) ? httpField.header['set-cookie'] : [httpField.header['set-cookie']];
    cookies = cookies.filter(setStr=>!!setStr);
      
    //返回结果
    return {
      succeeded: true,
      errMsg: 'ok',
      data: result,
      statusCode: httpField.statusCode,
      header: httpField.header,
      cookies,
    };
  }
}

export default CloudFuncPlugin;