本文从属于笔者的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);
});