0
点赞
收藏
分享

微信扫一扫

大语言模型LLM应用篇

灵魂跑者 2024-05-15 阅读 31

封装一个规范的请求通常涉及到以下几个方面:

  1. 请求方法(GET、POST 等)
  2. 请求参数构造(根据使用的请求库,包括不同 Method 要求的 params 格式、空值处理等)
  3. 格式转换(如请求体和返回体参数的下划线格式和驼峰格式转换,一般前端会用驼峰命名格式,后台会用下划线命名格式)
  4. 请求头构造(根据不同请求类型,比如 json、form、multipart)
  5. 返回体处理(包括成功和错误处理等)

下面是一个具体的实现:

src/app.ts

import { viewClient } from 'src/request/index';
const info = await viewClient.getUserInfo();
console.log(info);

src/request/index.ts

import UserClient from './UserClient';
export const viewClient = new ViewClient();

src/request/UserClient.ts

class UserClient extends ClientBase {
	constructor() {
		super('/center-api');
	}
	getUserInfo(params) {
		return this.get('/v1/user/getUserInfo', params);
	}
	updateUserInfo(params) {
		return this.post('/v1/user/updateUserInfo', params);
	}
	uploadData(params) {
		return this.post('/v1/user/uploadData', params, {
			dataType: 'multipart'
		})
	}
	downloadData(params) {
		return this.get('/v1/user/downloadData', params, {
			responseType: 'blob'
		})
	}
}
export default UserClient;

src/request/ClientBase.ts

import axios from 'axios';
import humps from 'humps';
import qs from 'qs';
import isPlainObject from 'lodash/isPlainObject';
import { ElMessage } from 'element-plus';

interface IOption {
  underscoreRequestData: boolean; // 是否将入参对象的键从驼峰式命名转换为下划线分隔的形式
  camelizeResponseData: boolean; // 是否将返回体对象的键从下划线分割命名转换为驼峰式的形式
  removeEmptyValue: boolean; // 是否删除空入参
  removeEmptyValueTypeList: unknown[]; // value 值等于什么时被视为空入参 例如 [null, undefined, NaN, '']
  dataType: string; // request Content-Type
  responseType: string; // response Content-Type
}
type PartialIOption = Partial<IOption>;
type IOptionDefault = Pick<
  PartialIOption,
  'underscoreRequestData' | 'camelizeResponseData' | 'dataType'
