前端中的接口聚合

  • 2019 年 12 月 21 日
  • 笔记

request-combo

这是一个前端简易版接口聚合模块,主要用于以下场景:

  • 一个支持参数合并的接口,在组件化或其他场景下调用了不同参数的相同的接口,这时把这些调用合并成一个或多个接口再请求。
  • 避免发起相同的请求,某些情况下发起了相同的请求,经收集处理后,实际只发起一个请求。但是不同的发起端的callback 都能得到处理。

主要逻辑设计

  • 要知道接口的基本信息,包括但不限于 url、params、callback…
  • 既然要聚合,那么得有一个收集接口的队列
  • 每个接口的队列要有状态,当一个新接口到来时,该接口的队列可能还没创建,可能正在收集,可能刚发完请求。
  • 要有接口队列发起请求的条件,收集时间够了或者收集长度够了…
  • 有缓存机制,已获取的数据暂时缓存起来

API 设计

调用方法:requestCombo() 参数:

    apiData: ApiData,      params: object,      callback: Function,      request = axios,      collectTime = 100,      isCombo = true,      errorHandle?: Function

ApiData 类型中包含以下内容:

params

Description

Type

Example

url

接口地址

string

http:xxx/api

pack

参数合并逻辑函数

function

fn

unpack

数据拆解逻辑函数

function

fn

maxComboNum

接口最大收集次数

number

10

requestMethod

当前请求类型

string

'get'

具体实现

