前端中的接口聚合
- 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