>;
const DEFAULT_OPTIONS: IOptionDefault = {
  underscoreRequestData: true,
  camelizeResponseData: true,
  dataType: 'json',
};
export class ClientBase {
  // 不同实体 API 请求的前缀,如 UserClient 为 /center-api
  apiUrlPrefix: string;
  // 请求的配置项
  options: PartialIOption;
  // 是否登录中
  isLogging: boolean;
  /**
   * constructor
   * @param apiURLPrefix
   * @param options
   */
  constructor(apiUrlPrefix = '', options = {}) {
    this.apiUrlPrefix = apiUrlPrefix;
    this.options = {
      ...DEFAULT_OPTIONS,
      ...options,
    };
    this.isLogging = false;
  }
  /**
   * get, post, put, delete
   * @param url 请求api
   * @param data 入参
   * @param options 特定请求额外配置项
   * @returns
   */
  get(url: string, data?: any, options?: any) {
    return this._request(url, 'get', data, options);
  }
  post(url: string, data?: any, options?: any) {
    return this._request(url, 'post', data, options);
  }
  put(url: string, data?: any, options?: any) {
    return this._request(url, 'put', data, options);
  }
  delete(url: string, data?: any, options?: any) {
    return this._request(url, 'delete', data, options);
  }
  /**
   * _request
   * @param url
   * @param method
   * @param data
   * @param options
   * @returns
   */
  private _request(url: string, method = 'get', data: any = {}, options: PartialIOption = {}) {
    method = method.toUpperCase(); // GET, POST, PUT, DELETE
    // 'GET', 'DELETE'
    let query: any = {};
    // 'PUT', 'POST'
    let body: any = {};
    switch (method) {
      case 'GET':
      case 'DELETE':
        query = data;
        break;
      case 'PUT':
      case 'POST':
        body = data;
        break;
    }

    const apiUrlPrefix = this.apiUrlPrefix;
    const configurl = `${apiUrlPrefix}${url}`; // 例如 /center/v1/user/getUserInfo
    // 构造符合 axios 要求的请求配置
    const config = this._createRequestConfig(method, configurl, query, body, options);
    // 针对入参进行空值处理
    config.params = this._removeEmptyValueFun(config.params, options);
    config.data = this._removeEmptyValueFun(config.data, options);

    // request Content-Type
    const dataType = options.dataType || this.options.dataType;
    if (dataType === 'json') {
      // 用于发送 JSON 格式的数据,常用于 Web API 请求和前后端数据交互
      config.headers = {
        ...(config.headers || {}),
        'Content-Type': 'application/json',
      };
    }
    if (dataType === 'form') {
      // 用于提交 HTML 表单数据,是默认的表单提交方式。表单数据会被编码为键值对(key-value pairs),并以 & 符号分隔
      config.headers = {
        ...(config.headers || {}),
        'Content-Type': 'application/x-www-form-urlencoded',
      };
      config.data = qs.stringify(config.data);
    }
    if (dataType === 'multipart') {
      // 用于提交表单数据和二进制文件,如图片、视频等。这种类型通常用于文件上传操作
      config.headers = {
        ...(config.headers || {}),
        'Content-Type': 'multipart/form-data',
      };
    }
    // 其他类型
    // 'Content-Type': 'text/plain' 用于发送纯文本数据,通常用于发送简单的文本信息,如日志、报告等
    // 'Content-Type': 'application/octet-stream' 用于发送二进制数据,如程序、压缩包、文档等。

    // delete data key when it's {}
    if (config.data && isPlainObject(config.data) && !Object.keys(config.data).length) {
      delete config.data;
    }

    // 发起请求
    // 1. 假如返回 blob 类型数据 需要特殊处理 _handleFileSuccess
    if (options.responseType === 'blob') {
      config.responseType = 'blob';
      return axios(config)
        .catch(this._handleFail.bind(this))
        .then((res: any) => {
          this._handleFileSuccess(res);
        });
    }
    // 2. 返回数据默认处理 _handleSuccess
    return axios(config)
      .catch(this._handleFail.bind(this))
      .then((res) => {
        return this._handleSuccess([res, config]);
      });
  }
  /**
   * _createRequestConfig 构造符合 axios 要求的请求配置
   * @param method
   * @param url
   * @param params
   * @param data
   * @param options
   * @returns
   */
  private _createRequestConfig(
    method: string,
    url: string,
    params: any,
    data: any,
    options: PartialIOption
  ) {
    // https://axios-http.com/zh/docs/req_config
    // 是否需要对请求入参 key 转为下划线格式
    if (this._shouldUnderscoreRequest(options)) {
      params = humps.decamelizeKeys(params || {});
      data = humps.decamelizeKeys(data || {});
    }
    // axios configuration
    const config: any = {
      url,
      method,
      params,
      data,
      ...options,
    };
    return config;
  }
  private _shouldUnderscoreRequest(config: PartialIOption) {
    if (typeof config.underscoreRequestData === 'boolean') {
      // 假如传入了具体配置项
      return config.underscoreRequestData;
    }
    // 否则用默认配置
    return this.options.underscoreRequestData;
  }
  /**
   * _removeEmptyValueFun 针对入参进行空值处理
   * @param data
   * @param options
   * @returns
   */
  private _removeEmptyValueFun(data: any, options: PartialIOption) {
    const { removeEmptyValue = true, removeEmptyValueTypeList = [null, undefined, NaN] } = options;
    let rmTypeArray: unknown[] = [];
    if (removeEmptyValue) {
      rmTypeArray = ['', ...removeEmptyValueTypeList];
    } else {
      rmTypeArray = removeEmptyValueTypeList;
    }
    if (Array.isArray(data)) {
      data = data.reduce((r, val) => {
        if (!rmTypeArray.includes(val)) {
          r.push(val);
        }
        return r;
      }, []);
    } else if (isPlainObject(data)) {
      data = Object.keys(data).reduce((r: any, key) => {
        const val = data[key];
        if (!rmTypeArray.includes(val)) {
          r[key] = val;
        }
        return r;
      }, {});
    }
    return data;
  }
  /**
   * _handleSuccess 处理返回
   * @param param0
   * @returns
   */
  private _handleSuccess([resp, config]: [any, any]) {
    const { data } = resp;
    this._handleSSO(data);

    // error message handler
    if (
      data.code &&
      !data.code.toString().startsWith('2') &&
      !data.code.toString().startsWith('3')
    ) {
      // 1. message 默认错误
      // 2. display_msg 业务错误
      // 3. debug_msg 后台错误
      const errorMessage = data.display_msg || data.debug_msg || data.message || 'Request Error';
      ElMessage({
        message: errorMessage,
        type: 'error',
      });
      // 收集错误信息
      const error: any = new Error(errorMessage);
      error.resultCode = data.result_code;
      error.rawMsg = data.message;
      error.displayMsg = data.display_msg;
      error.debugMsg = data.debug_msg;
      error.requestId = data.request_id;
      error.config = config;
      error.responsed = true;
      throw error;
    }

    if (this._shouldCamelizeResponse(config)) {
      data.data = humps.camelizeKeys(data.data);
    }

    return data.data;
  }
  private _shouldCamelizeResponse(config: PartialIOption) {
    if (typeof config.camelizeResponseData === 'boolean') {
      // 假如传入了具体配置项
      return config.camelizeResponseData;
    }
    // 否则用默认配置
    return this.options.camelizeResponseData;
  }
  /**
   * _handleFileSuccess 处理 blob 格式返回
   * @param resp
   */
  _handleFileSuccess(resp: any) {
    const reader = new FileReader();
    reader.onload = (e: any) => {
      try {
        // 业务错误会放在返回resp.data里
        // 因此当JSON.parse解析正常代表返回的是包含业务错误的数据 而不是 blob 文件数据
        const data = JSON.parse(e.target?.result);
        const errorMessage = data.display_msg || data.debug_msg || data.message || 'Request Error';
        ElMessage({
          message: errorMessage,
          type: 'error',
        });
      } catch (error) {
        // 代表是一个 blob 文件数据
        const href = window.URL.createObjectURL(new Blob([resp.data]));
        const link = document.createElement('a');
        link.href = href;
        link.setAttribute('target', 'blank');
        if (resp.headers['content-disposition']) {
          // 包含文件名信息
          const cd = resp.headers['content-disposition'];
          const fl = /.+?filename=(.+)/.exec(cd);
          const filename = (fl && fl[1]) || 'unknown';
          link.setAttribute('download', decodeURI(filename));
        } else {
          link.setAttribute('download', 'unknown');
        }
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      }
    };
    reader.readAsText(new Blob([resp.data]));
  }
  /**
   * _handleFail 请求错误处理
   * @param error
   */
  private _handleFail(error: { response: any; request: any; config: any }) {
    const { response, request, config } = error;
    let errorMessage = 'request error,try again later please!';
    let errorCode = 'request_error';
    let err: any;
    if (response) {
      const { data } = response;
      if (!data) {
        // 请求本身错误
        errorMessage = 'network error, try again later please!';
        errorCode = 'network_error';
      } else {
        // 后台处理后返回错误
        const { result_code, display_msg, debug_msg, message } = data;
        errorMessage =
          display_msg || debug_msg || message || 'request error,try again later please!';
        errorCode = result_code;
        err.requestId = data.request_id;
      }
      err = new Error(errorMessage);
      err.resultCode = errorCode;
      err.responsed = true;
    }
    if (request) {
      // 请求本身错误
      errorMessage = 'network error, try again later please!';
      errorCode = 'network_error';
      err = new Error(errorMessage);
      err.resultCode = errorCode;
      err.responsed = false;
    }
    if (!err) {
      throw error;
    } else {
      err.config = config;
      throw err;
    }
  }
  private _handleSSO(data: any) {
    if (data.code === 401) {
      if (this.isLogging === true) {
        return;
      }
      this.isLogging = true;
      // getSSOUrl 获取sso登录url
      getSSOUrl()
        .then((res) => {
          if (res) {
            window.location.href = res;
          }
        })
        .finally(() => {
          this.isLogging = false;
        });
      return;
    }
  }
}

举报

相关推荐

0 条评论