# @ysfe/request

## Introduction

[![npm version](https://badge.fury.io/js/%40ysfe%2Frequest.svg)](https://badge.fury.io/js/%40ysfe%2Frequest)
[![NPM downloads](https://img.shields.io/npm/dm/%40ysfe%2Frequest.svg?style=flat)](https://npmjs.org/package/ysfe/request)

>

## 描述

<br />

> 基于 axios 的 二次封装, 提供插件化的通用能力.

---

<br />

-   特性扩展

    -   请求防抖
    -   最大请求并发限制
    -   url 格式校验
    -   ILogger 自定义请求过程日志
    -   局部 mock / 全局 mock 能力
    -   长请求 loading
    -   自定义响应值格式.

-   插件能力
    -   serialize-data | (默认集成) 基于 `content-type` 属性, 序列化数据
    -   serialize-url | (默认集成) url 检查&修复, 去除重复 `/`. 基于`urlFormat` 参数启用
    -   env | 提供标准化环境识别协议
    -   dynamic-proxy | 请求动态代理 (需要 `@ysfe/vue-default-config` 支持)
    -   mock | 全局 mock/局部 mock 切换
    -   loading | 请求过程 loading
    -   filter-null-value | 空参数过滤
    -   signature | 请求签名
    -   transform-request | 扩展请求前置处理操作
    -   transform-response | 扩展响应前置处理操作

<br />

---

<br />

## 支持

<br />
<br />
<br />

### 构造参数 (IRequestOptions)

> AxiosRequestOptions 接口扩展, 参数类型见[接口](#接口)

| 参数         | 描述                    | 类型                                | 默认值 | 可选 |
| ------------ | ----------------------- | ----------------------------------- | ------ | ---- |
| urlFormat    | 是否启用 url 序列化检查 | boolean                             | true   | ✅   |
| mock         | 是否启用 mock           | boolean                             | -      | ✅   |
| logger       | 自定义日志(工厂方法)    | [ILogger](#ilogger)                 | -      | ✅   |
| loading      | loading 插件支持        | [ILoadingFactory](#iloadingfactory) | -      | ✅   |
| responseData | 响应数据格式            | [TResponseData](#tresponsedata)     | 'data' | ✅   |
| only         | 请求防抖开关            | [TOnly](#tonly)                     | -      | ✅   |
| max          | 并发请求数限制开关      | number ( >= 0 )                     | -      | ✅   |

<br />
<br />
<br />

### 方法

> 提供链式调用的插件引用, 以及常用请求接口支持, 参数类型见[接口](#接口)

-   **use params** - `(plugin: IPlugin)`
-   **get params** - `(url: string, params: any, options: IRequestOptions)`
-   **post params** - `(url: string, data:any, options: IRequestOptions)`
-   **sendBeacon params** - `(url: string, params: any)`
-   **IRequestOptions** - [接口声明](#irequestoptions)
-   **appendHeader params** - `(key:string, value:any)`
-   **setHeaders params** - `(headers: {[key:string]:any})`

-   注: get,post 差异参考 axios, 分别作用于请求的 `params`、`data` 属性
-   注: sendBeancon 仅能发送简短请求, 请求参数一定要简短, 否则会产生失败请求.

| method       | 描述                                                      | 参数                    |
| ------------ | --------------------------------------------------------- | ----------------------- |
| use          | 加载插件, 插件需要实现 IPlugin 接口 (允许链式调用)        | **use params**          |
| appendHeader | 添加 header                                               | **appendHeader params** |
| removeHeader | 移除 header                                               | string                  |
| setHeaders   | 设置 headers                                              | **setHeaders params**   |
| cancelAll    | 中断所有请求, 一般用于切换页面时, 中止未完成请求          | -                       |
| request      | 发送请求, 请求发送的默认方法                              | **IRequestOptions**     |
| get          | 发送 get 请求                                             | **get params**          |
| post         | 发送 post 请求                                            | **post params**         |
| head         | 发送 head 请求                                            | **get params**          |
| options      | 发送 options 请求                                         | **get params**          |
| delete       | 发送 delete 请求                                          | **get params**          |
| put          | 发送 put 请求                                             | **post params**         |
| patch        | 发送 patch 请求                                           | **post params**         |
| sendBeacon   | 通过 navigator.sendBeacon()发出简短请求, 一般用于埋点上报 | **sendBeacon params**   |

<br />
<br />
<br />

### 插件

> 通过切面编程方式, 扩展 axios 能力 , 参数类型见[接口](#接口)
>
> 插件使用参考: [使用参考](#使用参考)

-   **env parmas** - `(envs: IEnv | Array<IEnv>, envName?: TEnvName)`
-   **loading parmas** - `(handler: (status: boolean) => void, delayTime: number = 500)`
    -   `delayTime`: 毫秒(ms)
-   **fnv params** - `(filterEmptyString?: boolean)`
    -   `filterEmptyString`: 是否过滤空字符串, (即,过滤字符串为`''`场景)
-   **signature params** - `(options: ISignatureOptions = { key: 'sign' })`
    -   [ISignatureOptions](#isignatureoptions)
-   **transformRequest parmas** - `(handler: ITransformRequest)`
    -   [ITransformRequest](#itransformrequest)
-   **transformResponse parmas** - `(handler: ITransformResponse)`
    -   [ITransformResponse](#itransformresponse)
-   **errorHandler parmas** - `(handler: IErrorHandler)`
    -   [IErrorHandler](#ierrorhandler)
-   **dynamicProxy params** - `(proxyPath?: string)`
    -   `proxyPath`: 自定义代理路径

---

> 注: `dynamic-proxy` 动态代理插件仅在 `'process.env.NODE_ENV==="development"'` 场景下生效

| 插件               | 描述                                                    | 默认集成 | 参数                         |
| ------------------ | ------------------------------------------------------- | -------- | ---------------------------- |
| serialize-data     | 基于 `content-type` 属性, 序列化数据                    | ✅       | -                            |
| serialize-url      | url 检查&修复, 去除重复 `/`. 基于`urlFormat` 参数启用   | ✅       | -                            |
| env                | 提供标准化环境识别协议                                  | -        | **env params**               |
| dynamic-proxy      | 请求动态代理 (需要 `@ysfe/vue-default-config` 支持)     | -        | **dynamicProxy params**      |
| mock               | 注入 全局/局部 mock 能力, 启用开关依赖`IRequestOptions` | -        | -                            |
| loading            | 请求过程 loading                                        | -        | **loading params**           |
| filter-null-value  | 空参数过滤                                              | -        | **fnv params**               |
| signature          | 请求签名                                                | -        | **signature params**         |
| transform-request  | 标准化请求前置处理操作                                  | -        | **transformRequest params**  |
| transform-response | 标准化响应前置处理操作                                  | -        | **transformResponse params** |
| error-handler      | 标准化异常处理操作                                      | -        | **errorHandler parmas**      |

### 接口

#### IData

```typescript
export interface IData {
    [key: string]: any
}
```

#### IEnv

```typescript
/** 环境名 */
export type TEnvName = 'production' | 'test' | 'dev' | 'local' | string
/** 环境配置 | 接口定义 */
export interface IEnv {
    /** 环境名, 支持重名, 及 `test1`、`dev1`等 */
    name: TEnvName
    /** 环境启用条件
     * @description Request工具根据 rule规则定义,
     * @type {string} 基于 host 匹配, 当host相同时, 启用.
     * @type {RegExp} 正则匹配, rule.test(location.href), 使用正则匹配url, 当满足规则时, 使用当前环境配置
     *    - 示例: //api.server.com/xxx -> /api\.server\.com/
     * @type {() => boolean} 自定义环境判断 Factory Function. 当返回值为true时, 启用当前环境
     */
    rule: string | RegExp | (() => boolean)
    /** 请求地址前缀 */
    baseURL: string
    /** mock地址
     * @description mock地址, 当启用 mock = true 时, 启用 mock 环境
     */
    mockURL?: string
    /** 动态代理, 来源路径. 用于本地开发时, 绕过跨域限制
     * @description 仅 NODE_ENV==='production' 时, 可用.
     */
    referer?: string
    /** 环境优先级, 当同时满足多条环境规则时, 根据优先级大小选择环境, 默认则根据环境定义顺序, 选择环境 */
    order?: number
}
```

#### ILoadingFactory

```typescript
/** loading 操作处理接口 */
export interface ILoadingFactory {
    /** loading 切换处理方法 */
    handler: (status: boolean) => void
    /** 延迟显示时间, 默认500毫秒, 小于等于 0ms, 则被忽略 */
    delayTime: number
}
```

#### ILogger

```typescript
/**
 * 指示日志消息的严重性。
 * 日志级别按严重性递增的顺序排列。所以“Debug”比“Trace”等更严重。
 */
export enum LogLevel {
    /** 极低严重性诊断消息的日志级别. */
    Trace = 0,
    /** 调试错误. */
    Debug = 1,
    /** 消息. */
    Information = 2,
    /** 警告. */
    Warning = 3,
    /** 错误. */
    Error = 4,
    /** 严重错误. */
    Critical = 5,
    /** 最高日志级别。在配置日志记录以指示不应发出日志消息时使用. */
    None = 6
}

export interface ILogger {
    /** Called by the framework to emit a diagnostic message.
     *
     * @param {LogLevel} logLevel The severity level of the message.
     * @param {string} message The message.
     */
    log(...msg: LogLevel | any): void
}
```

#### IOnly

```typescript
export interface IOnlyOption {
    type?: boolean | 'merge' | 'error'
    /** 请求延迟, 用来避免请求处理速度较快情况下的并发请求 */
    delay?: number
    /** 错误消息, 默认: '您的操作太快了' */
    message?: string
}
export type TOnly = boolean | 'merge' | 'error' | IOnlyOption
```

#### IPlugin

```typescript
/** 插件接口 */
export type IPlugin = {
    /** 插件名, 唯一属性 */
    pluginName: string
    /** 处理方法 */
    handler: (axios: AxiosInstance, plugins?: Array<string>) => void
}
```

#### IRequestOptions

```typescript
export interface IRequestOptions extends AxiosRequestConfig {
    /** 请求防抖 - 唯一请求, 默认: 'merge', 可通过 设置 false 关闭.
     * @param {'merge'| 'skip' | 'error'} type 防抖方式
     *  - merge | 合并重复请求
     *  - skip | 出现重复请求时, 忽略
     *  - error | 出现重复请求时, 抛出异常
     * @param {number} delay 请求时延, 通过延时函数, 将在一定时间段内发起的请求进行合并, 对节约请求资源很有帮助
     * @param {Function} checker 自定义重复请求检查方法
     */
    only?: TOnly
    /** 并发请求数限制, 默认: 不限制 */
    max?: number
    /** 是否启用 url 格式校验, 默认: true */
    urlFormat?: boolean
    /** 自定义日志工具 */
    logger?: ILogger

    /** 启用mock能力, 需要依赖 `request.use(mock())`
     * @description |
     *  - 作为全局配置时, 启用全局mock能力
     *  - 作为请求参数时, 启用局部mock能力
     */
    mock?: boolean

    /** 请求loading */
    loading?: ILoadingFactory

    /** 响应数据格式
     * @param {'data'| 'origin'} responseData 防抖方式
     *  - data | 返回 res.data
     *  - origin | 返回 res
     */
    responseData?: TResponseData
}
```

#### ISignatureOptions

```typescript
/** 请求签名参数 */
export interface ISignatureOptions {
    /** 签名字段, 默认: sign */
    key?: 'sign' | 'signature' | string
    /** 盐值 | 用于参数加盐计算
     * @type {string} 盐值字段, 默认附加在结尾
     * @type {IData} 传入盐值内容, 附加至对象结尾.
     * @type {(data?:IData)} 工厂方法, 通过自定义方法, 向参数对象写入盐值
     *
     * @description 注: 加盐操作, 在参数排序后进行, 默认附加在参数最后一位. 如果需要参与排序, 那么需要通过工厂方法, 重新计算参数顺序
     */
    salt?: string | IData | ((data?: IData) => IData)
    /** 是否启用参数排序, 可自定义参数排序方法, 默认: true */
    sort?: boolean | ((key1: string, key2: string) => number)
    /** 签名算法, 默认: md5 */
    sign?: 'md5' | 'sha1' | 'sha256' | ((serializeData: string, originData?: any) => string)
    /** 是否禁用过滤空值操作 */
    disableFilterNullValue?: boolean
}
```

#### ITransformRequest

```typescript
/** 请求参数转换处理方法 */
export type ITransformRequest = (req: IRequestOptions) => IRequestOptions | Promise<IRequestOptions> | void
```

#### ITransformResponse

```typescript
export type ITransformResponse = (
    status: number,
    code: number,
    data: any,
    response: AxiosResponse
) => Promise<void> | void
```

#### IErrorHandler

```typescript
/** 异常处理
 * @description 如果中止请求, 请抛出 throw new Error('request about')
 */
export type IErrorHandler = (e: Error | string) => Promise<void> | void
```

#### TResponseData

```typescript
/** 响应数据格式
 * @param {'data'| 'origin'} responseData 防抖方式
 *  - data | 返回 res.data
 *  - origin | 返回 res
 */
export type TResponseData = 'data' | 'origin'
```

<br />
<br />

## 安装

-   执行 `yarn add @ysfe/request`

## 使用参考

### Env.ts 定义

```typescript
/** 交易虎 后端接口配置 */
export const envs: Array<IEnv> = [
    {
        name: 'production',
        rule: () => true,
        referer: 'http://www.fe.com',
        baseURL: '//server.xxx.com',
        order: -1
    },
    // 预发环境
    {
        name: 'pre',
        rule: /test\.fe\.com/,
        referer: 'http://test.fe.com',
        baseURL: '//server-pre.xxx.com'
    },
    // 本地调试&开发环境部署
    {
        name: 'dev',
        rule: /(www\.fe\.com|localhost|127\.0\.0\.1)/,
        referer: 'http://www.fe.com',
        baseURL: '//server.fe.com'
        order: process.env.NODE_ENV !== 'production' ? 1 : -1
    }
]
```

### 定义请求工具

```typescript
/** 创建请求处理工具 */
export const createRequest = (
    logger: ILogger,
    envName?: 'production' | 'pre' | 'test' | 'test2' | 'dev' | 'dev2'
): Request<IResponseData> => {
    return (
        new Request<IResponseData>({
            headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
            logger,
            only: true,
            mock: false // 当使用全局 mock 时, 将此项修改为 true
        })
            .use(env(envs, envName)) // 注入环境变量
            .use(mock()) // 注入mock
            .use(dynamicProxy()) // 注入动态代理
            .use(filterNullValue()) // 注入请求空值过滤
            // 切换loading显示, 采用默认的 500ms等待时间
            .use(
                loading((status: boolean) => {
                    // TODO 切换loading 显示
                })
            )
            // 附加请求参数
            .use(
                transformRequest((req: IRequestOptions) => {
                    // 请求前额外附加header
                    // TODO 通过 getNativeHeader 获取 请求headers
                    const headers: any = {}
                    Object.assign(req.headers, headers)
                })
            )
            // 响应处理
            .use(
                transformResponse((status: number, code: number, data: any) => {
                    // ? http 状态码 >= 400, 表示请求出错. 弹出报错信息即可
                    if (status >= 400) throw new Error('请求出错了')
                    // ? 正常请求
                    if ([0].includes(code)) return
                    // ? 未登录
                    if ([302].includes(code)) {
                        Cookie.remove('token', { expires: new Date().getTime() + 50 * 30 * 24 * 60 * 1000 * 1000 })
                        // TODO 延迟 1000ms后, replace 到登录页
                    }
                    if ([undefined, 53].includes(code)) {
                        console.log('outSide interface (example => upload)')
                        return
                    }
                    throw data
                })
            )
    )
}
```

### 自定义插件

```typescript
/** 定义一个插件
 * @description 可以给定自定义参数
 */
export const plugin = (): IPlugin => {
    return {
        pluginName: '[plugin name]',
        handler: (axios: AxiosInstance) => {
            // TODO 基于 axios 规范, 对 axios 对象进行扩展
        }
    }
}

// 使用

const requst: Request = new Request().use()

// 执行请求

request.post('/xxx')
```
