我的ajax跨域方案

我的移动端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();
});

简单请求

简单请求是指能够满足以下条件的跨域请求:

  1. 请求方法仅限于:
    GET
    HEAD
    POST

  2. 设置的请求头仅限于:
    Accept
    Accept-Language
    Content-Language
    Content-Type

  3. Content-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']);
} }));

你可能感兴趣的:(我的ajax跨域方案)