ts加rollup开发请求模块(2)

406 阅读18分钟

书接上文

6.helper插件

前面我们已经把基础功能开发完了,下面开始开发一些辅助包的开发,nestjs的那些装饰器看起来挺好看的我们前后端格式上统一一下也整一个

6.1 初始化

运行命令npm run create创建send-helper包

6.2 更新文件

6.2.1 安装依赖

npm i reflect-metadata --save-dev

6.2.2 更新文件

  1. package.json
  "peerDependencies": {
    "@babel/runtime-corejs3": "^5.3.0",
    "core-js": "^3.20.2",
    "reflect-metadata": "^0.1.13",
    "@light-send/utils": "latest"
  },
 "devDependencies": {
    "@light-send/utils": "file:../utils",
   ....
 }
  1. rollup.config.js
const globals = {
  '@light-send/utils':'lightUtils'
}

// 基础配置
const commonConf = {
  input: getPath("./src/index.ts"),
  external: ['reflect-metadata', '@light-send/utils'],
  plugins:[
    ...commPlugin
  ],
};

6.3 实现

6.3.1 PathDecorator.ts

index.ts

export * from "./PathDecorator";

PathDecorator.ts全部导出

6.3.1.1 Const.ts

先定义一些要用到的常量

/** 默认的连接符,这里有可能有rpc的话就不是/了*/
export const DEFAULT_UNIT = "/";
/** 装饰器获取数据的key*/
export const CONTROLLER = "Controller";
/** 装饰器获取数据的key*/
export const UNIT = "UNIT";
/** 装饰器获取数据的key*/
export const UN_LINK = "UN_LINK";
/** 装饰器获取数据的key*/
export const OPTIONS = "OPTIONS";
/** 装饰器获取数据的key*/
export const INTERCEPTOR = "INTERCEPTOR";
/** 装饰器获取数据的key*/
export const CANCEL_INTERCEPTOR = "CANCEL_INTERCEPTOR";
/** 装饰器获取数据的key*/
export const LOCAL_INTERCEPTOR = "LOCAL_INTERCEPTOR";
/** 装饰器获取数据的key*/
export const FILTER = "FILTER";
export const LOCAL_FILTER = "LOCAL_FILTER";
/** 装饰器获取数据的key*/
export const FILTER_TRUNCATION = "FILTER_TRUNCATION";
/** 装饰器获取数据的key*/
export const ERROR_CODE = "ERROR_CODE";
/** 装饰器获取数据的key*/
export const HTTP_CODE = "HTTP_CODE";
/** 装饰器获取数据的key*/
export const CATCH = "CATCH";
/** 装饰器获取数据的key*/
export const TRANSFORM_PIPE = "TRANSFORM_PIPE";
/** 装饰器获取数据的key*/
export const PIPE_INIT = "PIPE_INIT";

6.3.1.2 Enum.ts

先定义一些枚举值

/**
 * http请求方式枚举
 */
export enum MethodEnum {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
  PATCH = "PATCH",
  OPTIONS = "OPTIONS",
  HEAD = "HEAD",
  PATH = "PATH"
}

6.3.1.3 更新utils模块

新增Tool.ts

import { isFunction } from "./Judge";

/**
 * 拿到对象原型上的所有方法
 * @param target
 */
export function getClassMethodNames(target: any) {
  return Object.getOwnPropertyNames(target.prototype).filter((key) => {
    return isFunction(target.prototype[key]) && key !== "constructor";
  });
}

更改Judge.ts

... 新增
export function isString(target: any): target is string {
  return getType(target) === "string";
}
export function isUndefined(target: any): target is undefined {
  return target === undefined || target === null;
}

export function isPrimitiveEmpty(target: any): target is undefined {
  return isUndefined(target) || target === "";
}

更改typings/Util.d.ts

  type isString = (target: any)=> target is string;
  type getType = (target: any)=> Typings;
  type getClassMethodNames = (target: any)=> string[]
	type isUndefined =(target: any)=> target is undefined;
  type isPrimitiveEmpty=(target: any)=> target is undefined;
  type Utils = {
    getType: getType,
    isFunction:isFunction
    getClassMethodNames:getClassMethodNames
    isString: isString,
    isUndefined: isUndefined,
    isPrimitiveEmpty: isPrimitiveEmpty
  }

6.3.1.4 新增SendHelper.d.ts

declare namespace LightSendHelper{
  interface IOptions {
    url?: string
    method?: string
  }
}

6.3.1.5 Controller 实现

PathDecorator.js

/**
 * 合并Controller和method装饰器的路径
 * @param path
 * @param prefix
 * @param unit
 * @param unLink
 */
function computeUrl(path: string, prefix = "", unit = "/", unLink?: boolean): string {
  if (prefix && isString(prefix)) {
    const last = prefix.length - 1;
    prefix = prefix.charAt(last) === unit ? prefix.substring(0, last - 1) : prefix;
  } else {
    prefix = "";
  }
  path = path.charAt(0) !== unit ? unit + path : path;
  return unLink ? path : prefix + path;
}
/**
 * 这是一个类的装饰器,主要作用是把被这个装饰器装的
 * 的类里面所有被方法装饰器装饰过的url都加上path前缀,
 * 并且把
 * @param path 前缀
 * @param unit 连接符
 * @constructor
 */
export const Controller = <
  T extends LightSendHelper.IOptions & LightSendHelper.IInterceptOptions = LightSendHelper.IOptions
