# 前端网关

提供给微信小程序的前端缓存/限流 SDK，通过重写 wx.request/wx.cloud.callContainer ，对请求进行前置和后置处理，从而对后端网关进行特殊时期的保护。

1. 名词解释

##### 缓存：对接口返回的内容（data、header、statusCode）进行缓存。

常用场景：在一定时间内不会变化的数据即可缓存，降低接口压力

##### 主动限流：有多大比例用户可以访问到接口。

常用场景：非关键接口在流量高峰时直接不请求后端接口、抢购场景

##### 被动限流：根据接口返回的http状态码或者业务错误码，冻结用户一段时间不能访问接口（发出请求的接口会被丢弃）。

常用场景：提前根据后端状态配置好熔断策略，不需要临时配置



## 快速上手

### 1、npm 下载

```bash
npm install @wecity/mp-fe-gateway --save-dev
```

### 2、初始化

在 app.js 里初始化

```js
// 引入rum
// 备注：sdk要在aegis之后miniprogram-api-promise（如果有使用该库包装wx）之前
import Aegis from 'aegis-mp-sdk'
const aegis = new Aegis({
  id: 'xxxx',
  // 上报 id
  reportApiSpeed: false,
  // 接口测速
  spa: false, // spa 应用页面跳转的时候开启 pv 计算
});
const mpRateLimit = require('@wecity/mp-fe-gateway/rateLimit.min');
// 如需使用定时清理缓存需单独引入`cronCachePlugin`
const cronCachePlugin = require('@wecity/mp-fe-gateway/cronCachePlugin.min');
// 初始化sdk
mpRateLimit.init({
  url: '<%= YOUR CONFIG URL %>', // get请求策略配置信息的地址
  enable: true, // 是否关闭sdk，关闭后不会拉取配置信息，即缓存/限流等功能都不会生效，默认开启（true），开启：true，关闭：false
  debugger: true, //  是否开启控制台日志，默认关闭（false），这样在console控制台可以看到打印的信息，开启：true，关闭：false
  plugins: [cronCachePlugin], // 选择希望支持的插件
  // 全局处理函数, 写入缓存/命中缓存/被动限流/主动限流，这里可以用来做上报逻辑
  globalHandler: function (type, params) {
    switch (type) {
      // 写入缓存
      case mpRateLimit.enum.writeCache:
      // 命中缓存
      case mpRateLimit.enum.hitCache:
        // request 请求配置
        // response 返回的内容
        // cacheStorageKey 缓存Key
        // cacheTime 缓存有效时间
        const { request, cacheStorageKey = '', cacheTime } = params;
        const tempArr = cacheStorageKey.split('|');
        aegis.reportEvent({
          name: type,
          ext1: tempArr[0],
          ext2: tempArr[1],
        });
        break;
      // 被动限流
      case mpRateLimit.enum.passiveLimit:
        /*
          apiUrl：接口地址
          limitData：限流信息
              hitTime: 命中限流时间戳
              coolingTime: 冷却时间（秒）
              limitConfig: 限流配置
                 statusCode/errCode: 状态码/错误码
                 effect: 影响范围1：访问当前接口被限流，2：访问上次限流的同域名接口会被限流，不填或者填写不符合规范，默认1
        */
        const { apiUrl, limitData } = params;
        aegis.reportEvent({
          name: type,
          ext1: apiUrl,
          ext2: limitData.coolingTime,
          ext3: limitData.limitConfig.statusCode || limitData.limitConfig.errCode,
        });
        break;
      // 主动限流
      case mpRateLimit.enum.activeLimit:
        /*
         apiUrl：接口地址
         access：是否限流，false：命中限流 true：正常通行
       */
        aegis.reportEvent({
          name: type,
          ext1: params.apiUrl,
          ext2: params.access ? 'success' : 'fail',
        });
        break;
    }
  },
})
App({
  onLaunch() {
    // 包装一个自定义的全局request
    // 后续的接口请求都通过这个自定义的request
    wx.myRequest = function(options) {
      return wx.request({
        ...options,
        success: (res) => {
          // 返回了limitData（{"hitTime":1651134682386,"coolingTime":10,"statusCode":503,limitType:1}），说明触发了被动限流
          // hitTime：触发时的时间戳，coolingTime：总的冷却时间（秒），被动限流才有该字段
          // remainTime：剩余多久解封（毫秒） statusCode：触发时的状态码，被动限流才有该字段
          // limitType: 1，主动限流 2、被动限流
          if (res.limitData) {
            // 被动限流处理
            if (res.limitData.limitType === 2) {
              wx.showModal({
                title: '提示',
                content: `当前访问人数太多，还需要冷却${(res.limitData.remainTime/1000).toFixed(1)}秒，请稍候再试`,
                showCancel: false,
                confirmText: '关闭',
              });
            }
          }
          options.success(res);
        },
        fail: (res) => {
          if (res.limitData) {
            // 主动限流处理
            wx.showModal({
              title: '提示',
              content: `主动限流`,
              showCancel: false,
              confirmText: '关闭',
            });
          }
          options.fail(res);
        },
      });
    }
  }
})

// 页面中使用
Page({
  fetchUserList() {
    wx.myRequest({ url: '/api/fetchUserList' })
  }
})
```

