用5种方式实现本地代理服务器

2,830 阅读6分钟

前端本地开发时,除了本地mock数据,往往还需要和后台联调接口。最简单的联调接口的方式就是,利用本地的Node.js server将请求转发到后台的本地环境。这样,前后台修改了代码后,可以马上看到效果,而不需要重新刷版本到环境上。

下面就来介绍一下,如何使用Node.js实现一个代理服务器。

1.使用Node.js原生的http(s).request实现

思路:本地的Node.js server收到浏览器发出的请求后,分别获取到请求头和请求体,再使用http(s).request向对应的后台server发送请求。

http(s).request的API如下: http.request

有几个事件:

  • data

    每当有一个字符的响应数据返回,则触发一次data事件。如果需要保存响应的数据,则需要事先定义一个缓冲变量buffer,每触发一次data事件,则往buffer变量中追加数据(类似字符串拼接的方式)

  • end

    response成功结束

  • error

    遇到错误

由于这种事件回调的方式会和业务逻辑强耦合,容易形成回调地狱。为了将这部分逻辑独立出来,使用一个promise将这部分逻辑进行封装,便于外部获取。

代码如下:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const https = require('https');
const config = require('./config/config');
const token = '666'; // 这个token是通过IAM鉴权后台得到的,和本文关系不大。为了简便,暂时写死;实际场景中如果需要获取,可以通过调接口异步地获取。

const app = express();
app.use(bodyParser.json());
// 这里用的是HTTPS协议,所以才需要生成证书。如果用HTTP协议,则不需要。
const privateKey = fs.readFileSync('./cert/private.pem', 'utf8');
const certificate = fs.readFileSync('./cert/file.crt', 'utf8');
const publicKey = fs.readFileSync('./cert/csr.pem', 'utf8');

function getProxyByHttps(token, req) {
    return new Promise((resolve, reject) => {
        const options = {
            hostname: '192.168.1.101',
            port: 7443,
            path: req.url,
            method: req.method,
            headers: {
                // 后台接口需要的请求头就在这里添加
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'X-Auth-Token': token
            },
            rejectUnauthorized: false
        };
        const request = https.request(options, (res) => {
            let buffer = '';
            res.on('data', data => {
                buffer += data;
            });
            res.on('end', () => {
                try {
                    resolve(JSON.parse(buffer)); // 返回的数据是原始的字符串形式的,需要解析成JSON,才可以返回给浏览器
                } catch (err) {
                    reject(err);
                }
            });
        });
        request.on('error', err => {
            reject(err);
        });
        request.end();
    });
}

app.use('/rest', async (req, res) => {
    try {
        const result = await getProxyByHttps(token, req);
        res.send(result);
    } catch (err) {
        console.error(err);
    }
});
// 加第二个参数0.0.0.0是为了允许从其他IP访问。如果只需要在本地访问,则0.0.0.0可以不加
app.listen(3000, '0.0.0.0');

总结:

这种方式的优点如下: 1.http(s).request是Nodejs原生的API,不需要引入任何中间件来做转发

2.可以在其他机器上调用接口,不限于本地调用。e.g. 可以把本地的IP发给其他人,他们在各自的电脑上也可以访问本地的server。

缺点:

需要在事件回调中自行拼装响应数据,代码比较繁琐

2. 使用request中间件来实现

request简介:request是封装了http.request而来的一个三方件,也是使用http.request向后台server发送数据,

思路同上

代码和1类似:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const request = require('request');
const config = require('./config/config');
const token = '666'; // 这个token是通过IAM鉴权后台得到的,和本文关系不大。为了简便,暂时写死;实际场景中如果需要获取,可以通过调接口异步地获取。

const app = express();
app.use(bodyParser.json());
// 这里用的是HTTPS协议,所以才需要生成证书。如果用HTTP协议,则不需要。
const privateKey = fs.readFileSync('./cert/private.pem', 'utf8');
const certificate = fs.readFileSync('./cert/file.crt', 'utf8');
const publicKey = fs.readFileSync('./cert/csr.pem', 'utf8');

function getProxyByRequest(token, req) {
    const options = {
        url: 'https://192.168.1.101:7443' + req.url,
        method: req.method,
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'X-Auth-Token': token
        },
        body: JSON.stringify(req.body),
        strictSSL: false,
    };
    return new Promise((resolve, reject) => {
        let buffer = '';
        request(options, (error, response, body) => {
        }).on('data', (data) => {
            buffer += data;
        }).on('response', (response) => {
            // resolve(buffer);
        }).on('end', () => {
            try {
                resolve(JSON.parse(buffer));
            } catch (err) {
                reject(err);
            }

        }).on('error', (err) => {
            reject(err);
        });
    });
}

app.use('/rest', getProxy(token));
// 加第二个参数0.0.0.0是为了允许从其他IP访问。如果只需要在本地访问,则0.0.0.0可以不加
app.listen(3000, '0.0.0.0');

同样需要拼接响应数据

有没有不需要拼接响应数据的方式呢?下面就介绍几种httpclient中间件:

3. 采用Axios实现

Axios是一个 基于promise的http client库