>(
  path: string,
  unit?: string
): ClassDecorator => {
  return (target) => {
    const methodsName = getClassMethodNames(target);
    methodsName.forEach((name) => {
      const metadataKey = `${METHOD_VALUE}:${name}`;
      const originMetadataKey = `${METHOD_ORIGIN_VALUE}:${name}`;
      const originValue = Reflect.getMetadata(originMetadataKey, target);
      if (!originValue) {
        Reflect.defineMetadata(originMetadataKey, target.prototype[name], target);
      }
      const value = Reflect.getMetadata(metadataKey, target.prototype) || target.prototype[name];
      type TargetValue = typeof value;
      type Return = ReturnType<TargetValue>;
      type Params = Parameters<TargetValue>;
      /**
       * 拿到所有的原型方法,对方法的功能增强
       * @param options
       * @param arg
       */
      target.prototype[name] = async function (options: T, ...arg: Omit<Params, "0">): Promise<Return> {
        const refOptions: T = Reflect.getMetadata(OPTIONS, originValue);
        const handle = value.bind(this);
        let args = [options, ...arg];
        /**
         * 如果meta上有options,这和方法的第一个参数做合并
         */
        if (refOptions) {
          const unLink: boolean = Reflect.getMetadata(UN_LINK, value);
          refOptions.url = refOptions.url ? computeUrl(refOptions.url || "", path, unit, unLink) : refOptions.url;
          args = [Object.assign({}, refOptions, options), ...arg];
        }
        return await handle(...args);
      };
      Reflect.defineMetadata(metadataKey, target.prototype[name], target.prototype);
    });
  };
};

6.3.1.6 Method装饰器实现

这里Get等方法装饰器一定要配合Controller使用才能出效果

/**
 * 方法的装饰器,主要的功能是为往被装饰器的第一个参数里面注入url和method
 * 这里的url会加上Controller装饰器的前缀
 * @param method
 */
export const createMappingDecorator = <T extends LightSendHelper.IOptions = LightSendHelper.IOptions>(
  method: string
) => {
  return (path: string, unlink?: boolean): MethodDecorator => {
    return (target, key, descriptor) => {
      const options: T = {
        url: path,
        method
      } as T;
      Reflect.defineMetadata(OPTIONS, options, descriptor.value || target);
      Reflect.defineMetadata(UN_LINK, unlink, descriptor.value || target);
    };
  };
};

export const Get = createMappingDecorator(MethodEnum.GET);
export const Post = createMappingDecorator(MethodEnum.POST);
export const Put = createMappingDecorator(MethodEnum.PUT);
export const Delete = createMappingDecorator(MethodEnum.DELETE);
export const Patch = createMappingDecorator(MethodEnum.PATCH);
export const Options = createMappingDecorator(MethodEnum.OPTIONS);
export const Head = createMappingDecorator(MethodEnum.HEAD);
/**
 * Path是给不是http请求用的
 */
export const Path = createMappingDecorator(MethodEnum.PATH);

6.4.1 Interceptor.ts

拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

(以上摘自nestjs)

这里的拦截器和Send拦截器的区别在于:

  • 可以使用装饰器的拦截器做一些副作用的功能比如loading、notice等这样不影响业务结构又能添加功能
  • 如果不喜欢装饰器的话也可以通过扩展Send拦截器做到同样的功能,两者可以独立使用

6.4.1.1 添加类型

typings/SendHelper.d.ts

declare namespace LightSendHelper{
  interface IOptions {
    url?: string
    method?: string
  }
interface IInterceptMeta<Handle = any, Context = any> {
    // 当前全局的过滤器
    filter?: LightSendHelper.IExceptionFilter,
    // 当前函数是否需要执行过滤器
    needFilter?:boolean,
    // 最原始的函数
    originValue?: Handle,
    // 最原始函数的上下文
    Context?: Context
  }

  /**
   * 拦截器会把过滤器过滤后的异常继续执行
   */
  interface IIntercept {
    intercept(handle: any, args: Parameters<typeof handle>, meta: IInterceptMeta):ReturnType<typeof handle>
  }
}

6.4.1.2 新增Interceptor.ts

import { CANCEL_INTERCEPTOR, INTERCEPTOR, LOCAL_INTERCEPTOR } from "./Const";

