我的移动端web app前后端分离后,前端页面的静态资源从后端分离,交由cdn加速,而后端也不再处理页面渲染,只提供业务数据供前端通过ajax获取。
虽然这带来了喜闻乐见的跨域问题,但是现代浏览器都通过XMLHttpRequest对象实现了对CORS的原生支持,我们只需要注意有限几点就可以优雅得跨域取数据。
服务器设置允许跨域
浏览器发送的跨域请求都会有一个Origin头部,服务器需要根据这个头部信息来判断是否为合法跨域,如果接受这个跨域请求,需要在响应时在Access-Control-Allow-Origin头部回发相同的源信息。如果服务器的响应没有Access-Control-Allow-Origin头部,或信息与源信息不匹配,浏览器会驳回请求。
以Node.js的express为例:
var app = require('express')();
app.use(function(req, res, next) {
var origin = req.header('origin');
// 指定域名的跨域
// if(!/baidu|qq|alibaba/.test()) return next();
// 允许跨域
res.header("Access-Control-Allow-Origin", origin);
// 允许携带票据
res.header("Access-Control-Allow-Credentials", true);
// 允许跨域自定义的 Header
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});
简单请求
简单请求是指能够满足以下条件的跨域请求:
请求方法仅限于:
GET
HEAD
POST设置的请求头仅限于:
Accept
Accept-Language
Content-Language
Content-TypeContent-Type的值仅限于:
application/x-www-form-urlencoded
multipart/form-data
text/plain
详情参考
另外:
Webkit will force any cross-origin request to be preflighted simply if you register an onprogress event handler.
预请求(Preflighted requests)
对于不满足上面简单请求条件的跨域请求,姑且称“复杂请求”,浏览器发送前会先向服务器自动发送一个OPTIONS请求 以确保复杂请求是可以正常发出的。服务器需要作出与简单请求类似的响应。
以Node.js的express为例:
// enable pre-flight
app.options('*', function(req, res) {
// pre-flight可被缓存的秒数
res.header('Access-Control-Max-Age', 3);
res.end();
});
使用cors简化服务器端配置
以上一、三中基于express的设置可用通过引入cors简化:
var app = require('express')();
// 配置跨域
//
var cors = require('cors');
app.use(cors({
origin: /baidu|qq|alibaba/,
credentials: true,
maxAge: 60*60*24*100 // pre-flight时效,100天
}));
app.options('*', cors());// enable pre-flight
浏览器端发起跨域请求
传统 Ajax 指的是 XMLHttpRequest(XHR),但是XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise友好。
Fetch API 是基于 Promise 设计,可以很好的解决XHR的问题。但是Fetch自身也存在一些问题,我从使用到放弃的过程中遇到的最大问题是,不原生支持请求超时,而通过setTimeout模拟只是“自欺欺人”,很容易出现浏览器端被模拟超时中止掉之后,服务器端仍然处理了请求。
后来我就通过XHR模拟Fetch:
// fetch风格的ajax post
function _post(url, data, withCredentials = false) {
return new Promise((resolve, reject) => {
var req = new XMLHttpRequest();
// 启动一个post,到指定接口,异步
req.open('post', url, true);
// 默认情况下,浏览器发起的跨域请求不提供票据(cookie等)
// 当服务器设置了允许携带票据后,还要在浏览器端设置携带票据
req.withCredentials = withCredentials;
// 请求数据格式统一为json
// 为了符合跨域的Simple requests要求
// 借助Content-Language与服务器协商替代
// 'Content-Type': 'application/json; charset=utf-8'
req.setRequestHeader('Content-Language', 'json');
data = JSON.stringify(data) || null;
// 设置超时
req.timeout = timeout;
req.ontimeout = function() {
reject({ message: '请求超时' });
};
// xhr.readystate = 4
req.onload = function() {
let result = req.responseText;
// 某些情况(如服务器宕机)会导致访问req.status报错
if(req.status < 400) {
if(/json/.test(req.getResponseHeader('Content-Type'))) {
result = JSON.parse(result);
}
resolve(result);
}
else reject({ message: result, status: req.status });
};
// Network error
req.onerror = function() {
reject({ message: '网络异常' });
}
req.send(data);
});
}
server端借助header的Content-Language处理json:
// config body parser
//
var bodyParser = require('body-parser');
// parse application/json
// Content-Type 为 application/json 的 cors request 不符合 simple requests,会触发 pre-flight
// 约定:Content-Type: application/json 用 content-language 含有 "json" 代替
app.use(bodyParser.json({ type: function(req) {
return
/json/.test(req.headers['content-type']) ||
/json/.test(req.headers['content-language']);
} }));