基于 Fetch 的 HTTP 透明代理

本文从属于笔者的Web前端中DOM系列文章.

笔者在浏览器跨域方法与基于Fetch的Web请求最佳实践一文中介绍了浏览器跨域的基本知识与Fetch的基本使用,在这里要提醒两个前文未提到的点,一个是根据附带凭证信息的请求这里描述的,当你为了配置在CORS请求中附带Cookie等信息时,来自于服务器的响应中的Access-Control-Allow-Origin不可以再被设置为 * ,必须设置为某个具体的域名,则响应会失败。另一个就是因为Fetch中不自带Cancelable与超时放弃功能,往往需要在代理层完成。笔者在自己的工作中还遇到另一个请求,就是需要在客户端抓取其他没有设置CORS响应或者JSONP响应的站点,而必须要进行中间代理层抓取。笔者为了尽可能小地影响逻辑层代码,因此在自己的封装中封装了如下方法:


/**
 * @function 通过透明路由,利用get方法与封装好的QueryParams形式发起请求
 * @param BASE_URL 请求根URL地址,注意,需要添加http://以及末尾的/,譬如`http://api.com/`
 * @param path 请求路径,譬如"path1/path2"
 * @param queryParams 请求的查询参数
 * @param contentType 请求返回的数据格式
 * @param proxyUrl 请求的路由地址
 */
getWithQueryParamsByProxy({BASE_URL=Model.BASE_URL, path="/", queryParams={}, contentType="json", proxyUrl="http://api.proxy.com"}) {

    //初始化查询字符串,将BASE_URL以及path进行编码
    let queryString = `BASE_URL=${encodeURIComponent(BASE_URL)}&path=${encodeURIComponent(path)}&`;

    //根据queryParams构造查询字符串
    for (let key in queryParams) {

        //拼接查询字符串
        queryString += `${key}=${encodeURIComponent(queryParams[key])}&`;

    }

    //将查询字符串进行编码
    let encodedQueryString = (queryString);

    //封装最终待请求的字符串
    const packagedRequestURL = `${proxyUrl}?${encodedQueryString}action=GET`;

    //以CORS方式发起请求
    return this._fetchWithCORS(packagedRequestURL, contentType);

}

另外自带缓存的透明代理层的配置为,代码存放于Github仓库:


/**
 * Created by apple on 16/7/26.
 */
var express = require('express');
var cors = require('cors');

import Model from "../model/model";
import ServerCache from "./server_cache";

//创建服务端缓存实例
const serverCache = new ServerCache();

/**
 * @region 全局配置
 * @type {string}
 */
const hashKey = "ggzy"; //缓存的Hash值
const timeOut = 5; //设置超时时间,5秒
/**
 * @endregion 全局配置
 */

//添加跨域支持
var app = express(cors());

//默认的GET类型的透明路由
app.get('/get_proxy', cors(), (req, res)=> {

    //所有查询参数是以GET方式传入
    //获取原地址
    let BASE_URL = decodeURIComponent(req.query.BASE_URL);

    //获取原路径
    let path = decodeURIComponent(req.query.path);

    //反序列化请求参数集合
    let params = {};

    //构造生成的全部的字符串
    let url = "";

    //遍历所有传入的参数集合
    for (let key in req.query) {

        if (key == "BASE_URL" || key == "path") {
            //对于传入的根URL与路径直接忽略,
            //封装其他参数
            continue;
        } else {
            params[key] = decodeURIComponent(req.query[key]);
        }

        url += `${key}${req.query[key]}`;

    }

    //判断缓存中是否存在值
    serverCache.get(hashKey, url).then((data)=> {

        //如果存在数据
        res.set('Access-Control-Allow-Origin', '*');
        res.send(data);
        res.end();

    }).catch((error)=> {

        //如果不存在数据,执行数据抓取
        //发起GET形式的请求
        const model = new Model();

        //判断是否已经返回
        let isSent = false;

        //使用模型类发起请求,并且不进行解码直接返回
        model.getWithQueryParams({
            BASE_URL,
            path,
            params,
            contentType: "text" //不进行解码,直接返回
        }).then((data)=> {

            if (isSent) {
                //如果已经设置了超时返回,则直接返回
                return;
            }
            //返回抓取到的数据
            res.set('Access-Control-Allow-Origin', '*');
            res.send(data);
            res.end();

            isSent = true;

        }, (error)=> {

            if (isSent) {
                //如果已经设置了超时返回,则直接返回
                return;
            }

            //如果直接抓取失败,则返回无效信息
            res.send(JSON.stringify({
                "message": "Invalid Request"
            }));

            isSent = true;

            throw error;

        });

        //设置秒超时返回N
        setTimeout(
            ()=> {

                if (isSent) {
                    //如果已经设置了超时返回,则直接返回
                    return;
                }

                //设置返回超时
                res.status(504);
                
                //终止本次返回
                res.end();

                isSent = true;

            },
            1000 * timeOut
        );

    });


});