export const UseInterceptor = <T extends LightSendHelper.IIntercept = LightSendHelper.IIntercept>(
  interceptor: T
): ClassDecorator => {
  return (target) => {
    const methodsName = getClassMethodNames(target);
    methodsName.forEach((name) => {
      const metadataKey = `${METHOD_VALUE}:${name}`;
      const originMetadataKey = `${METHOD_ORIGIN_VALUE}:${name}`;
      /**
       * 拿到最原始的运行方法
       */
      const originValue = Reflect.getMetadata(originMetadataKey, target);
      if (!originValue) {
        /**
         * 如果没有最原始的方法则,把当前的方法存入
         */
        Reflect.defineMetadata(originMetadataKey, target.prototype[name], target);
      }
      /**
       * 拿到当前运行的方法,因为这里的方法肯能是被装饰过的和最原始的方法可能不是一个方法
       */
      const value = Reflect.getMetadata(metadataKey, target.prototype) || target.prototype[name];
      type TargetValue = typeof value;
      type Return = ReturnType<TargetValue>;
      type Params = Parameters<TargetValue>;
      /**
       * 拿到所有的原型方法,对方法的功能增强
       * @param arg
       */
      target.prototype[name] = async function (...arg: Params): Promise<Return> {
        const handle = value.bind(this);
        /**
         * 是否有全局的过滤器
         */
        const filter: LightSendHelper.IExceptionFilter = Reflect.getMetadata(FILTER, target);
        /**
         * 是否有方法的的过滤器
         */
        const localLFFilter: LightSendHelper.IExceptionFilter = Reflect.getMetadata(FILTER, target);
        /**
         * 该方法是否需要执行过滤器
         */
        const needFilter = Reflect.getMetadata(CATCH, originValue);
        /**
         * 最终要执行的过滤器
         */
        const currentFilter = localLFFilter ? localLFFilter : filter;

        /**
         * 拿到方法级别的拦截器
         */
        const localInterceptor: LightSendHelper.IIntercept = Reflect.getMetadata(LOCAL_INTERCEPTOR, originValue);
        /**
         * 看看这个方法是否取消全局拦截器执行
         */
        const cancelInterceptor: boolean = Reflect.getMetadata(CANCEL_INTERCEPTOR, originValue);
        /**
         *  1.有方法拦截器直接执行方法拦截器
         *  2.如果设置了取消拦截器则直接不执行拦截器
         *  3.最后才是全局拦截器
         */
        const currentInterceptor = localInterceptor ? localInterceptor : !cancelInterceptor ? interceptor : null;
        /**
         * 如果有过滤器,这在异常的时候如果命中了异常过滤器直接执行异常过滤器
         * 如果没有命中则执行拦截器的后续
         */
        return currentInterceptor
          ? currentInterceptor.intercept(handle, arg, {
              filter: currentFilter,
              needFilter,
              originValue,
              Context: this
            })
          : handle(...arg);
      };
      Reflect.defineMetadata(metadataKey, target.prototype[name], target.prototype);
    });
    Reflect.defineMetadata(INTERCEPTOR, interceptor, target);
  };
};
/**
 * 被修饰的函数是否取消拦截器执行
 * @param cancel
 * @constructor
 */
export const CancelInterceptor = (cancel = true): MethodDecorator => {
  return (target, propertyKey, descriptor: TypedPropertyDescriptor<any>) => {
    Reflect.defineMetadata(CANCEL_INTERCEPTOR, cancel, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${propertyKey.toString()}`, descriptor.value, target);
    return descriptor;
  };
};
/**
 * 局部拦截器,如果配置了会直接取消全局拦截器
 * @param interceptor
 * @constructor
 */
export const LocalInterceptors = <T extends LightSendHelper.IIntercept = LightSendHelper.IIntercept>(
  interceptor: T
): MethodDecorator => {
  return (target, propertyKey, descriptor) => {
    Reflect.defineMetadata(LOCAL_INTERCEPTOR, interceptor, target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${propertyKey.toString()}`, descriptor.value, target);
    return descriptor;
  };
};

6.4.1.3 CommonInterceptor

这里会提供一个通用的拦截器抽象类使用,各位可以根据自己的需求开发不通的拦截器

typings/SendHelper.d.ts

....
interface IInterceptOptions {
    showLoading?: boolean;
    loadingMessage?: string;
    showNotice?: boolean;
    noticeMessage?: string;
    showErrorNotice?: boolean;
    errorNoticeMessage?: string;
}

Interceptor.ts

/**
 * 插件通用的拦截器主要功能是
 * 1.发送请求前打开loading
 * 2.发送结束和关闭loading
 * 3.请求成功的时候弹出通知
 * 4.请求失败时弹出失败通知
 * 5.异常的时候首先看看是否命中过滤器,命中了执行过滤器后续,没有执行拦截器后续
 * 等带有副作用的操作,其他功能可以根据自己的实际情况开发
 */
export abstract class CommonInterceptor<
  Request extends LightSendHelper.IInterceptOptions = LightSendHelper.IInterceptOptions,
  E = any,
  Response = any
> implements LightSendHelper.IIntercept
{
  async intercept(handle: any, arg: any[], meta: LightSendHelper.IInterceptMeta): Promise<ReturnType<typeof handle>> {
    const options: Request = arg?.[0];
    const { filter, needFilter, originValue } = meta;
    const { showLoading, showNotice, showErrorNotice } = options;
    if (showLoading) {
      await this.loading(options);
    }
    try {
      const res = (await handle(...arg)) as Response;
      if (showNotice) {
        await this.notice(options, res);
      }
      return res;
    } catch (e) {
      /**
       * 如果有filter并且方法需要被catch则执行异常过滤器
       */
      if (filter && needFilter) {
        return await filter.catch(e, originValue, options);
      }
      if (showErrorNotice) {
        this.errorNotice(options, e as E);
      }
      throw e;
    } finally {
      await this.closeLoading(options);
    }
  }
  abstract loading(options: Request): any;
  abstract closeLoading(options: Request): any;
  abstract notice(options: Request, response: Response): any;
  abstract errorNotice(options: Request, e: E): any;
}
/**
 * loading装饰器,自动往第一个参数里面添加
 * showLoading =  true 和message
 * @param message
 * @constructor
 */
export const Loading = (message?: string): MethodDecorator => {
  return (target, key, descriptor) => {
    const options = Reflect.getMetadata(OPTIONS, descriptor.value || target) || {};
    options.showLoading = true;
    options.loadingMessage = message;
    Reflect.defineMetadata(OPTIONS, options, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target.constructor);
    return descriptor;
  };
};
/**
 * Notice装饰器,自动往第一个参数里面添加通知相关参数
 * @param noticeOptions
 * @constructor
 */
export const Notice = (
  noticeOptions?: Omit<LightSendHelper.IInterceptOptions, "showLoading" | "loadingMessage">
): MethodDecorator => {
  return (target, key, descriptor) => {
    const options = Reflect.getMetadata(OPTIONS, descriptor.value || target) || {};
    Reflect.defineMetadata(OPTIONS, Object.assign(options, noticeOptions), descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target.constructor);
    return descriptor;
  };
};

