用Proxy的方式统一管理接口请求

现状描述

我们一般在做vue或者react的单页面项目的时候,对所有接口的请求的管理一般都是创建一个公共的js,里面导出许多返回一个Promise的函数。

例如我在src/api目录下创建了一个common.js,代码如下:(注:request方法默认的Content-Type为application/json)

import request from @/utils/request import qs from qs /***** 分组 *****/ // 分组列表 export function groupList(params) { return request({ url: /zm/group, method: get, params }) } // 分组详情 export function groupDetail(id) { return request({ url: `/zm/group/${id}`, method: get, }) } // 分组新增 export function groupAdd(data) { return request({ url: /zm/group, method: post, data }) } // 分组修改 export function groupUpdate(id, data) { return request({ url: `/zm/group/${id}`, method: put, data }) } // 分组删除 export function groupDelete(id) { return request({ url: `/zm/group/${id}`, method: delete, }) } /***** 应用 *****/ // 应用列表 export function applicationList(fromType, unitSetId, params) { return request({ url: `/zm/${fromType}/${unitSetId}/application`, method: get, params }) } // 应用详情 export function applicationDetail(fromType, unitSetId, id) { return request({ url: `/zm/${fromType}/${unitSetId}/application/${id}`, method: get, }) } // 应用新增 export function applicationAdd(fromType, unitSetId, data) { return request({ url: `/zm/${fromType}/${unitSetId}/application`, method: post, headers: { Content-Type: application/x-www-form-urlencoded }, transformRequest: [(data) => qs.stringify(data)], data }) } // 应用修改 export function applicationUpdate(dOpts, data) { const { fromType, unitSetId, id } = dOpts; return request({ url: `/zm/${fromType}/${unitSetId}/application/${id}`, method: put, headers: { Content-Type: application/x-www-form-urlencoded }, transformRequest: [(data) => qs.stringify(data)], data }) } /***** 文件上传 *****/ // 图片上传通用接口 export function sysUpload(formdata) { return request({ url: /zm/sys/upload, method: post, headers: { Content-Type: multipart/form-data }, data: formdata }) }

这也是我经常在一些项目中看到的一种方式,虽然这样做到对接口的统一管理,但是从我的角度看,还是有一些不足的地方:

接口数不太多,但是代码量却拉的很长,因为重复的代码太多了,例如 export function、return request等。Content-Type不同时,需要额外费点精力处理下。因为只要是一个返回一个Promise的函数,导致写代码时个人自主性太强,很难形成一个规范,多人进行开发任务的时候,由于没有一个统一规范,不利于统一管理和维护。例如上面应用新增这个接口,某个开发人员把data这个形参放在第一个,后续其他人接手开发的时候,就很可能产生问题。

使用Proxy写接口请求服务

Proxy 对象可以拦截目标对象的任意属性,这所以很适用来写请求接口的服务。直接上代码,在src/service/index.js里的代码如下:

import qs from qs; import request from @/utils/request; // 说明: post/2, put/2, patch/2 表示Content-Type为application/x-www-form-urlencode的请求 export default new Proxy( { /***** 分组 *****/ // #region // 分组列表 groupList: get /zm/group, // 分组详情 groupDetail: { method: get, url: (id) => `/zm/group/${id}` }, // 分组新增 groupAdd: post /zm/group, // 分组修改 groupUpdate: { method: put, url: (id) => `/zm/group/${id}` }, // 分组删除 groupDel: { method: delete, url: (id) => `/zm/group/${id}` }, // 所有分组列表 groupAll: get /zm/group/all, // #endregion /***** 应用 *****/ // #region // 应用列表 applicationList: { method: get, url: (fromType, unitSetId) => `/zm/${fromType}/${unitSetId}/application`, }, // 应用详情 applicationDetail: { method: get, url: (fromType, unitSetId, id) => `/zm/${fromType}/${unitSetId}/application/${id}`, }, // 应用新增 applicationAdd: { method: post/2, url: (fromType, unitSetId) => `/zm/${fromType}/${unitSetId}/application`, config: { headers: { i-custom-header: ftx } }, }, // 应用修改 applicationUpdate: { method: put/2, url: (fromType, unitSetId, id) => `/zm/${fromType}/${unitSetId}/application/${id}`, }, // #endregion /***** 文件上传 *****/ // #region // 图片上传通用接口 sysUpload: fileupload /zm/sys/upload, // #endregion }, { cache: new Map(), handleRequest(url, method, conf) { if ([get, post, put, patch, delete].includes(method)) { const dkey = [get, delete].includes(method) ? params : data; return (params, config) => request({ url, method, [dkey]: params, ...conf, ...config, }); } else if ([post/2, put/2, patch/2].includes(method)) { const [m] = method.split(/); return (params, config) => request({ url, method: m, data: params, ...conf, ...config, headers: { ...(config?.headers ? config?.headers : conf?.headers), Content-Type: application/x-www-form-urlencoded, }, transformRequest: [(data) => qs.stringify(data)], }); } else if (method === fileupload) { return (params, config) => request({ url, method: post, data: params, ...conf, ...config, headers: { ...(config?.headers ? config?.headers : conf?.headers), Content-Type: multipart/form-data, }, }); } }, get(target, prop, receiver) { const currentCache = this.cache; const handleRequest = this.handleRequest; if (currentCache.has(prop)) return currentCache.get(prop); let fun; const methodList = [ get, post, put, patch, delete, post/2, put/2, patch/2, fileupload, ]; if (typeof target[prop] === string) { const [method, url] = target[prop].split( ); if (methodList.includes(method)) fun = handleRequest(url, method); } else if (Object.prototype.toString.call(target[prop]) === [object Object]) { const { method, url, config: config = {} } = target[prop]; if (methodList.includes(method)) { if (typeof url === string) { fun = handleRequest(url, method, config); } else if (typeof url === function) { fun = (...args) => { if (Array.isArray(args[0])) { // args[0]是一个数组,是拼接url需要的参数 args[1]为请求参数 args[2]为config参数 return handleRequest(url(...args[0]), method, config)(args[1], args[2]); } else { return handleRequest(url(...args), method, config)(); } }; } } } if (fun) { currentCache.set(prop, fun); return fun; } return Reflect.get(target, prop, receiver); }, set() { throw new Error(error); }, }, );

增加了一个缓存,提高性能。

那么我们请求接口的时候我们就可以这样去请求:

import service from @/service export default { methods: { // 请求分组列表接口 async groupList() { const res = await service.groupList({ name: , limit: 10, page: 1 }) }, // 请求分组详情接口 async groupDetail() { const res = await service.groupDetail(123) }, // 请求分组新增接口 async groupAdd() { const res = await service.groupAdd({ name: xxxx, value: 2222 }) }, // 请求分组修改接口 async groupUpdate() { const res = await service.groupUpdate([123], { name: xxxx, value: 333 }) }, // 请求分组删除接口 async groupDelete() { const res = await service.groupDelete(123, { headers: { token: 6utm3UBVGS= } }) }, // 请求应用列表接口 async applicationList() { const res = await service.applicationList([from, 234], { name: , limit: 10, page: 1 }) }, // 请求应用详情接口 async applicationDetail() { const res = await service.applicationDetail(from, 234, 123) }, // 请求应用新增接口 async applicationAdd() { const res = await service.applicationAdd([from, 234], { name: xxxx, value: 2222 }) }, // 请求应用修改接口 async applicationUpdate() { const res = await service.applicationUpdate([from, 234, 123], { name: xxxx, value: 333 }) }, // 请求图片上传通用接口 async sysUpload(File) { const formdata = new FormData() formdata.append(file, File) formdata.append(name, e123) const res = await service.sysUpload(formdata) }, } }

Proxy写接口请求服务2.0(请求接口url路径参数使用冒号 : 标记)

import qs from qs; import request from @/utils/request; import { compile } from path-to-regexp; const PATH_REGEXP = /(\\.)|([\/.])?(?:(?:\:(\w+)(?:\(((?:\\.|[^\\()])+)\))?|\(((?:\\.|[^\\()])+)\))([+*?])?|(\*))/; // 说明: post/2, put/2, patch/2 表示Content-Type为application/x-www-form-urlencode的请求 export default new Proxy( { /***** 分组 *****/ // #region // 分组列表 groupList: get /api/group, // 分组详情 groupDetail: get /api/group/:id, // 分组新增 groupAdd: post /api/group, // 分组修改 groupUpdate: put /api/group/:id, // 分组删除 groupDel: delete /api/group/:id, // 所有分组列表 groupAll: get /api/group/all, // #endregion /***** 应用 *****/ // #region // 应用列表 applicationList: get /api/:fromType/:unitSetId/application, // 应用详情 applicationDetail: get /api/:fromType/:unitSetId/application/:id, // 应用新增 applicationAdd: post/2 /api/:fromType/:unitSetId/application, // 应用修改 applicationUpdate: put/2 /api/:fromType/:unitSetId/application/:id, // #endregion /***** 文件上传 *****/ // #region // 图片上传通用接口 sysUpload: fileupload /api/sys/upload, // #endregion }, { cache: new Map(), handleRequest(url, method) { if ([get, post, put, patch, delete].includes(method)) { const dkey = [get, delete].includes(method) ? params : data; return (params, config) => request({ url, method, [dkey]: params, ...config, }); } else if ([post/2, put/2, patch/2].includes(method)) { const [m] = method.split(/); return (params, config) => request({ url, method: m, data: params, ...config, headers: { ...config?.headers, Content-Type: application/x-www-form-urlencoded, }, transformRequest: [(data) => qs.stringify(data)], }); } else if (method === fileupload) { return (params, config) => request({ url, method: post, data: params, ...config, headers: { ...config?.headers, Content-Type: multipart/form-data, }, }); } }, get(target, prop, receiver) { const currentCache = this.cache; const handleRequest = this.handleRequest; if (currentCache.has(prop)) return currentCache.get(prop); const methodList = [ get, post, put, patch, delete, post/2, put/2, patch/2, fileupload, ]; if (typeof target[prop] === string) { const [method, url] = target[prop].split( ); if (methodList.includes(method)) { const fun = PATH_REGEXP.test(url) ? (pathParams, params, config) => handleRequest(compile(url)(pathParams), method)(params, config) : handleRequest(url, method); currentCache.set(prop, fun); return fun; } } return Reflect.get(target, prop, receiver); }, set() { throw new Error(error); }, }, );

请求接口的时候我们就可以这样去请求:

import service from @/service export default { methods: { // 请求分组列表接口 async groupList() { const res = await service.groupList({ name: , limit: 10, page: 1 }) }, // 请求分组详情接口 async groupDetail() { const res = await service.groupDetail({ id: 123 }) }, // 请求分组新增接口 async groupAdd() { const res = await service.groupAdd({ name: xxxx, value: 2222 }) }, // 请求分组修改接口 async groupUpdate() { const res = await service.groupUpdate({ id: 123 }, { name: xxxx, value: 333 }) }, // 请求分组删除接口 async groupDelete() { const res = await service.groupDelete({ id: 123 }) }, // 请求应用列表接口 async applicationList() { const res = await service.applicationList({ fromType: from, unitSetId: 234 }, { name: , limit: 10, page: 1 }) }, // 请求应用详情接口 async applicationDetail() { const res = await service.applicationDetail({ fromType: from, unitSetId: 234, id: 123 }) }, // 请求应用新增接口 async applicationAdd() { const res = await service.applicationAdd({ fromType: from, unitSetId: 234 }, { name: xxxx, value: 2222 }) }, // 请求应用修改接口 async applicationUpdate() { const res = await service.applicationUpdate({ fromType: from, unitSetId: 234, id: 123 }, { name: xxxx, value: 333 }) }, // 请求图片上传通用接口 async sysUpload(File) { const formdata = new FormData() formdata.append(file, File) formdata.append(name, e123) const res = await service.sysUpload(formdata) }, } }

请求多个域名的下接口

import qs from qs; import request from @/utils/request; import { compile } from path-to-regexp; const PATH_REGEXP = /(\\.)|([\/.])?(?:(?:\:(\w+)(?:\(((?:\\.|[^\\()])+)\))?|\(((?:\\.|[^\\()])+)\))([+*?])?|(\*))/; const serverProxy = { /base: { target: //www.base.xyz, pathRewrite: { ^/base: , }, }, /api: { target: //www.api.xyz, pathRewrite: { ^/api: , }, }, }; /** * @description 组合url */ export const combineURLs = (baseURL, relativeURL) => relativeURL ? `${baseURL.replace(/\/+$/, )}/${relativeURL.replace(/^\/+/, )}` : baseURL; /** * @description 判断是否绝对路径 */ export const isAbsoluteURL = (url) => /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); const proxyList = Object.keys(serverProxy).map((context) => { let proxyOptions; const correctedContext = context.replace(/^\*$/, **).replace(/\/\*$/, ); if (typeof serverProxy[context] === string) { proxyOptions = { context: correctedContext, target: serverProxy[context], }; } else { proxyOptions = Object.assign({}, serverProxy[context]); proxyOptions.context = correctedContext; } return proxyOptions; }); /** * @description 返回请求的接口的url */ const handleUrl = (path) => { let url = path; if (isAbsoluteURL(url)) { return url; } else { for (const proxy of proxyList) { const reg = new RegExp(`^${proxy.context}/`); if (url.match(reg)) { if (proxy.pathRewrite) { Object.keys(proxy.pathRewrite).forEach((regKey) => { url = url.replace(new RegExp(regKey), proxy.pathRewrite[regKey]); }); return combineURLs(proxy.target, url); } else { return combineURLs(proxy.target, url); } } } } return url; }; // 说明: post/2, put/2, patch/2 表示Content-Type为application/x-www-form-urlencode的请求 export default new Proxy( { /***** 分组 *****/ // #region // 分组列表 groupList: get /api/group, // 分组详情 groupDetail: get /api/group/:id, // 分组新增 groupAdd: post /api/group, // 分组修改 groupUpdate: put /api/group/:id, // 分组删除 groupDel: delete /api/group/:id, // 所有分组列表 groupAll: get /api/group/all, // #endregion /***** 应用 *****/ // #region // 应用列表 applicationList: get /base/:fromType/:unitSetId/application, // 应用详情 applicationDetail: get /base/:fromType/:unitSetId/application/:id, // 应用新增 applicationAdd: post/2 /base/:fromType/:unitSetId/application, // 应用修改 applicationUpdate: put/2 /base/:fromType/:unitSetId/application/:id, // #endregion /***** 文件上传 *****/ // #region // 图片上传通用接口 sysUpload: fileupload /api/sys/upload, // #endregion }, { cache: new Map(), handleRequest(url, method) { if ([get, post, put, patch, delete].includes(method)) { const dkey = [get, delete].includes(method) ? params : data; return (params, config) => request({ url, method, [dkey]: params, ...config, }); } else if ([post/2, put/2, patch/2].includes(method)) { const [m] = method.split(/); return (params, config) => request({ url, method: m, data: params, ...config, headers: { ...config?.headers, Content-Type: application/x-www-form-urlencoded, }, transformRequest: [(data) => qs.stringify(data)], }); } else if (method === fileupload) { return (params, config) => request({ url, method: post, data: params, ...config, headers: { ...config?.headers, Content-Type: multipart/form-data, }, }); } }, get(target, prop, receiver) { const currentCache = this.cache; const handleRequest = this.handleRequest; if (currentCache.has(prop)) return currentCache.get(prop); const methodList = [ get, post, put, patch, delete, post/2, put/2, patch/2, fileupload, ]; if (typeof target[prop] === string) { const [method, url] = target[prop].split( ); if (methodList.includes(method)) { const fun = PATH_REGEXP.test(url) ? (pathParams, params, config) => handleRequest(handleUrl(compile(url)(pathParams)), method)(params, config) : handleRequest(handleUrl(url), method); currentCache.set(prop, fun); return fun; } } return Reflect.get(target, prop, receiver); }, set() { throw new Error(error); }, }, );

如果你的项目中有需要jsonp的请求方式,或者是app内嵌的h5需要调用native的方法,都可以用类似的方法处理,例如这样的去做处理(写的比较简单):

import jsonp from @/utils/jsonp import util from @/utils/util export default new Proxy( { authStatus: jsonp /user/authStatus, getClientInfo: nativepost /client/getClientInfo, }, { get(target, prop, receiver) { if (typeof target[prop] === string) { const [method, url] = target[prop].split( ); if (method === jsonp) return (params, conf) => jsonp(url, params, conf) if (method === nativepost) { if (util.isIos()) { return (params, conf) => IosNativePost(url, params, conf) } else if (util.isAndroid()) { return (params, conf) => AndroidNativePost(url, params, conf) } } } return Reflect.get(target, prop, receiver); } } );

使用Proxy的方式还有许多的好处,例如:1、在请求接口之前做拦截,就拿我举例子,我以前做的一个项目,需要接入单点登录,单点架构部门是通过提供一个jsonp的接口,如果请求需要鉴权的接口,让我们去请求他们的服务,然后会返回登录是否有权限或者超时,如果超时需要我们自己跳转到单点登录页。对于这种需求用Proxy很方便,在需要鉴权的接口之前请求下认证接口,如果认证通过则返回函数,认证不通过跳转登录页。 2、做vue、react ssr的情况下请求接口,比如请求的接口的时候需要携带cookie认证信息,一般我们是使用axios作http库(因为它可以从node.js创建 http 请求),在node环境中需要加入{ headers: { cookie: xxx=www } },但是在浏览器中是不能有这段代码的,通过使用Proxy拦截的方式,很容易就能解决这么个问题。

以上,内容到这里就介绍完了。