1. 前后端通信
1.1. 同源策略和限制
源:协议、域名、端口
限制:不是一个源的文档没有权利去操作另一个源的文档,包括 cookie, localStorage, indexDB, DOM 无法获取, Ajax 无法发送
1.2. 前后端通信方式
- Ajax 同源限制
- WebSocket 不受限制,因为请求头中加入了 Origin。他不是 Http,是基于 frame、双全工 tcp 协议的通信方式。
- CORS 支持同源和非同源
1.2.1. 如何实现浏览器内多个标签页之间的通信? (阿里)
WebSocket、SharedWorker;也可以调用 localStorage、cookies 等本地存储方式;
localStorage 另一个浏览上下文里被添加、修改或删除时,它都会触发一个事件,我们通过监听事件,控制它的值来进行页面信息通信。
Safari 在无痕模式下设置 localStorage 值时会抛出 QuotaExceededError 的异常;
// localStorage change, trigger below event
window.addEventListener('storage', function(e) {
document.querySelector('.my-key').textContent = e.key;
document.querySelector('.my-old').textContent = e.oldValue;
document.querySelector('.my-new').textContent = e.newValue;
document.querySelector('.my-url').textContent = e.url;
document.querySelector('.my-storage').textContent = e.storageArea;
});
1.2.2. WebSocket 如何兼容低浏览器?(阿里)
- Adobe Flash Socket
- ActiveX HTMLFile (IE)
- 基于 multipart 编码发送 XHR
- 基于长轮询的 XHR
1.3. 创建 Ajax 要点
XMLHttpRequest 对象的工作流程、兼容性、事件触发条件和顺序
/**
* [json 实现 ajax 的json]
* @param {[type]} options [description]
* @return {[type]} [description]
*/
util.json = function(options) {
var opt = {
url: '',
type: 'get',
data: {},
success: function() {},
error: function() {},
};
util.extend(opt, options);
if (opt.url) {
var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
var data = opt.data,
url = opt.url,
type = opt.type.toUpperCase(),
dataArr = [];
for (var k in data) {
dataArr.push(k + '=' + data[k]);
}
if (type === 'GET') {
url = url + '?' + dataArr.join('&');
xhr.open(type, url.replace(/\?$/g, ''), true);
xhr.send();
}
if (type === 'POST') {
xhr.open(type, url, true);
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(dataArr.join('&'));
}
xhr.onload = function() {
// 304 缓存中数据未变,206 video等大文件媒体资源成功
if (xhr.status === 200 || xhr.status === 304) {
var res;
if (opt.success && opt.success instanceof Function) {
res = xhr.responseText;
if (typeof res === 'string') {
res = JSON.parse(res);
opt.success.call(xhr, res);
}
}
} else {
if (opt.error && opt.error instanceof Function) {
opt.error.call(xhr, res);
}
}
};
}
};
1.4. 跨域
- Jsonp
- Hash (hash # 改变页面不刷新,? 后面 queryString 改变会刷新页面)
- postMessage (html5)
- WebSocket (html5)
- CORS(支持跨域的变种 ajax,当发送 ajax 跨域请求时,http 请求头加入 origin)
1.4.1. Jsonp
利用 script 标签可以不同源加载实现的。Jsonp 只支持 GET 请求
- 在主站客户端 window 全局注册一个函数 cb
- url 传递参数和回调函数名字
- 服务端解析 url 后根据参数拿到数据,执行这个函数,数据作为函数的参数
- 删除全局注册函数
Jsonp 的优缺点
优点
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持
- 在请求完毕后可以通过调用 callback 的方式回传结果。将回调方法的权限给了调用方。这个就相当于将 controller 层和 view 层分开了。我提供的 Jsonp 服务只提供纯服务的数据,至于提供服务以后的页面渲染和后续 view 操作都由调用者来自己定义就好了。如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,逻辑都可以使用同一个 Jsonp 服务。
缺点
- 它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求
- Jsonp 在调用失败的时候不会返回各种 HTTP 状态码。解决:timeout 触发 onerror 事件
- 安全性。万一假如提供 Jsonp 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的。那么所有调用这个 Jsonp 的网站都会存在漏洞。于是无法把危险控制在一个域名下。所以在使用 Jsonp 的时候必须要保证使用的 Jsonp 服务必须是安全可信的。
basic demo 1
Jsonp.html 页面定义一个函数,然后在远程 remote.js 中传入数据进行调用。
<script>
var localHandler = function(data){
alert('我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:' + data.result);
};
</script>
<script src="http://remoteserver.com/remote.js"></script>
remote.js 文件代码如下:
localHandler({ result: '我是远程js带来的数据' });
demo 2
怎么让远程 js 知道它应该调用的本地函数叫什么名字呢?
通过 queryString 传递回调函数名字,Jsonp 服务端读取并动态创建
Jsonp.html 页面的代码:
<script>
// 得到航班信息查询结果后的回调函数
var flightHandler = function(data){
alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。');
};
// 提供 Jsonp 服务的 url 地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
var url = "http://flightQuery.com/Jsonp/flightResult.aspx?code=CA1998&callback=flightHandler";
// 创建 script 标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把 script 标签加入 head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
</script>
不再直接把远程 js 文件写死,而是编码实现动态查询,而这也正是 Jsonp 客户端实现的核心部分。我们看到调用的 url 中传递了一个 code 参数,告诉服务器我要查的是 CA1998 次航班的信息,而 callback 参数则告诉服务器,我的本地回调函数叫做 flightHandler,所以请把查询结果传入这个函数中进行调用。
服务器读取 url 根据 queryString 生成代码:
flightHandler({
code: 'CA1998',
price: 1780,
tickets: 5,
});
jQuery 如何实现 Jsonp 调用?
$.ajax({
type: 'get',
async: false,
url: 'http://flightQuery.com/Jsonp/flightResult.aspx?code=CA1998',
dataType: 'jsonp',
jsonp: 'callback', //传递给请求处理程序或页面的,用以获得Jsonp回调函数名的参数名(一般默认为:callback)
jsonpCallback: 'flightHandler', //自定义的Jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
success: function(json) {
alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
},
error: function() {
alert('fail');
},
});
为什么我这次没有写 flightHandler 这个函数呢?而且竟然也运行成功了!jquery 在处理 Jsonp 类型的 ajax 时(虽然 jquery 也把 Jsonp 归入了 ajax,但其实它们真的不是一回事儿),自动帮你生成回调函数并把数据取出来供 success 属性方法来调用。
jsonp 错误捕获
如果你把 url 参数改成某个不存在的地址,你会惊奇的发现:虽然浏览器终端报出错误(404 或其他网络错误),但你的 error 回调却没有被执行!?
那怎么做才能使 Jsonp 的 error 回调被执行呢?
有两个方法,方法一:添加 timeout 参数。
$.ajax({
url: 'https://api.github.com/users/jarontai/repos',
type: 'GET',
dataType: 'Jsonp', // dataType 为 Jsonp
timeout: 5000, // 添加timeout参数
success: function(data) {
$('.result').text(JSON.stringify(data));
},
error: function(jqXHR, textStatus) {
// 此时textStatus为‘timeout’
$('.result').text('error');
alert('Jsonp error!');
},
});
添加 timeout 参数后,虽然 Jsonp 请求本身的错误没有被捕获,但是最终会因为超时而执行 error 回调。
方法二出场:使用 jquery Jsonp 插件 - https://github.com/jaubourg/jquery-jsonp
$.jsonp({
url: 'https://api.github.com/users/jarontai/repos',
callbackParameter: 'callback',
timeout: 5000,
error: function(xOptions, textStatus) {
// 错误发生时,立即执行
$('.result').text('error');
alert('Jsonp error!');
},
success: function(data) {
$('.result').text(JSON.stringify(data));
},
});
使用 jsonp 插件,能够在错误发生时立即执行 error 回调,并且还附带如 数据过滤 等功能
jsonp 封装
/**
* [function 在页面中注入js脚本]
* @param {[type]} url [description]
* @param {[type]} charset [description]
* @return {[type]} [description]
*/
util.createScript = function(url, charset) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
charset && script.setAttribute('charset', charset);
script.setAttribute('src', url);
script.async = true;
return script;
};
/**
* [function 获取一个随机的5位字符串]
* @param {[type]} prefix [description]
* @return {[type]} [description]
*/
util.getName = function(prefix) {
return (
prefix +
Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5)
);
};
/**
* [function jsonp]
* @param {[type]} url [description]
* @param {[type]} onsucess [description]
* @param {[type]} onerror [description]
* @param {[type]} charset [description]
* @return {[type]} [description]
*/
util.jsonp = function(url, onsuccess, onerror, charset) {
var callbackName = util.getName('tt_player');
window[callbackName] = function() {
if (onsuccess && util.isFunction(onsuccess)) {
onsuccess(arguments[0]);
}
};
var script = util.createScript(url + '&callback=' + callbackName, charset);
script.onload = script.onreadystatechange = function() {
if (!script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = script.onreadystatechange = null;
// 移除该script的 DOM 对象
if (script.parentNode) {
script.parentNode.removeChild(script);
}
// 删除函数或变量
window[callbackName] = null;
}
};
script.onerror = function() {
if (onerror && util.isFunction(onerror)) {
onerror();
}
};
document.getElementsByTagName('head')[0].appendChild(script);
};
1.4.2. Hash 原理
页面 A 中通过 iframe
嵌入 B, 需求是 A 给 B 发消息
- 拿到 B 的 url
- 改变 B 的 hash
- B 中接受
onhashchange
// 利用hash,场景是当前页面 A 通过 iframe 嵌入了跨域的页面 B
// 在A中伪代码如下:
var B = document.getElementsByTagName('iframe');
B.src = B.src + '#' + 'data';
// 在B中的伪代码如下
window.onhashchange = function() {
var data = window.location.hash;
};
1.4.3. WebSocket
WebSocket 是双全工、实时、基于 frame 的 tcp 通信协议,使用 ws://
(非加密)和 wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面代码中,有一个字段是 Origin,表示该请求的请求源(origin),即发自哪个域名。因为有了 Origin 这个字段,而且非 http 协议,所以 WebSocket 才没有实行同源政策。
var ws = new WebSocket('wss://echo.websocket.org');
onopen, onmessage, onclose
// 客户端
var ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function(e) {
console.log('Connection open ...');
ws.send('Hello WebSockets!');
};
ws.onmessage = function(e) {
console.log('Received Message: ', e.data);
ws.close();
};
ws.onclose = function(e) {
console.log('Connection closed.');
};
1.4.4. postMessage
窗口 A(http:A.com)向跨域的窗口 B(http:B.com)发送信息
// A 中代码
window.postMessage('Hi B, from A', 'http://B.com');
// B中监听代码
window.addEventListener(
'message',
function(event) {
console.log(event.origin);
console.log(event.source);
console.log(event.data);
},
false,
);
这是一个安全的跨域通信方法,postMessage(message, targetOrigin)
也是 HTML5 引入的特性。 可以给任何一个 window 发送消息,不论是否同源。第二个参数可以是 *
,但如果你设置了一个 URL 但不相符,那么该事件不会被分发。看一个普通的使用方式吧:
/*
* In window A's scripts, with A being on <http://example.com:8080>:
*/
var popup = window.open(...popup details...);
// This does nothing, assuming the window hasn't changed its location.
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");
// This will successfully queue a message to be sent to the popup, assuming
// the window hasn't changed its location.
popup.postMessage("hello there!", "http://example.com");
function receiveMessage(event)
{
// Do we trust the sender of this message? (might be
// different from what we originally opened, for example).
if (event.origin !== "http://example.com")
return;
// event.source is popup
// event.data is "hi there yourself! the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
* In the popup's scripts, running on <http://example.com>:
*/
// Called sometime after postMessage is called
function receiveMessage(event) {
// Do we trust the sender of this message?
if (event.origin !== 'http://example.com:8080') return;
// event.source is window.opener
// event.data is "hello there!"
// Assuming you've verified the origin of the received message (which
// you must do in any case), a convenient idiom for replying to a
// message is to call postMessage on event.source and provide
// event.origin as the targetOrigin.
event.source.postMessage(
'hi there yourself! the secret response ' + 'is: rheeeeet!',
event.origin,
);
}
window.addEventListener('message', receiveMessage, false);
1.4.5. CORS 跨域资源共享 (Cross-Origin Resource Sharing)
参考资料:
- https://www.maxcdn.com/one/visual-glossary/cors/
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
CORS 就是为了让 AJAX 可以实现可控的跨域访问而生的。
- 服务器设置
Access-Control-Allow-Origin
HTTP 响应头之后,允许浏览器跨域请求。 - 浏览器在头信息之中,增加一个 Origin 字段。Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。
- 服务器根据这个值,决定是否同意这次请求。CORS 支持所有类型的 HTTP 请求
如果服务器同意,响应结果头:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
Cookie, withCredentials 属性
step 1:
CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,服务器需指定 Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
step 2:
客户端方面,在 AJAX 请求中打开 withCredentials
属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。
但是,如果省略 withCredentials
设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭 withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果要发送 Cookie,
Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的 Cookie。
// CORS 参考资料: http://www.ruanyifeng.com/blog/2016/04/cors.html
// url(必选),options(可选)
// 实现了 CORS 通信
let myHeaders = new Headers({
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain',
});
fetch(url, {
method: 'GET',
headers: myHeaders,
mode: 'cors',
}).then((res) => {
// TODO
});
为什么 fetch、CORS 会实现跨域通信?
ajax 跨域请求默认会被浏览器拦截,fetch、CORS 在请求头加入了 origin,服务器端设置了 Access-Control-Allow-Origin
发在源是允许就返回数据。