6.5.1 Filter

过滤器主要是过滤一些特殊的异常,并对异常做一些指定的处理,比如http请求中的status码等

6.5.1.1 更新类型

typings/SendHelper.d.ts

 interface IExceptionFilter<E = any, Request= any, Response = LightSendHelper.IInterceptOptions> {
    catch: (e: E, target: any, options: Request) => any;
  }

6.5.1.2 过滤器实现

Filter.ts

import { CATCH, ERROR_CODE, FILTER, FILTER_TRUNCATION, HTTP_CODE, LOCAL_FILTER, METHOD_ORIGIN_VALUE } from "./Const";
import { isUndefined } from "@light-send/utils";
/**
 * 类的装饰器, 使用过滤器
 * @param filter
 * @constructor
 */
export const UseFilter = <T extends LightSendHelper.IExceptionFilter = LightSendHelper.IExceptionFilter>(
  filter: T
): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(FILTER, filter, target);
  };
};
/**
 * 方法几倍的过滤器,优先级比全局高
 * @param localFilter
 * @constructor
 */
export const UseLocalFilter = <F = LightSendHelper.IExceptionFilter>(localFilter: F): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(LOCAL_FILTER, localFilter, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target);
    return descriptor;
  };
};
/**
 * 插件提供的异常过滤器的抽象类可以通过实现这个类来使用异常过滤器
 */
export abstract class ExceptionFilter<E = any, Request = any, Response = any>
  implements LightSendHelper.IExceptionFilter<E, Request, Response>
{
  async catch(e: E, target: any, options: Request): Promise<Response | void> {
    /**
     * 是否阻断,如果设置为true则会以catch函数的返回值作为新的返回值
     * 如果是false则不会阻断异常的继续网上抛出
     */
    const truncation: boolean = Reflect.getMetadata(FILTER_TRUNCATION, target);
    const httpCode: string | number | Array<string | number> = Reflect.getMetadata(HTTP_CODE, target);
    const code: string | number | Array<string | number> = Reflect.getMetadata(ERROR_CODE, target);
    let res: Response | void;
    /**
     * 如果有 httpCode或者code,则执行 catchHttpCode或者catchCode,否则执行handleCatch
     */
    const httpCodeIsUnd = isUndefined(httpCode);
    const codeIsUnd = isUndefined(code);
    if (!httpCodeIsUnd || !codeIsUnd) {
      if (!httpCodeIsUnd) {
        const responseCode = this.getHttpCode(e, options);
        if ((Array.isArray(httpCode) && httpCode.includes(responseCode)) || httpCode === responseCode) {
          res = await this.catchHttpCode(httpCode, e, options);
        } else {
          res = await this.handleCatch(e, target, options);
        }
      }
      if (!codeIsUnd) {
        const responseCode = this.getCode(e, options);
        if ((Array.isArray(code) && code.includes(responseCode)) || code === responseCode) {
          res = await this.catchCode(code, e, options);
        } else {
          res = await this.handleCatch(e, target, options);
        }
      }
    } else {
      res = await this.handleCatch(e, target, options);
    }

    if (truncation) return res;
    throw e;
  }
  abstract handleCatch(e: E, target: any, options: Request): Promise<Response | void>;

  abstract getCode(e: E, options: Request): string | number;
  abstract getHttpCode(e: E, options: Request): string | number;
  abstract catchCode(code: string | number | Array<string | number>, e: E, options: Request): Promise<Response | void>;
  abstract catchHttpCode(
    code: string | number | Array<string | number>,
    e: E,
    options: Request
  ): Promise<Response | void>;
}

/**
 * 方法装饰器哪些code需要捕获
 * @param code
 * @param truncation
 * @constructor
 */
export const Code = (code: string | number | Array<string | number>, truncation = false): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(CATCH, true, descriptor.value || target);
    Reflect.defineMetadata(ERROR_CODE, code, descriptor.value || target);
    Reflect.defineMetadata(FILTER_TRUNCATION, truncation, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target);
    return descriptor;
  };
};
/**
 * 方法装饰器
 * @param truncation
 * @constructor
 */
export const Catch = (truncation?: boolean): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(CATCH, true, descriptor.value || target);
    Reflect.defineMetadata(FILTER_TRUNCATION, truncation, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target);
    return descriptor;
  };
};
/**
 * 方法装饰器哪些httpCode需要捕获
 * @param httpCode
 * @param truncation
 * @constructor
 */
export const HttpCode = (httpCode: string | number | Array<string | number>, truncation = false): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(CATCH, true, descriptor.value || target);
    Reflect.defineMetadata(HTTP_CODE, httpCode, descriptor.value || target);
    Reflect.defineMetadata(FILTER_TRUNCATION, truncation, descriptor.value || target);
    Reflect.defineMetadata(`${METHOD_ORIGIN_VALUE}:${key.toString()}`, descriptor.value, target);
    return descriptor;
  };
};

6.6.1 Pipe

6.6.1.1 更新类型

typings/SendHelper.d.ts

  interface IPipeTransform<Input = any, Output =any> {
    transform: (value: Input)=> Output
  }

6.6.1.3 过滤器实现

Pipe.ts

import { METHOD_ORIGIN_VALUE, METHOD_VALUE, OPTIONS, PIPE_INIT, TRANSFORM_PIPE } from "./Const";
import { getClassMethodNames } from "@light-send/utils";

/**
 * 方法级别的管道 local=> global
 * @param piper
 * @constructor
 */
