使用Express搭建一个简单的请求转发层

4,120 阅读6分钟

由于前端同学要对应多个后端,就需要调用多个域名下的接口。方案一是用go起一个中间层,将多个后端的数据汇聚到中间层,前端同学只需要调用中间层的数据即可,但是这个方案要面临走一套完整的申请资源等流程,为了节省时间,决定暂时采用方案二,即用Express做个中间层,起到简单的请求转发功能。

拦截前端请求

转发请求的第一步就是拦截前端请求,拿到请求数据,具体来说就是拿到http的header和body。

1.先上代码

//server.js
var express = require('express')
var app = express()
var api_route = require('./api_route.js')
app.use('/dev-api/*', api_route)
app.listen('8888')
//api_route.js
router.post('/', function(req, res, next) {
console.log(req.headers, req.body)
console.log(req.query, req.params)
})

2.简单解释

(1).server.js是启动express以及请求转发的主体,其中app.listen('8888')将express启动并监听在指定端口。app.use('/dev-api/*', api_route)意思是拦截url中前缀为'scheme+ip+port/dev-api'的所有请求,且处理这些请求的路由为api_route。

(2).api_route中使用router的post方法对post类型请求做统一处理,同理post()方法的第一个参数也是路径,express支持多种方式匹配url,详见路径匹配规则。参数中的req即为前端发送的http请求,其中headers并没有出现在Properties中,但实际使用中req.headers确实是可访问属性,某一次get请求拦截到的req.headers如下。

{
 host: 'localhost:8888',
 connection: 'keep-alive',
 pragma: 'no-cache',
 'cache-control': 'no-cache',
 accept: 'application/json, text/plain, */*',
authorization:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpkX2RldmVsb3BlciIsImF1ZCI6ImNvbnNvbGUiLCJleHAiOjE1OTgyNDU4NjgsImp0aSI6IjFnV21MbURWTXlUVHFNOFZwdmkxZ01uWUlTTiIsImlhdCI6MTU5ODIzODY2OCwiaXNzIjoicm9tZSIsIm5iZiI6MTU5ODIzODY2OCwic3ViIjoicm9tZS1qd3QifQ.J9iS3GHfJ6lCQzMr7x7w8ZkrEQ1BYs9TLOVhr - Ymx2M ',
 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36',
 'sec-fetch-site': 'same-origin',
 'sec-fetch-mode': 'cors',
 'sec-fetch-dest': 'empty',
 referer: 'http://localhost:9528/?',
 'accept-encoding': 'gzip, deflate, br',
 'accept-language': 'zh-CN,zh;q=0.9',
 cookie: 'sidebarStatus=1; vue_admin_template_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpkX2RldmVsb3BlciIsImF1ZCI6ImNvbnNvbGUi
 LCJleHAiOjE1OTgyNDU4NjgsImp0aSI6IjFnV21MbURWTXlUVHFNOFZwdmkxZ01uWUlTTiIsImlhdCI6MTU5ODIzODY2OCwiaXNzIjoicm9tZSIsIm5iZiI6MTU5ODIzODY2OCwic3ViIjoicm
 9 tZS1qd3QifQ.J9iS3GHfJ6lCQzMr7x7w8ZkrEQ1BYs9TLOVhr - Ymx2M ' 
}

req.query和req.params可以获取url中拼接的请求参数,通常情况下这两个属性不需要中间件就可以解析,如果实际请求中有值,而解析出来是{},那么可以看一下app.set(),在文档中存在这么一句话When query parser is set to disabled, it is an empty object {}, otherwise it is the result of the configured query parser.

(3).req.body的解析

相比req.params和req.query,req.body就没这么好运了,它默认是无法被解析的,文档中是这么描述的Contains key-value pairs of data submitted in the request body. By default, it is undefined, and is populated when you use body-parsing middleware such as express.json() or express.urlencoded().,默认是undefind,需要中间件来解析。现阶段http应用中content-type为application/json的情况比较多,使用body-parser中间件解析,在server.js中配置如下

var bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

body-parser具体使用方法见文档。除了application/json之外,前端涉及到上传文件的content-type为form-data,解析form-data的中间件同样有很多,其中formidable文档中这句话A Node.js module for parsing form data, especially file uploads.这个especially用的相当有吸引力,于是采用formidable解析form-data,这次项目中的form-data主要是上传文件。在api_route中的配置如下

var FormData = require('form-data')
var formidable = require('formidable')

解析文件使用如下

form.parse(req, (err, fileds, files) => {
   console.log('fileds', fileds)
   console.log('files', files)
 })

