前端开发过程中经常遇到接口跨域问题, 很难处理. 因此有了本地起一个 koa, 通过转发接口绕过跨域限制的方案. 这个方案具体实现步骤如下
处理同一服务的不同接口
假设本地开发的请求需要转移到 a1.ke.com 项目上, 那么我们需要做这么几件事
添加中间件, 捕获以指定字符串开始的请求, 以便后续转移
首先配置服务地址, 区分本地/dev/测试/预览/线上环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import env from "../env" ;let config : { [key in typeof env]: string } = { local : "http://dev-a1.ke.com/a1/api" , dev : "http://dev-a1.ke.com/a1/api" , test : "http://test-a1.ke.com/a1/api" , pre : "http://pre-a1.ke.com/a1/api" , prod : "http://a1.ke.com/a1/api" , };export const Const _Host = config[env];export const Const _Prefix = "/api/a1" as const ;export const Const _Match_Reg = /^\/api\/a1\/.+/ ;
安装 koa-router, 注册中间件, 添加路由以捕获特定请求
在转发 http 请求, 配置 headers 头时需要注意, 不能直接透传客户端发送的 header 头, 要采取白名单模式, 只转发特定的 header 字段, 理由如下
客户端请求的 host(dev-server 地址)和实际请求域名(api 服务)不一致, 则对方 Nginx 服务器无法根据 host 值做端口转发, http 报 403, https 报证书验证失败
如果后续修改过请求内容, content-length 会和实际请求长度不一致, 则有可能被认为是非法请求被 api 服务端直接拒绝
使用 axios 进行请求转发时, cookie 不能为 undefined, 只能为空字符串或不传, 否则 axios 转发时会报配置异常----如果 h5 环境中正好没有 cookie, 那么 ctx.request.headers?.[“cookie”]就是 undefined, 不加兜底的""就会导致无法转发网络请求
具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 import * as A1_Ke_Com_ApiHost from "~/src/config/api-host/a1.ke.com" ;import Router from "koa-router" ;import Koa from "koa" ;import axios from "axios" ;import cookie from "cookie" ;type Type _Prefix = typeof A1_Ke_Com_ApiHost.Const_Prefix ;let http = axios.create ();function getApiHost (prefix: Type_Prefix ) { switch (prefix) { case A1_Ke_Com_ApiHost.Const_Prefix : return A1_Ke_Com_ApiHost.Const_Host ; default : return A1_Ke_Com_ApiHost.Const_Host ; } }let getAsyncRedirectResponse = (prefix: Type_Prefix ) => { return async (ctx : Koa .ParameterizedContext ) => { let headers = { cookie : ctx.request .headers ?.["cookie" ] || "" , "user-agent" : ctx.request .headers ?.["user-agent" ] || "" , accept : "application/json" , }; let cookieStr = ctx.request .headers ?.["cookie" ] || "" ; let cookieObj : { token?: string ; } = cookie.parse (cookieStr); let token = cookieObj.token || "" ; if (prefix === A1_Ke_Com_ApiHost.Const_Prefix ) { headers["a1.ke.com-token" ] = token; } let rawRequestUrl = ctx.request .url ; let requestUrl = rawRequestUrl.split (prefix)[1 ]; let api_host = getApiHost (prefix); let targetUrl = `${api_host} /${requestUrl} ` ; let response; if (ctx.request .method === "GET" ) { response = await http.get (targetUrl, { headers : headers, }); } else { response = await http.post ( targetUrl, { ...ctx.request ?.body , }, { headers : headers, } ); } if (response?.status === 200 ) { ctx.body = response?.data || "" ; ctx.set ("Content-Type" , response?.headers ?.["content-type" ]); } else { ctx.status = response?.status ; ctx.body = { success : false , }; ctx.set ("Content-Type" , "application/json" ); } return ; }; };let totalRouter = new Router ();let a1_ke_com_ApiRouter = new Router (); a1_ke_com_ApiRouter.all ( A1_Ke_Com_ApiHost.Const_Match_Reg , getAsyncRedirectResponse (A1_Ke_Com_ApiHost.Const_Prefix ) ); totalRouter.use (a1_ke_com_ApiRouter.routes ());export default (_) => { return totalRouter.routes (); };
编写完中间件服务后, 在src/index.ts
中启用该中间件即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 require ("module-alias" ).addAlias ("~/src" , __dirname + "/" );import Koa from "koa" ;import ApiRedirectService from "~/src/service/api_redirect" ;const app = new Koa (); app.use (ApiRedirectService ); app.use (async (ctx) => { ctx.body = "Hello World" ; }); app.listen (3000 );
这样, 本地开发时, js 只要请求/api/a1/hello/world
, 经 dev-server 转发到刚才启动的 koa 服务上后, 即可被转发给http://a1.ke.com/a1/api/hello/world
(注意 h5 发出的请求是/api/a1, 实际有效请求 url 是/hello/world
, koa 将 config 中配置的 host 地址http://a1.ke.com/a1/api
和有效 url 请求/hello/world
拼接后, 生成最后的实际请求地址http://a1.ke.com/a1/api/hello/world
)
处理多个服务的接口转发请求
在上述单服务端口转发请求示例中, 我们通过src/config/api-host/a1.ke.com.ts
, getApiHost
和getAsyncRedirectResponse
已经留出了配置多个服务的扩展空间, 这里仅以添加对 b2.ke.com 的转发服务为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import env from "../env" ;let config : { [key in typeof env]: string } = { local : "http://dev-b2.ke.com/b2/api" , dev : "http://dev-b2.ke.com/b2/api" , test : "http://test-b2.ke.com/b2/api" , pre : "http://pre-b2.ke.com/b2/api" , prod : "http://b2.ke.com/b2/api" , };export const Const _Host = config[env];export const Const _Prefix = "/api/b2" as const ;export const Const _Match_Reg = /^\/api\/b2\/.+/ ;
调整getApiHost
和getAsyncRedirectResponse
的内容, 添加 b2 转发的 case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import * as B2_Ke_Com_ApiHost from "~/src/config/api-host/b2.ke.com" ;type Type _Prefix = | typeof A1_Ke_Com_ApiHost.Const_Prefix | typeof B2_Ke_Com_ApiHost.Const_Prefix ;function getApiHost (prefix: Type_Prefix ) { switch (prefix) { case A1_Ke_Com_ApiHost.Const_Prefix : return A1_Ke_Com_ApiHost.Const_Host ; case B2_Ke_Com_ApiHost.Const_Prefix : return B2_Ke_Com_ApiHost.Const_Host ; default : return A1_Ke_Com_ApiHost.Const_Host ; } }if (prefix === A1_Ke_Com_ApiHost.Const_Prefix ) { headers["a1.ke.com-token" ] = token; }if (prefix === B2_Ke_Com_ApiHost.Const_Prefix ) { }let b2_ke_com_ApiRouter = new Router (); b2_ke_com_ApiRouter.all ( B2_Ke_Com_ApiHost.Const_Match_Reg , getAsyncRedirectResponse (B2_Ke_Com_ApiHost.Const_Prefix ) ); totalRouter.use (b2_ke_com_ApiRouter.routes ());
这样, 通过一个文件即可解决前端开发中对接口转发的需求.
示例项目可戳 => https://github.com/YaoZeyuan/demo-koa-api-proxy