export const TransformPipe = <Input = any, Output = any>(
  piper: LightSendHelper.IPipeTransform<Input, Output>
): MethodDecorator => {
  return (target, key, descriptor) => {
    let localPapers: LightSendHelper.IPipeTransform<Input, Output>[] = Reflect.getMetadata(
      TRANSFORM_PIPE,
      descriptor.value || target
    );
    if (!Array.isArray(localPapers)) {
      localPapers = [];
    }
    localPapers.push(piper);
    Reflect.defineMetadata(TRANSFORM_PIPE, localPapers, descriptor.value || target);
    return descriptor;
  };
};
/**
 * 类的装饰器, 使用管道
 * 主要功能是把所有管道集中起来执行一遍计算出最新的入参
 * @constructor
 * @param piper
 */
export const UsePipers = <T extends LightSendHelper.IPipeTransform = LightSendHelper.IPipeTransform>(
  piper: T
): ClassDecorator => {
  return (target) => {
    let pipers: LightSendHelper.IPipeTransform[] = Reflect.getMetadata(TRANSFORM_PIPE, target);
    if (!Array.isArray(pipers)) {
      pipers = [];
    }
    pipers.push(piper);
    Reflect.defineMetadata(TRANSFORM_PIPE, pipers, target);
    const isInit = Reflect.getMetadata(PIPE_INIT, target);
    /**
     * 不要多次初始化
     */
    if (isInit) return;
    Reflect.defineMetadata(PIPE_INIT, true, target);
    const methodsName = getClassMethodNames(target);
    methodsName.forEach((name) => {
      const metadataKey = `${METHOD_VALUE}:${name}`;
      const originMetadataKey = `${METHOD_ORIGIN_VALUE}:${name}`;
      const originValue = Reflect.getMetadata(originMetadataKey, target);
      if (!originValue) {
        Reflect.defineMetadata(originMetadataKey, target.prototype[name], target);
      }
      const value = Reflect.getMetadata(metadataKey, target.prototype) || target.prototype[name];
      type TargetValue = typeof value;
      type Return = ReturnType<TargetValue>;
      type Params = Parameters<TargetValue>;
      /**
       * 拿到所有的原型方法,对方法的功能增强
       * @param options
       * @param arg
       */
      target.prototype[name] = async function (options: T, ...arg: Omit<Params, "0">): Promise<Return> {
        const refOptions: T = Reflect.getMetadata(OPTIONS, originValue);
        let currentOptions = Object.assign({}, refOptions, options);
        /**
         * 拿到方法的所有管道
         */
        const localPapers: LightSendHelper.IPipeTransform[] = Reflect.getMetadata(TRANSFORM_PIPE, originValue) || [];
        const global: LightSendHelper.IPipeTransform[] = Reflect.getMetadata(TRANSFORM_PIPE, target) || [];
        /**
         * 所有管道都执行一遍
         */
        currentOptions = localPapers.concat(global).reduce((options: T, piper) => {
          return piper.transform(options);
        }, currentOptions);
        const handle = value.bind(this);
        return await handle(currentOptions, ...arg);
      };
      Reflect.defineMetadata(metadataKey, target.prototype[name], target.prototype);
    });
  };
};

6.7.1 更新类型定义

typings/SendHelper.d.ts


declare namespace LightSendHelper{
  interface IOptions {
    url?: string
    method?: string
  }
  interface IInterceptMeta<Handle = any, Context = any> {
    // 当前全局的过滤器
    filter?: LightSendHelper.IExceptionFilter,
    // 当前函数是否需要执行过滤器
    needFilter?:boolean,
    // 最原始的函数
    originValue?: Handle,
    // 最原始函数的上下文
    Context?: Context
  }