代码如下:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const axios = require('axios');
const config = require('./config/config');
const token = '666'; // 这个token是通过IAM鉴权后台得到的,和本文关系不大。为了简便,暂时写死;实际场景中如果需要获取,可以通过调接口异步地获取。

const app = express();
app.use(bodyParser.json());
// 这里用的是HTTPS协议,所以才需要生成证书。如果用HTTP协议,则不需要。
const privateKey = fs.readFileSync('./cert/private.pem', 'utf8');
const certificate = fs.readFileSync('./cert/file.crt', 'utf8');
const publicKey = fs.readFileSync('./cert/csr.pem', 'utf8');

function getProxyByAxios(token, req) {
    const instance = axios.create({
        baseURL: 'https://192.168.1.101:7443/',
        headers: { 'X-Auth-Token': token }
    });
    return instance.request({
        url: req.url,
        method: req.method,
        headers: req.headers,
        data: req.body,
        httpsAgent: new https.Agent({ rejectUnauthorized: false }) // 由于本地的server是使用自前面的https证书搭建的,为了避免未认证的问题,需要设置此项
    });
}

app.use('/rest', async (req, res) => {
    try {
        const result = await getProxyByAxios(token, req);
        res.send(result.data);
    } catch (err) {
        console.error(err);
    }
});
// 加第二个参数0.0.0.0是为了允许从其他IP访问。如果只需要在本地访问,则0.0.0.0可以不加
app.listen(3000, '0.0.0.0');

注意:axios返回的promise resolve之后,得到的是一个包含响应数据的对象,响应数据在data属性中

4. 使用superagent实现

superagent也是一个http client库,详见:github仓库

思路和3类似

代码如下:

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const axios = require('axios');
const config = require('./config/config');
const token = '666'; // 这个token是通过IAM鉴权后台得到的,和本文关系不大。为了简便,暂时写死;实际场景中如果需要获取,可以通过调接口异步地获取。

const app = express();
app.use(bodyParser.json());
// 这里用的是HTTPS协议,所以才需要生成证书。如果用HTTP协议,则不需要。
const privateKey = fs.readFileSync('./cert/private.pem', 'utf8');
const certificate = fs.readFileSync('./cert/file.crt', 'utf8');
const publicKey = fs.readFileSync('./cert/csr.pem', 'utf8');

function getProxyByAgent(token, req) {
    const options = {
        hostname: '127.0.0.1',
        port: 3001,
        path: '/',
        method: req.method,
        key: privateKey,
        cert: publicKey
    };
    options.agent = new https.Agent(options);
    return superagent
        .agent(options.agent)
        .post('https://127.0.0.1:7443' + req.url)
        .trustLocalhost()  // https://visionmedia.github.io/superagent/
        .send(req.body) // sends a JSON post body
        .set('X-Auth-Token', token)
        // .key(privateKey)
        // .cert(certificate)
        // .set('accept', 'json')
        ;
}

app.use('/rest', async (req, res) => {
    try {
        const result = await getProxyByAgent(token, req);
        res.send(result);
    } catch (err) {
        console.error(err);
    }
});
// 加第二个参数0.0.0.0是为了允许从其他IP访问。如果只需要在本地访问,则0.0.0.0可以不加
app.listen(3000, '0.0.0.0');

缺点:由于superagent对https支持不好(详见: issue

只能支持转发到本地的server,这在实际开发中基本不可行。

5. 使用express-http-proxy实现

简介:express-http-proxy是express的http-proxy插件,可以使用express开启一个代理服务器。 这个中间件对外暴露了几个事件钩子,可以根据需要选择适当的钩子函数进行业务逻辑的处理。所有的配置项详见 Github

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const expressProxy = require('express-http-proxy');
const config = require('./config/config');
const token = '666'; // 这个token是通过IAM鉴权后台得到的,和本文关系不大。为了简便,暂时写死;实际场景中如果需要获取,可以通过调接口异步地获取。

const app = express();
app.use(bodyParser.json());
// 这里用的是HTTPS协议,所以才需要生成证书。如果用HTTP协议,则不需要。
const privateKey = fs.readFileSync('./cert/private.pem', 'utf8');
const certificate = fs.readFileSync('./cert/file.crt', 'utf8');
const publicKey = fs.readFileSync('./cert/csr.pem', 'utf8');

function getProxy(token) {
    return expressProxy('https://192.168.1.101:7443', {
        proxyReqPathResolver(req) {
            return req.url;
        },
        proxyReqOptDecorator(proxyReqOpts, srcReq) {
            proxyReqOpts.rejectUnauthorized = false;
            proxyReqOpts.headers['X-Auth-Token'] = token;
            return proxyReqOpts;
        },
        proxyReqBodyDecorator(bodyContent, srcReq) {
            return bodyContent;
        }
    })
}

app.use('/rest', async (req, res) => {
    try {
        const result = await getProxyByHttps(token, req);
        res.send(result);
    } catch (err) {
        console.error(err);
    }
});
// 加第二个参数0.0.0.0是为了允许从其他IP访问。如果只需要在本地访问,则0.0.0.0可以不加
app.listen(3000, '0.0.0.0');