parse()方法可以解析上传的文件,其中files和fileds可以拿到文件相关信息,某次文件上传可以拿到的信息如下

fileds { fileName: 'arrow_left' }
files { uploadFile:
  File {
    domain: null,
    _events: {},
    _eventsCount: 0,
    _maxListeners: undefined,
    size: 1828,
    path: 'C:\\Users\\LIUBAO~1\\AppData\\Local\\Temp\\upload_7c6871dcda212df67cc0f4085db2bccf',
    name: 'arrow_left.png',
    type: 'image/png',
    hash: null,
    lastModifiedDate: 2020-08-24T07:47:35.681Z,
    _writeStream:
     WriteStream {
       _writableState: [Object],
       writable: false,
       domain: null,
       _events: {},
       _eventsCount: 0,
       _maxListeners: undefined,
       path: 'C:\\Users\\LIUBAO~1\\AppData\\Local\\Temp\\upload_7c6871dcda212df67cc0f4085db2bccf',
       fd: null,
       flags: 'w',
       mode: 438,
       start: undefined,
       autoClose: true,
       pos: undefined,
       bytesWritten: 1828,
       closed: true } } }

关于formidable更多使用方法见文档

转发前端请求

通过拦截前端请求部分我们已经可以把常用的内容从req中提取出来了,接下来就是构造请求参数,传给后端。用axios封装原生http的sent(),首先引入axios,api_route中配置如下

var axios = require('axios')

构造一个axios对象,api_route中配置如下

const request1 = axios.create({
  timeout: 30000
})

在调用axios.create时可以配置一些header如公共鉴权字段等等。通过拦截请求的介绍我们知道需要转发的内容类型主要用两种。

(1). 当content-type为application/json时

// url 可以直接写请求接口的全url
request1.post(url, params)
   .then(function (response) {
   }).catch(err => {
 }).finally()

这里的params就是希望给后端传递的参数,通常情况下如果不需要对数据进行加工处理,那么params = req.xxx

(2). 当content-type为formdata时

对于文件来说,我们需要把从formidable.parse()中解析到的参数构造成formdata形式,这里采用form-data作为构造form-data的中间件,最一开始想用原生的new FormData()去构造form-data。但是会有问题,所以最后还是采用了中间件。同时使用fs构建文件流(fs文档中有这么一句话This package name is not currently in use, but was formerly occupied by another package. To avoid malicious use, npm is hanging on to the package name, but loosely, and we'll probably give it to you if you want it.,但是这个四年前就不再维护的包周下载量竟然还有60万,看来还是很好用的),api_route中配置如下

var FormData = require('form-data')
var fs = require('fs')

业务逻辑中代码如下

// 构造form-data
const formdata = new FormData()
// 初始化form,用作文件解析
 const form = formidable({ multiples: true })
 // 解析文件
 form.parse(req, (err, fileds, files) => {
 // 通过fs创建文件的流
   const stream = fs.createReadStream(files.uploadFile.path)
   console.log('fileds', fileds)
   console.log('files', files)
   // 使用formdata的append方法填充内容
   if (typeof fileds.type !== 'undefined' && fileds.type) {
     formdata.append('type', fileds.type)
   }
   formdata.append('fileName', fileds.fileName)
   formdata.append('uploadFile', stream)
   const formHeader = formdata.getHeaders()
   // 使用axios发送formdata
   request1.post(url, formdata)
     .then(function (response) {
     }).catch(err => {
   })

转发后端数据

通过以上两个部分可以把请求发送给后端了,最后一步就是将后端返回的数据转发给前端,这里只做最简单的转发,当然也可以在这里加共处理后端返回数据。转发后端数据主要用到res的一些方法,代码如下

 request1.post(url, formdata)
     .then(function (response) {
     // code为2xx系列,调用send()直接返回response.data
       data = response.data
       res.send(data)
     }).catch(err => {
     // code为非2xx 系列,保留返回的status code,返回response.data (不知道status code?看我之前的文章)
     res.status(err.response.status).json(err.response.data)
   })

附属说明

上述过程构造了一个完整的转发过程。当存在多个后端时,可以通过server.js配置多个router,解决了文中开始提出的问题。方案一其实是比较健壮的方法,代码还在开发中。其实按照我的思路还有一个方案三,直接打一个node+nginx的镜像,用nginx做转发,镜像已经打好了,还没有测试。文中如有问题,请多多指教,感恩。(ps: 解决文件上传以及中间件选取过程让我掉了很多头发)