## API

### mpRateLimit.init({url,enable,debugger,globalHandler})
限流 SDK 的初始化

#### 入参说明
| 参数 | 简介 | 类型 | 备注 |
| --- | --- | --- | --- |
| url |  get请求策略配置信息的地址 | string |地址返回的配置信息请看下面的[策略配置](#divtop)|
| urlNeedTimestamp | get请求策略配置信息的地址是否需要加上时间戳（url?t=Date.now()），默认false，是：true，否：false | bolean ||
| enable | 是否关闭sdk，关闭后不会拉取配置信息，即缓存/限流等功能都不会生效，默认开启（true），开启：true，关闭：false | bolean |
| debugger | 是否开启限流日志，默认关闭（false），这样在console控制台可以看到打印的信息，开启：true，关闭：false | bolean |验证后建议关闭|
| globalHandler | 全局自定义处理函数，可以做上报等功能 | function (type, params) ||

<a name="divtop"> 策略配置： </a>

```js
{
  // 是否开启主动限流， 是：1 否：0
  "openActiveLimit": 1,
  // 是否开启被动限流（当接口返回某些状态码/错误码时，自动触发被动限流）， 是：1 否：0
  "openPassiveLimit": 1,

  // 插件配置
  "plugins": {
    // 实时与本地日志插件配置
    "ReportErrorLog": {
      // 是否开启小程序实时与本地日志上报，是：1 否：0
      // 关闭后实时和本地日志均不会写入上报
      "open": 1,

      // 当业务状态码不符合条件时上报
      // 影响范围：仅符合 bisDomains(业务域名白名单) 的请求才上报
      // 示例解释：!(response.data.errocde == 0 || response.data.code == '200') 时上报
      "errCodes": [
        {
          "codeKey": "errcode",
          "errCode": 0
        },
        {
          "codeKey": "code",
          "errCode": "200"
        }
      ],
      // 业务域名白名单
      "bisDomains": ["xxx.yy.com"],

      // 当存在 HTTP 状态码不符合条件时上报
      // 影响范围：所有域名的请求
      // 示例解释：[200].indexOf(response.statusCode) === -1 时上报
      "httpCodes": [
        {
          "statusCode": 200
        }
      ]
    }
  }

  // 多久后重新获取这个限流文件，单位秒
  "refreshConfigTime": 60, // 60秒后
  // 接口列表
  "apiList": [
    {
      // 路径,可以带域名
      // 同时支持 /path/** 方式，
      // 可匹配到/path/isLoginV1 或者/path/isLoginV2, 
      // ** 只能匹配到非/内的路径字符，无法匹配到/
      "path": "/path/isLoginV2",
      // 主动限流配置
      "activeLimit": {
        // 初始百分比
        "percent": 50,
        // 请求接口失败后，下次提升10%的成功率（接口请求成功后，成功率重置回原来初始百分比），默认是0
        "increaseNum": 10,
      },
      // 被动限流配置
      "passiveLimit": {
        // 根据接口返回的状态码进行被动限流
        "httpCodes": [
          // 下面配置解释（配置了当前接口限流后，后续请求的所有接口都会限流）：
          // wx.request访问isLoginV2接口,回调函数(success)因为503状态码返回了限流信息limitData：{"statusCode":503,"limitData":{"hitTime":1651134682386,"remainTime":3000,"hitTime":1651134682386,"coolingTime":10,"statusCode":503}}，
          // 那么10秒内，通过wx.request访问A/B/C接口，回调函数(fail)会直接返回限流信息limitData（刚刚isLoginV2接口返回的内容）：{"statusCode":503,"limitData":{"hitTime":1651134682386,"remainTime":3000,"coolingTime":10,"statusCode":503}}
          // 业务方根据limitData去做定制处理
          {
            "statusCode": 503, // 必填：状态码
            "effect": 2, // 影响范围1：访问当前接口被限流，2：访问上次限流的同域名接口会被限流，不填或者填写不符合规范，默认1
            "coolingTime": 10, // 必填：秒，限流冷却时间，在这段时间发起的请求不会到达后端，不填或者填写不符合规范，默认6秒
          }
        ],
        // 根据接口body返回的errcode进行被动限流
        "errCodes": [
          {
            "codeKey": "errcode", // 必填：错误码的键 errcode code?
            "errCode": 10002, // 必填：body返回的错误码
            "effect": 2, // 必填：影响范围1：访问当前接口被限流，2：访问上次限流的同域名接口会被限流
            "coolingTime": 10, // 必填：秒，限流冷却时间，在这段时间发起的请求不会到达后端
          }
        ],
      },
      // 缓存配置   resp.statusCode / get(resp.data, codeKey)
      "cache": {
        // 必填：该请求缓存时间，单位为秒， 大于0的数字
        "cacheTime": 60,
        "cronCacheTime": '* 0 * * *', // 每日0点失效
        // 缓存参数值，会根据该值确定缓存是否生效
        // 必填：提供的参数为 query/data/header，可以通过模版进行组合
        "cacheKey": "${header.sid}-${data['x-table']}-${data.params[0]['x-table']}-${query.id}",

        // 必填：状态码
        "statusCode": 200, // 必填：http response 状态码
        "codeKey": "errcode", // 必填：http response data 业务状态码字段
        "errCode": 0 // // 必填：http response data 业务状态码
      }
    }
  ]
}
```

### mpRateLimit.safeFetchCfg(apiUrl)
获取限流配置，返回promise对象

使用场景：利用sdk的配置下发能力，把业务配置放到限流配置里，实现业务逻辑
```
apiUrl传入则获取对应api的配置规则，不传则返回整个json配置文件
// 读取配置文件
rateLimit.safeFetchCfg("/path/isLoginV2").then(res=> {
  //会返回配置文件apiList中/path/isLoginV2的配置
console.log(res)
}).catch(()=> {})
```

### mpRateLimit.clearCache()

清除所有rateLimit设置的缓存数据， 可以使用在不同用户登陆时 又没有不同用户唯一标识情况下数据错乱时，做清除用户处理。

### mpRateLimit.checkCacheByDate("2022-05-05 12:30:45")

根据服务器时间更正 cache是否依然在缓存有效期，如果不在则清理

### mpRateLimit.clearCacheByUrlPath(urlPath)

清理urlPath 匹配成功的所有缓存内容，使用正则表达式匹配 例如 
```
// 如果json里 apiList 里 path 配置为 /api/fetchUserList/get /api/fetchUserList/put  
mpRateLimit.clearCacheByUrlPath('/api/fetchUserList/get') // 清理路径/api/fetchUserList/get的缓存
mpRateLimit.clearCacheByUrlPath('/api/fetchUserList/([a-z]+)') // 清理/api/fetchUserList/get, /api/fetchUserList/put的缓存 

mpRateLimit.clearCacheByUrlPath('/api/fetchUserList/get', {
  cacheKeyData: {
    header: {
      sid: '1212',
    },
    query: {
      qid: '222',
    },
    data: {
      did: '333'
    }
  }
}) // 精准清理 具体某一条的 缓存
```



## 内置插件

点击查看插件详细介绍

### 1. [实时与本地日志上报插件](./src/plugins/log/README.md)

### 2. [Cron 格式缓存时间插件](./src/plugins/cron/README.md)

## 其他说明

1. 因限流配置为异步拉取，首次生效期可能会有一定的延迟，取决于限流配置拉取回来的时间。
2. 用户登录/退出建议清理下缓存。另外根据具体业务情况清理缓存。
3. 当cdn挂了，使用上次请求回来的配置规则。
4. 改小cacheTime会重新加载数据，不会走缓存；如果不想走缓存，把cacheTime设置为0。
