我的移動端web app前后端分離后,前端頁面的靜態(tài)資源從后端分離,交由cdn加速昔榴,而后端也不再處理頁面渲染妓肢,只提供業(yè)務數(shù)據(jù)供前端通過ajax獲取捌省。
雖然這帶來了喜聞樂見的跨域問題,但是現(xiàn)代瀏覽器都通過XMLHttpRequest對象實現(xiàn)了對CORS的原生支持碉钠,我們只需要注意有限幾點就可以優(yōu)雅得跨域取數(shù)據(jù)纲缓。
服務器設置允許跨域
瀏覽器發(fā)送的跨域請求都會有一個Origin頭部,服務器需要根據(jù)這個頭部信息來判斷是否為合法跨域喊废,如果接受這個跨域請求祝高,需要在響應時在Access-Control-Allow-Origin頭部回發(fā)相同的源信息。如果服務器的響應沒有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);
// 允許攜帶票據(jù)
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)
對于不滿足上面簡單請求條件的跨域請求,姑且稱“復雜請求”陆蟆,瀏覽器發(fā)送前會先向服務器自動發(fā)送一個OPTIONS請求 以確保復雜請求是可以正常發(fā)出的雷厂。服務器需要作出與簡單請求類似的響應。
以Node.js的express為例:
// enable pre-flight
app.options('*', function(req, res) {
// pre-flight可被緩存的秒數(shù)
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
瀏覽器端發(fā)起跨域請求
傳統(tǒng) Ajax 指的是 XMLHttpRequest(XHR),但是XMLHttpRequest 是一個設計粗糙的 API林束,不符合關注分離(Separation of Concerns)的原則钩杰,配置和調(diào)用方式非常混亂诊县,而且基于事件的異步模型寫起來也沒有現(xiàn)代的 Promise友好讲弄。
Fetch API 是基于 Promise 設計,可以很好的解決XHR的問題依痊。但是Fetch自身也存在一些問題避除,我從使用到放棄的過程中遇到的最大問題是,不原生支持請求超時胸嘁,而通過setTimeout模擬只是“自欺欺人”瓶摆,很容易出現(xiàn)瀏覽器端被模擬超時中止掉之后,服務器端仍然處理了請求性宏。
后來我就通過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);
// 默認情況下毫胜,瀏覽器發(fā)起的跨域請求不提供票據(jù)(cookie等)
// 當服務器設置了允許攜帶票據(jù)后书斜,還要在瀏覽器端設置攜帶票據(jù)
req.withCredentials = withCredentials;
// 請求數(shù)據(jù)格式統(tǒng)一為json
// 為了符合跨域的Simple requests要求
// 借助Content-Language與服務器協(xié)商替代
// '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: '網(wǎng)絡異常' });
}
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,會觸發(fā) 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']);
} }));