import axios from 'axios';    interface ApiData {      url: string;      pack: Function;      unpack: Function;      maxComboNum?: number;      requestMethod?: string;  }    /**   * status: number   * 0:init   * 1:pending   * 2:done   *   * request   * The api must be the same as axios   */  const dataCache: object = {};  const busData: object = {};    export const requestCombo = (apiData: ApiData, params: object, callback?: Function, request = axios, collectTime = 100, isCombo = true, errorHandle?: Function) => {      const { url, requestMethod = 'get', maxComboNum = 10, pack, unpack } = apiData;        const method: string = requestMethod.toLocaleLowerCase();      const cacheKey = `${url}_${method}_${JSON.stringify(params)}`;      const busKey = `${url}_${method}`;        if (!url) return;        const sendRequest = async () => {          clearTimeout(busData[busKey].timer);          const paramList = busData[busKey].paramList;          const paramObject = pack(paramList);            busData[busKey] = null;          try {              const result = await applyRequest(url, paramObject, method, request);              // 拆解,拆完要对应回去,因此要用 param 做key              const obj = unpack(result, paramList) || {};              Object.keys(obj).forEach((key) => {                  const dataNode = dataCache[cacheKey];                  if (!dataNode) {                      errorHandle ? errorHandle('Data Unpack Error') : console.log('Data Unpack Error');                  } else {                      dataNode.data = obj[key];                      dataNode.status = 2;                      dataNode.cbs.forEach((cb: Function) => {                          cb(obj[key]);                      });                  }              });          } catch (ex) {              if (errorHandle) {                  errorHandle(ex);                  return;              }              throw ex;          }      };        return new Promise((resolve, reject) => {          if (!callback) callback = () => { }; //预处理接口返回数据          const _callback = callback;          callback = (json: any) => {              const raw = _callback(json);              if (raw && typeof raw.then === 'function') {//认为是Promise                  raw.then((data: any) => {                      resolve(data);                  }).catch((err: any) => { reject(err); }); //终结的promise链必须捕获错误,否则丢失错误链              } else {                  resolve(raw);              }          };            if (dataCache[cacheKey]) {              if (dataCache[cacheKey].status === 1) {                  dataCache[cacheKey].cbs.push(callback);              } else if (dataCache[cacheKey].status === 2) {                  callback(dataCache[cacheKey].data);              }          } else {              dataCache[cacheKey] = {                  status: 1,                  cbs: [],                  data: {}              };              if (!isCombo) {                  applyRequest(url, params, requestMethod, request).then((data: object) => {                      dataCache[cacheKey].status = 2;                      dataCache[cacheKey].data = data;                      dataCache[cacheKey].cbs.forEach((cb: Function) => {                          cb(data);                      });                      resolve(data);                  });              } else {                  if (!busData[busKey]) {                      busData[busKey] = {                          paramList: [params],                          url,                          timer: setTimeout(sendRequest, collectTime)                      };                  } else {                      busData[busKey].paramList.push(params); // 加入参数队列                      if (busData[busKey].paramList.length >= maxComboNum) {                          // 发起请求                          sendRequest();                      }                  }              }          }      }).catch((ex) => {          if (errorHandle) {              errorHandle(ex);              return;          }          throw ex;      });  };    const applyRequest = async (url: string, params: object, requestMethod = 'get', request: any, ) => {      if (requestMethod === 'get') {          return request[requestMethod](url, { params });      } else {          return request[requestMethod](url, { ...params });      }  };

详见:https://github.com/LuckyWinty/ToolLibrary/tree/master/src/RequestCombo

Example

const ApiData = {      getPrice: {          url: '//test/prices',          maxComboNum: 10,          requestMethod: 'get',          pack (paramList: object[]) {              const skuids: string[] = [];              paramList.forEach((p: any) => {                  if (p.skuids) {                      skuids.push(p.skuids);                  }              });              const ret = {                  skuids: skuids.join(',')              };                console.log('合并后的价格参数', ret);              return ret;          },          unpack: (data: any, paramList: object[]) => {              if (data && data.data && length) {                  const resData = data.data || [];                  const ret = {};                  paramList.forEach((p: any) => {                      const key = JSON.stringify(p);                      resData.some((item: any, i: number) => {                          const sku = item.sku;                          if (sku === p.skuids) {                              ret[key] = [data[i]];                              return true;                          }                          return false;                      });                  });                  console.log('价格拆解数据', ret);                  return ret;              }              return [];          }      }  };    const p1 = requestCombo(ApiData['getPrice'], { skuids: '11111' }, (data: any) => {      console.log(data);  });  const p2 = requestCombo(ApiData['getPrice'], { skuids: '11112' }, (data: any) => {      console.log(data);  });  const p3 = requestCombo(ApiData['getPrice'], { skuids: '11113' }, (data: any) => {      console.log(data);  });  const data1 = await p1;  const data2 = await p2;  const data3 = await p3;

作为独立repo打包

这种情况适合使用 webpack 来作为打包器。我们主要配置几个点:

  • 支持各种模式的导入(umd、ES6的export、export default导出)
  • 打包压缩版用于生产环境,未压缩版用于开发环境
  • 将项目名与入口文件的返回值绑定(script引入时可以直接访问项目名称来访问包)

最后配置如下:

    //webpack.config.js      const TerserPlugin = require('terser-webpack-plugin');        module.exports = {          entry: {              'RequestCombo': './src/index.js',              'RequestCombo.min': './src/index.js'          },          output: {              filename: '[name].js',              library: 'RequestCombo',              libraryTarget: 'umd',              libraryExport: 'default'          },          mode: 'none',          optimization: {              minimize: true,              minimizer: [                  new TerserPlugin({                      include: /.min.js$/,                  })              ]          }      }

在工具库中,用 rollup 打包

这个跟 webpack 打包的目标是一致的。就是工具不同,配置稍有差异.

//为展示方便,删除了部分插件  const filesize = require('rollup-plugin-filesize')  const path = require('path')  const { terser } = require('rollup-plugin-terser')  const { name, version, author } = require('../package.json')    const componentName = process.env.COMPONENT  const componentType = process.env.COMPONENT_TYPE || 'js'    const banner = `${'/*!n* '}${name}.js v${version}n`    + ` * (c) 2018-${new Date().getFullYear()} ${author}n`    + ' * Released under the MIT License.n'    + ' */'    module.exports = [    {      input: path.resolve(__dirname, `../src/${componentName}/src/index.${componentType}`),      output: [        {          file: path.resolve(            __dirname,            `../src/${componentName}/dist/${componentName}.min.js`          ),          format: 'umd',          name,          banner,          sourcemap: true,        }      ],      plugins: [terser(), filesize()],    },    {      input: path.resolve(__dirname, `../src/${componentName}/src/index.${componentType}`),      output: {        file: path.resolve(          __dirname,          `../src/${componentName}/dist/${componentName}.min.esm.js`        ),        format: 'esm',        banner,      },      plugins: [terser(), filesize()],    },    {      input: path.resolve(__dirname, `../src/${componentName}/src/index.${componentType}`),      output: [        {          file: path.resolve(            __dirname,            `../src/${componentName}/dist/${componentName}.js`          ),          format: 'umd',          name,          banner,        }      ],      plugins: [],    }  ]

详见:https://github.com/LuckyWinty/ToolLibrary

发布到 npm

相关命令 添加用户: npm adduser

登陆: npm login

发布版本: npm publish

升级版本:

  • 升级补丁版本号: npm version patch
  • 升级小版本号: npm version minor
  • 升级大版本号: npm version major