  /**
   * 拦截器会把过滤器过滤后的异常继续执行
   */
  interface IIntercept {
    intercept(handle: any, args: Parameters<typeof handle>, meta: IInterceptMeta):ReturnType<typeof handle>
  }
  interface IExceptionFilter<E = any, Request= any, Response = LightSendHelper.IInterceptOptions> {
    catch: (e: E, target: any, options: Request) => any;
  }
  interface IPipeTransform<Input = any, Output =any> {
    transform: (value: Input)=> Output
  }
  interface IInterceptOptions {
    showLoading?: boolean;
    loadingMessage?: string;
    showNotice?: boolean;
    noticeMessage?: string;
    showErrorNotice?: boolean;
    errorNoticeMessage?: string;
  }
  /**
   * 插件提供的异常过滤器的抽象类可以通过实现这个类来使用异常过滤器
   */
  export abstract class ExceptionFilter<E = any, Request = any, Response = any> implements LightSendHelper.IExceptionFilter<E, Request, Response> {
    catch(e: E,  options: Request): Promise<Response | void>;
    abstract handleCatch(e: E, target: any, options: Request): Promise<Response | void>;
    abstract getCode(e: E, options: Request): string | number;
    abstract getHttpCode(e: E, options: Request): string | number;
    abstract catchCode(code: string | number | Array<string | number>, e: E, options: Request): Promise<Response | void>;
    abstract catchHttpCode(code: string | number | Array<string | number>, e: E, options: Request): Promise<Response | void>;
  }
  /**
   * 插件通用的拦截器主要功能是
   * 1.发送请求前打开loading
   * 2.发送结束和关闭loading
   * 3.请求成功的时候弹出通知
   * 4.请求失败时弹出失败通知
   * 5.异常的时候首先看看是否命中过滤器,命中了执行过滤器后续,没有执行拦截器后续
   * 等带有副作用的操作,其他功能可以根据自己的实际情况开发
   */
  export  abstract class CommonInterceptor<Request extends LightSendHelper.IInterceptOptions = LightSendHelper.IInterceptOptions, E = any, Response = any> implements LightSendHelper.IIntercept {
    intercept(handle: any, arg: any[], meta: LightSendHelper.IInterceptMeta): Promise<ReturnType<typeof handle>>;
    abstract loading(options: Request): any;
    abstract closeLoading(options: Request): any;
    abstract notice(options: Request, response: Response): any;
    abstract errorNotice(options: Request, e: E): any;
  }
  type Context = {
    /**
     * 这是一个类的装饰器,主要作用是把被这个装饰器装的
     * 的类里面所有被方法装饰器装饰过的url都加上path前缀,
     * 并且把
     * @param path 前缀
     * @param unit 连接符
     * @constructor
     */
    Controller: <T extends LightSendHelper.IOptions = LightSendHelper.IOptions>(path: string, unit?: string | undefined) => ClassDecorator;
    Get: (path: string,unlink?: boolean) => MethodDecorator,
    Post: (path: string,unlink?: boolean) => MethodDecorator,
    Put:  (path: string,unlink?: boolean) => MethodDecorator,
    Delete:  (path: string,unlink?: boolean) => MethodDecorator,
    Patch:  (path: string,unlink?: boolean) => MethodDecorator,
    Options:  (path: string,unlink?: boolean) => MethodDecorator,
    Head: (path: string,unlink?: boolean) => MethodDecorator,
    Path: (path: string,unlink?: boolean) => MethodDecorator,
    UseInterceptor: <T extends LightSendHelper.IIntercept = LightSendHelper.IIntercept>(interceptor: T) => ClassDecorator,
    /**
     * 被修饰的函数是否取消拦截器执行
     * @param cancel
     * @constructor
     */
    CancelInterceptor: (cancel?: boolean) => MethodDecorator,
    /**
     * 局部拦截器,如果配置了会直接取消全局拦截器
     * @param interceptor
     * @constructor
     */
    LocalInterceptor: <T extends LightSendHelper.IIntercept = LightSendHelper.IIntercept>(interceptor: T) => MethodDecorator,
    /**
     * 类的装饰器, 使用过滤器
     * @param filter
     * @constructor
     */
    UseFilter: <T extends LightSendHelper.IExceptionFilter = LightSendHelper.IExceptionFilter>(filter: T) => ClassDecorator,
    /**
     * 方法几倍的过滤器,优先级比全局高
     * @param localFilter
     * @constructor
     */
    UseLocalFilter: <F = LightSendHelper.IExceptionFilter>(localFilter: F) => MethodDecorator;
    /**
     * 插件提供的异常过滤器的抽象类可以通过实现这个类来使用异常过滤器
     */
    ExceptionFilter: typeof ExceptionFilter,
    UsePipers: <T extends LightSendHelper.IPipeTransform = LightSendHelper.IPipeTransform>(piper: T) => ClassDecorator;
    TransformPipers: <Input = any, Output = any>(piper: LightSendHelper.IPipeTransform<Input, Output>) => MethodDecorator,
    /**
     * 方法装饰器哪些code需要捕获
     * @param code
     * @param truncation
     * @constructor
     */
    Code: (code: string | number | Array<string | number>, truncation?: boolean) => MethodDecorator,
    /**
     * 方法装饰器哪些httpCode需要捕获
     * @param httpCode
     * @param truncation
     * @constructor
     */
    HttpCode: (httpCode: string | number | Array<string | number>, truncation?: boolean) => MethodDecorator,
    Catch: (truncation?: boolean) => MethodDecorator,
    /**
     * 插件通用的拦截器主要功能是
     * 1.发送请求前打开loading
     * 2.发送结束和关闭loading
     * 3.请求成功的时候弹出通知
     * 4.请求失败时弹出失败通知
     * 等带有副作用的操作,其他功能可以根据自己的实际情况开发
     */
    CommonInterceptor: typeof CommonInterceptor,
    /**
     * loading装饰器,自动往第一个参数里面添加
     * showLoading =  true 和message
     * @param message
     * @constructor
     */
    Loading: (message?: string | undefined) => MethodDecorator;
    /**
     * Notice装饰器,自动往第一个参数里面添加通知相关参数
     * @param noticeOptions
     * @constructor
     */
    Notice: (noticeOptions?: Omit<LightSendHelper.IInterceptOptions, "showLoading" | "loadingMessage"> | undefined) => MethodDecorator;
  }
}

typings/index.d.ts

/// <reference types="reflect-metadata" />
/// <reference path="./SendHelper.d.ts" />
declare module "@light-send/send-helper" {
  const Context: LightSendHelper.Context
  export  = Context
}
declare module "@light-send/send-helper/index.cjs.js" {
  const Context: LightSendHelper.Context
  export  = Context
}
declare module "@light-send/send-helper/index.umd.js" {
  const Context: LightSendHelper.Context
  export  = Context
  global {
    interface Window {
      lightSendHelper: LightSendHelper.Context
    }
  }
}

7.测试

7.1 准备

在原来的vue项目里

新增controller/MusicController.ts

import service from "@/service";

export class MusicController {}

export default new MusicController();

typing/index.d.ts添加全局类型

/// <reference types="reflect-metadata" />
/// <reference types="@light-send/send-helper" />
/// <reference types="@light-send/send-axios" />

7.2 Path装饰器

7.2.1 ControllerGetPost

更新controller/MusicController.ts

import { Get, Controller } from "@light-send/send-helper";
import service from "@/service";
// 前缀
@Controller("/api/music")
export class MusicController {
  // route
  @Get("/{path}/:id1")
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options);
  }
}

