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