//设置POST类型的默认路由

//默认的返回值
app.get('/', function (req, res) {
    res.send('Hello World!');
    res.end();

});

//启动服务器
var server = app.listen(399, '0.0.0.0', function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log('Example app listening at http://%s:%s', host, port);
});

笔者在这里是使用Redis作为缓存:


/**
 * Created by apple on 16/8/4.
 */
var redis = require("redis");

export default class ServerCache {

    /**
     * @function 默认构造函数
     */
    constructor() {

        //构造出Redis客户端
        this.client = redis.createClient();

        //监听Redis客户端创建错误
        this.client.on("error", (err) => {
            this.client = null;
            // console.log("Redis Client Error " + err);
        });
    }

    /**
     * @function 从缓存中获取数据
     * @param hashKey
     * @param url
     * @returns {Promise}
     */
    get(hashKey = "hashKey", url = "url") {

        return new Promise((resolve, reject)=> {

            if (!!this.client) {
                //从Redis中获取数据
                this.client.hget(hashKey, url, function (err, replies) {

                    //如果存在数据
                    if (!!replies) {
                        resolve(replies);
                    } else {
                        reject(err);
                    }

                });
            } else {
                reject(new Error("Invalid Client"));
            }


        });

    }


    /**
     * @function 默认将数据放置到缓存中
     * @param hashKey 存入的键
     * @param url 存入的域URL
     * @param data 存入的数据
     * @param expire 第一次存入时候的过期时间
     * @result 如果设置失败,则返回null
     */
    put(hashKey = "hashKey", url = "url", data = "data", expire = 60 * 60 * 6 * 1000) {

        //判断客户端是否有效
        if (!this.client) {
            //如果客户端无效,直接返回null
            return null;
        }

        //第一次设置的时候判断ggzy是否存在,如果不存在则设置初始值
        this.client.hlen(hashKey, function (err, replies) {

            //获取键值长度,第一次获取时候长度为0
            if (replies == 0) {

                //12小时之后删除数据
                client.expire(hashKey, expire);
            }

        });

        //设置数据
        client.hset(hashKey, url, data);

    }

}

注意,笔者在这里使用的是isomorphic-fetch,因此在服务端与客户端的底层请求上可以复用同一份代码,测试代码如下,直接使用babel-node model.test.js即可:


/**
 * Created by apple on 16/7/21.
 */

import Model from "./model";

const model = new Model();

//正常的发起请求
model
    .getWithQueryParams({
        BASE_URL: "http://ggzy.njzwfw.gov.cn/njggzy/jsgc/",
        path: "001001/001001001/001001001001/",
        queryParams: {
            Paging: 100
        },
        contentType: "text"

    })
    .then(
        (data)=> {
            console.log(data);
        }
    )
    .catch((error)=> {
        console.log(error);
    });

//使用透明路由发起请求
model
    .getWithQueryParamsByProxy({
        BASE_URL: "http://ggzy.njzwfw.gov.cn/njggzy/jsgc/",
        path: "001001/001001001/001001001001/",
        queryParams: {
            Paging: 100
        },
        contentType: "text",
        proxyUrl: "http://153.3.251.190:11399/"

    })
    .then(
        (data)=> {
            console.log(data);
        }
    )
    .catch((error)=> {
        console.log(error);
    });

    原文作者:HTTP
    原文地址: https://juejin.im/entry/57a310eda633bd006030fb27
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