export default new MusicController();

页面App.vue直接使用Controller

import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "./components/HelloWorld.vue";
import musicController from "@/controller/MusicController";
Component.registerHooks(["created"]);
@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {
  public list: IDisc[] = [];
  async created() {
  
    try {
      const { list } = await musicController.getDiscList({}, [
        {
          data: {
            path: "getDiscList",
            id: 1,
          },
        },
        {
          data: {
            path: "getDiscList",
            id: 1,
          },
        },
      ]);
      this.list = list;
    } catch (e) {
      console.log("页面异常", e);
    }
  }
}

请求正常发出

7.2.1.1 Unit

Controller添加连接符再运行

@Controller("/api/music", ".")

两个路径中的连接符已经变了

7.2.1.2 unLink

这个请求不想和前缀拼接Get添加第2个参数

  // route
  @Get("/{path}/:id", true)
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }

7.2.1.3 Post

  @Post("/{path}/:id")
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }

这里请方法被改成了post,我们把服务端改为post后

  @Post('getDiscList/1')
  async getDiscList1(): Promise<MusicData> {
    // await sleep(5000)
    return this.musicService.getDiscList();
  }

又能正常显示了

7.3 拦截器

这里我们添加一个拦截器,拦截器的主要功能是:

  • ✅发送请求以前可以选择打开loading
  • ✅请求结束的时候关闭loading
  • ✅请求成功的时候可以选择弹出消息通知
  • ✅请求失败的时候可以选择弹出消息通知

7.3.1 ServiceInterceptor

基于插件的CommonInterceptor抽象类来实现一个基础的拦截器,也可以自行实现自己需求来实现

import { Loading, Notification } from "element-ui";
import { CommonInterceptor } from "@light-send/send-helper";
export class ServiceInterceptors extends CommonInterceptor<
  LightSendHelper.IInterceptOptions,
  IServiceError,
  AxiosResponse
> {
  /**
   * 饿了么loading关闭的时候的实例对象
   */
  loadingInstance: any;

  /**
   * 告诉拦截器怎么关闭loading
   * @param options
   */
  closeLoading(options: LightSendHelper.IInterceptOptions): any {
    this.loadingInstance?.close();
  }

  /**
   * 告诉拦截器怎么弹出错误通知
   * @param options
   * @param e
   */
  errorNotice(
    options: LightSendHelper.IInterceptOptions,
    e: IServiceError
  ): any {
    const { errorNoticeMessage = "系统异常,请稍后重试" } = options;
    Notification.error(errorNoticeMessage);
  }
  /**
   * 告诉拦截器怎么打开loading
   * @param options
   * @param response
   */
  loading(options: LightSendHelper.IInterceptOptions): any {
    const { loadingMessage } = options;
    this.loadingInstance = Loading.service({
      text: loadingMessage || "...加载中",
    });
  }
  /**
   * 告诉拦截器怎么弹出成功通知
   * @param options
   * @param response
   */
  notice(
    options: LightSendHelper.IInterceptOptions,
    response: AxiosResponse
  ): any {
    const { noticeMessage = "操作成功" } = options;
    Notification.success(noticeMessage);
  }
}

typing/index.d.ts

declare type IServiceError = AxiosError<IResult>;

MusicController使用

import {
  Get,
  Controller,
  Post,
  UseInterceptors,
} from "@light-send/send-helper";
import { ServiceInterceptors } from "@/controller/Interceptor/ServiceInterceptors";
import service from "@/service";
// 前缀
@Controller("/api/music")
@UseInterceptors(new ServiceInterceptors())
export class MusicController {
  // route
  @Post("/{path}/:id")
  @Loading()
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
}

export default new MusicController();

服务端加个延迟函数

@Post('getDiscList/1')
  async getDiscList1(): Promise<MusicData> {
    await sleep(2000)
    return this.musicService.getDiscList();
 }

7.3.2 通知

 // route
  @Post("/{path}/:id")
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }

请求异常的时候通知

// route
  @Post("/{path}")
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }

7.3.3 页面多个请求的处理

我们经常碰到这么一个需求,一个页面初始化的时候会发多个请求,如果每个请求都走loading的话,整个页面会变的很混乱,那么这个如何处理呢?

 // Promise.all的函数添加副作用的装饰器
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}/:id")
  public async getDiscList1(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}/:id")
  public async getDiscList2(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }

App.vue

async created() {
    /**
     * 发送请求,这里不用担心会有异常,因为service已经全部捕获过了
     */
    try {
      const result = await musicController.getDiscList({}, [
        {
          data: {
            path: "getDiscList",
            id: 1,
          },
        },
        {
          data: {
            path: "getDiscList",
            id: 1,
          },
        },
      ]);
      console.log(result);
    } catch (e) {
      console.log("页面异常", e);
    }
  }

ServiceInterceptors.tsloading里面打印一下日志看触发几次

  loading(options: LightSendHelper.IInterceptOptions): any {
    console.log("触发loading");
    const { loadingMessage } = options;
    this.loadingInstance = Loading.service({
      text: loadingMessage || "...加载中",
    });
  }

这里可以因为loading和发送请求的行为是解耦的,并不因为你是一个请求或者多个请求而出现页面loading混乱难以管理的局面。装饰器的使用可以根据方法自由组合,而且因为是配置优先,所哟即使你是静态配置了某个装饰器,但是可以通过入参动态的取消这个配置,非常方便ღ( ´・ᴗ・` )

7.4 过滤器

很多时候我们在处理异常的时候需要对一些特殊的http status或者业务code做一些特殊处理,虽然拦截器也可实现,但是会做很多的逻辑判断,这里独立对这些异常过滤自己处理,那么耦合度又进一步降低

Filter/ServiceExceptionFilter.ts

import {
  Controller,
  Post,
  UseInterceptors,
  Loading,
  Notice,
  HttpCode,
  UseFilter,
  CancelInterceptor,
} from "@light-send/send-helper";
import { ServiceInterceptors } from "@/controller/Interceptor/ServiceInterceptors";
import service from "@/service";
import { ServiceExceptionFilter } from "@/controller/Filter/ServiceExceptionFilter";
// 前缀
@Controller("/api/music")
@UseInterceptors(new ServiceInterceptors())
@UseFilter(new ServiceExceptionFilter())
export class MusicController {
  // Promise.all的函数添加副作用的装饰器
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  @HttpCode("", true)
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}")
  @CancelInterceptor()
  public async getDiscList1(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}/:id")
  @CancelInterceptor()
  public async getDiscList2(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
}

export default new MusicController();

MusicController.ts

import {
  Controller,
  Post,
  UseInterceptors,
  Loading,
  Notice,
  HttpCode,
  UseFilter,
  CancelInterceptor,
} from "@light-send/send-helper";
import { ServiceInterceptors } from "@/controller/Interceptor/ServiceInterceptors";
import service from "@/service";
import { ServiceExceptionFilter } from "@/controller/Filter/ServiceExceptionFilter";
// 前缀
@Controller("/api/music")
@UseInterceptors(new ServiceInterceptors())
@UseFilter(new ServiceExceptionFilter())
export class MusicController {
  // Promise.all的函数添加副作用的装饰器
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  // 捕获404做处理
  @HttpCode(404)
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}")
  // 这个请求就不要经过拦截器了
  @CancelInterceptor()
  public async getDiscList1(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
  // 这个函数不加副作用的装饰器loading/notice之类的
  @Post("/{path}/:id")
  // 这个请求就不要经过拦截器了
  @CancelInterceptor()
  public async getDiscList2(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
}

export default new MusicController();

阻断上报

@HttpCode(404, true)
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }

这里业务层直接收到请求成功的状态

没有匹配到code走普通的handleCatch

  @HttpCode("")
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }

\

Catch普通的捕获

Code捕获业务上的异常

 @Code(0)
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions,
    params: LightAxiosSend.IOptions[]
  ): Promise<Partial<IDiscList>[]> {
    const [param1, param2] = params;
    return Promise.all([this.getDiscList1(param1), this.getDiscList2(param2)]);
  }

Filter/ServiceExceptionFilter.ts

  /**
   * 如何处理命中的异常
   * @param code
   * @param e
   * @param options
   */
  async catchCode(
    code: string | number | Array<string | number>,
    e: IServiceError,
    options: LightSendHelper.IInterceptOptions
  ): Promise<AxiosResponse<IResult> | void> {
    Message.error(`捕获了业务异常:${code}`);
    return;
  }

service/index.d.ts修改isSuccess方法实现

  protected isSuccess(response: AxiosResponse<IResult>): boolean {
    const code = get(response, "data.code");
    console.log("isSuccess", code);
    return code === 1;
  }

7.5 管道

管道有两大类功能:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

7.5.1 ServicePipe.ts

import { compile } from "path-to-regexp";
/**
 * 把url上带有{}里面的key找出来,替换成对象里面的值
 * @param url
 * @param reg
 * @param data
 */
function replaceTemplate(
  url: string,
  reg: RegExp,
  data: Record<string, string>
) {
  let mathResult;
  if ((mathResult = url.match(reg))) {
    const key = mathResult[1];
    if (key && data[key]) {
      url = url.replaceAll(new RegExp(reg, "g"), data[key]);
    }
  }
  return url;
}
export class ServicePipe implements LightSendHelper.IPipeTransform {
  transform(options: LightAxiosSend.IOptions): LightAxiosSend.IOptions {
    // eslint-disable-next-line prefer-const
    let { url, data, params } = options;
    const value = {
      ...(data ? data : {}),
      ...(params ? params : {}),
    };
    if (url) {
      url = replaceTemplate(url, /{(.*?)}/, value);
      url = compile(url)(value);
    }
    options.url = url;
    console.log("initUrTemplateReplace.request", options);
    return options;
  }
}

在业务中使用并恢复到单个请求的状态

import { ServicePipe } from "@/controller/Pipe/ServicePipe";
// 前缀
@Controller("/api/music")
@UseFilter(new ServiceExceptionFilter())
@UseInterceptor(new ServiceInterceptors())
@UsePipers(new ServicePipe())
export class MusicController {
  // Promise.all的函数添加副作用的装饰器
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  @Post("/{path}/:id")
  public async getDiscList(
    options: LightSendHelper.IInterceptOptions & LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    return service.getDiscList(options || {});
  }
}

添加多个管道和局部管道

import { ServicePipe } from "@/controller/Pipe/ServicePipe";
// 前缀
@Controller("/api/music")
@UseFilter(new ServiceExceptionFilter())
@UseInterceptor(new ServiceInterceptors())
@UsePipers({
  transform(options) {
    console.log("全局管道2", options);
    return options;
  },
})
@UsePipers(new ServicePipe())
export class MusicController {
  // Promise.all的函数添加副作用的装饰器
  @Loading()
  @Notice({
    showNotice: true,
    showErrorNotice: true,
  })
  @Post("/{path}/:id")
  @TransformPipers({
    transform(options) {
      console.log("局部管道1", options);
      return options;
    },
  })

8 结语

到这里本次教程基本结束了,各位可以根据喜好使用自由组合使用