传至基本模块(url,允诺形式)
允诺模块、允诺模块类别
增设允诺头
极度处置
随身携带cookie增设
布吕马允诺
二、后端展开互联网允诺的形式form配置文件、ifream、创下网页
Ajax – 触发器互联网允诺的集大成者
jQuery – 两个黄金时代
fetch – Ajax的继任者
axios、request等为数众多开放源码库
三、有关互联网允诺的疑点Ajax的再次出现化解了甚么难题
原生植物Ajax怎样采用
jQuery的互联网允诺方式
fetch的用语和坑点
怎样正确的采用fetch
怎样选择合适的布吕马形式
四、Ajax 的再次出现化解了甚么难题在Ajax再次出现之前,web程序是这样工作的:
这种交互的的缺陷是显而易见的,任何和服务器的交互都需要创下网页,用户体验非常差,Ajax的再次出现化解了这个难题。Ajax全称Asynchronous JavaScript + XML(触发器JavaScript和XML)。
采用Ajax,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(创下)整个网页。
Ajax本身不是一种新技术,而是用来描述一种采用现有技术集合实现的两个技术方案,浏览器的XMLHttpRequest是实现Ajax最重要的对象(IE6以下采用ActiveXObject)。
尽管X在Ajax中代表XML, 但由于JSON的许多优势,比如更加轻量和作为Javascript的一部分,目前JSON的采用比XML更加普遍。
五、原生植物 Ajax 的用语这里主要分析XMLHttpRequest对象,下面是它的一段基础采用:
var xhr = new XMLHttpRequest();
xhr.open(post,www.xxx.com,true)
// 接收返回值
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 ){
if(xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.responseText);
}
}
}
// 处置允诺模块
postData = {“name1″:”value1″,”name2″:”value2”};
postData = (function(value){
var dataString = “”;
for(var key in value){
dataString += key+”=”+value[key]+”&”;
};
return dataString;
}(postData));
// 增设允诺头
xhr.setRequestHeader(“Content-type”,”application/x-www-form-urlencoded”);
// 极度处置
xhr.onerror = function() {
console.log(Network request failed)
}
// 布吕马随身携带 cookie
xhr.withCredentials = true;
// 发出允诺
xhr.send(postData);
下面分别对XMLHttpRequest对象常用的的函数、属性、事件展开分析。
用于初始化两个允诺,用语:
xhr.open(method, url, async);
method:允诺形式,如get、post
url:允诺的url
async:是否为触发器允诺
send用于发送HTTP允诺,即调用该方法后HTTP允诺才会被真正发出,用语:
xhr.send(param)
param:http 允诺的模块,可以为string、Blob等类别。
abort用于终止两个ajax允诺,调用此方法后readyState将被增设为0,用语:
xhr.abort()
setRequestHeader用于增设HTTP允诺头,此方法必须在open()方法和send()之间调用,用语:
xhr.setRequestHeader(header, value);
getResponseHeadervar header = xhr.getResponseHeader(name);
属性readyState用来标识当前XMLHttpRequest对象所处的状态,XMLHttpRequest对象总是位于下列状态中的两个:
表示http允诺的状态, 初始值为0。如果服务器没有显式地指定状态码, 那么status将被增设为默认值, 即200。
responseType表示响应的数据类别,并允许我们手动增设,如果为空,默认为text类别,可以有下面的取值:
返回响应的正文,返回的类别由上面的responseType决定。
withCredentialsajax允诺默认会随身携带同源允诺的cookie,而布吕马允诺则不会随身携带cookie,增设xhr的withCredentials的属性为true将允许随身携带布吕马cookie。
事件回调onreadystatechange xhr.onreadystatechange = callback;
当readyState属性发生变化时,callback 会被触发。
onloadstart xhr.onloadstart = callback;
在ajax允诺发送之前(readyState==1后, readyState==2前),callback会被触发。
onprogress xhr.onprogress = function(event){
console.log(event.loaded / event.total);
}
回调函数可
onload xhr.onload = callback;
当两个资源及其依赖资源已完成加载时,将触发callback,通常我们会在onload事件中处置返回值。
极度处置onerror xhr.onerror = callback;
当ajax资源加载失败时会触发callback。
ontimeout xhr.ontimeout = callback;
当进度由于预定时间到期而终止时,会触发callback,超时时间可采用timeout属性展开增设。
六、jQuery 对 Ajax 的封装在很长一段时间里,人们采用jQuery提供的ajax封装展开互联网允诺,包括$.ajax、$.get、$.post等,这几个方法放到现在,我依然觉得很实用。
$.ajax({
dataType: json, // 增设返回值类别
contentType: application/json, // 增设模块类别
headers: {Content-Type,application/json},// 增设允诺头
xhrFields: { withCredentials: true }, // 布吕马随身携带 cookie
data: JSON.stringify({a: [{b:1, a:1}]}), // 传递模块
error:function(xhr,status){ // 错误处置
console.log(xhr,status);
},
console.log(data,status);
}
})
$.ajax只接收两个模块,这个模块接收一系列配置,其自己封装了两个jqXHR对象,有兴趣可以阅读一下 jQuary-ajax 源码:
https://github.com/jquery/jquery/blob/master/src/ajax.js
常用配置:
url当前页地址。发送允诺的地址。
type类别:String 允诺形式 (“POST” 或”GET”), 默认为 “GET”。注意:其它HTTP允诺方法,如PUT和 DELETE也可以采用,但仅部分浏览器支持。
timeout类别:Number增设允诺超时时间(毫秒)。此增设将覆盖全局增设。
success类别:Function 允诺成功后的回调函数。
jsonp在两个jsonp允诺中重写回调函数的名字。这个值用来替代在”callback=?”这种GET或POST允诺中URL模块里的”callback”部分。
error类别:Function ,允诺失败时调用此函数。
注意:源码里对错误的判定:
isSuccess = status >= 200 && status < 300 || status === 304;
返回值除了这几个状态码都会进error回调。
dataType “xml”: 返回 XML 文档,可用 jQuery 处置。
“html”: 返回纯文本 HTML 信息;包含的 script 标签会在插入 dom 时执行。
“script”: 返回纯文本 JavaScript 代码。不会自动缓存结果。除非增设了 “cache” 模块。注意:在远程允诺时 (不在同两个域下),所有 POST 允诺都将转为 GET 允诺。(因为将采用 DOM 的 script 标签来加载)
“json”: 返回 JSON 数据 。
“jsonp”: JSONP 格式。采用 JSONP 形式调用函数时,如 “myurl?callback=?” jQuery 将自动替换 ? 为正确的函数名,以执行回调函数。
“text”: 返回纯文本字符串
data类别:String 采用JSON.stringify转码。
complete类别:Function允诺完成后回调函数 (允诺成功或失败之后均调用)。
async类别:Boolean 默认值:true。默认增设下,所有允诺均为触发器允诺。如果需要发送同步允诺,请将此选项增设为 false。
contentType类别:String默认值: “application/x-www-form-urlencoded”。发送信息至服务器时内容编码类别。
键值对这样组织在一般的情况下是没有甚么难题的,这里说的一般是,不带嵌套类别JSON,也就是 简单的JSON,形如这样:
{
a: 1,
b: 2,
c: 3
}
但是在一些复杂的情况下就有难题了。 例如在 Ajax中你要传两个复杂的 json 对像,也就说是对象嵌数组,数组中包括对象,你这样传:application/x-www-form-urlencoded 这种形式是没有办法将复杂的JSON组织成键值对形式。
{
data: {
a: [{
x: 2
}]
}
}
可以用如下形式传递复杂的json对象:
$.ajax({
dataType: json,
contentType: application/json,
data: JSON.stringify({a: [{b:1, a:1}]})
})
七、jQuery 的继任者近年来后端MV*的发展壮大,人们越来越少的采用jQuery,我们不可能单独为了采用jQuery的Ajax api来单独引入他,无可避免的,我们需要寻找新的技术方案。
尤雨溪在他的文档中推荐大家用axios展开互联网允诺。axios基于Promise对原生植物的XHR展开了非常全面的封装,采用形式也非常的优雅。另外,axios同样提供了在node环境下的支持,可谓是互联网允诺的首选方案。
Fetch API是两个用用于访问和操纵 HTTP 管道的强大的原生植物 API。
这种功能以前是采用 XMLHttpRequest 实现的。Fetch 提供了两个更好的替代方法,可以很容易地被其他技术采用,例如 Service Workers。Fetch 还提供了单个逻辑位置来定义其他 HTTP 相关概念,例如 CORS 和 HTTP 的扩展。
可见fetch是作为XMLHttpRequest的替代品再次出现的。
采用fetch,你不需要再额外加载两个外部资源。但它还没有被浏览器完全支持,所以你仍然需要两个polyfill。
八、fetch 的采用两个基本的 fetch 允诺:
const options = {
method: “POST”, // 允诺模块
headers: { “Content-Type”: “application/json”}, // 增设允诺头
body: JSON.stringify({name:123}), // 允诺模块
credentials: “same-origin”, // cookie 增设
mode: “cors”, // 布吕马
}
fetch(http://www.xxx.com)
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson); // 响应数据
})
.catch(function(err){
console.log(err); // 极度处置
})
Fetch API提供了两个全局的fetch()方法,和几个辅助对象来发起两个互联网允诺。
fetc
Headers可以通过Headers()构造函数来创建两个你自己的headers对象,相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。
var myHeaders = new Headers();
myHeaders.append(“Content-Type”, “text/plain”);
Request通过Request()构造函数可以创建两个Request对象,这个对象可以作为fetch函数的第二个模块。
Response在fetch()处置完promises之后返回两个Response实例,也可以手动创建两个Response实例。
九、fetch polyfill 源码分析由于fetch是两个非常底层的API,所以我们无法进一步的探究它的底层,但是我们可以借助它的polyfill探究它的基本原理,并找出其中的坑点。
代码结构由代码可见,polyfill主要对Fetch API 提供的四大对象展开了封装:
fetch 封装代码非常清晰:
构造两个Promise对象并返回;
创建两个Request对象;
创建两个XMLHttpRequest对象;
取出Request对象中的允诺url,允诺方发,open两个xhr允诺,并将Request对象中存储的headers取出赋给 xhr;
xhr onload后取出response的status、headers、body封装Response对象,调用resolve。
极度处置可以发现,调用reject有三种可能:
允诺超时
允诺失败
注意:当和服务器建立简介,并收到服务器的极度状态码如404、500等并不能触发onerror。当互联网故障时或允诺被阻止时,才会标记为 reject,如布吕马、url不存在,互联网极度等会触发onerror。
所以采用 fetch 当接收到极度状态码都是会进入 then 而不是 catch。这些错误允诺往往要手动处置。
手动终止
可以在request模块中传至signal对象,并对signal对象添加abort事件监听,当xhr.readyState变为4(响应内容解析完成)后将 signal 对象的 abort 事件监听移除掉。
这表示,在两个fetch允诺结束之前可以调用signal.abort将其终止。在浏览器中可以采用AbortController()构造函数创建两个控制器,然后采用AbortController.signal属性。
这是两个实验中的功能,此功能某些浏览器尚在开发中。
Headers 封装在 header 对象中维护了两个map对象,构造函数中可以传至Header对象、数组、普通对象类别的header,并将所有的值维护到map中。
之前在fetch函数中看到调用了header的forEach方法,下面是它的实现:
可见header的遍历即其内部map的遍历。
另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象展开操作。
Request 对象Request对象接收的两个模块即fetch函数接收的两个模块,第两个模块可以直接传递url,也可以传递两个构造好的request对象。第二个模块即控制不同配置的option对象。
可以传至credentials、headers、method、mode、signal、referrer等属性。
这里注意:传至的headers被当作Headers构造函数的模块来构造 header 对象。
cookie 处置fetch 函数中还有如下的代码:
if (request.credentials === include) {
xhr.withCredentials = true
} else if (request.credentials === omit) {
xhr.withCredentials = false
}
默认的credentials类别为same-origin, 即可随身携带同源允诺的 coodkie。
然后我发现这里 polyfill 的实现和 MDN- 采用 Fetch(https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch) 和很多资料是不一致的:
mdn: 默认情况下,fetch 不会从服务端发送或接收任何 cookies
于是我分别实验了下采用polyfill和采用原生植物fetch随身携带 cookie 的情况,发现在不增设credentials的情况下居然都是默认随身携带同源cookie的,这和文档的说明说不一致的,查阅了许多资料后都是说fetch默认不会随身携带 cookie,下面是采用原生植物fetch在浏览器展开允诺的情况:
然后我发现在 MDN-Fetch-Request(https://developer.mozilla.org/zh-CN/docs/Web/API/Request/credentials) 已经指出新版浏览器credentials默认值已更改为same-origin,旧版依然是omit。
确实 MDN- 使用 Fetch 这里的文档更新的有些不及时,误人子弟了:
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch
Response 对象Response对象是fetch调用成功后的返回值:
回顾下fetch中对Response`的操作:
xhr.onload = function () {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || )
}
options.url = responseURL in xhr ? xhr.responseURL : options.headers.get(X-Request-URL)
var body = response in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options))
}
Response构造函数:
可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处置并挂载到Response对象上。
构造函数里面并没有对responseText的明确处置,最后交给了_initBody函数处置,而Response并没有主动声明_initBody属性,代码最后采用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看_initBody函数:
可见,_initBody函数根据xhr.response的类别(Blob、FormData、String…),为不同的模块展开赋值,这些模块在Body方法中得到不同的应用,下面具体看看Body函数还做了哪些其他的操作:
Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将 _initBody 中得到的不同类别的返回值返回。
这里还有一点需要说明:几个函数中都有类似下面的逻辑:
var rejected = consumed(this)
if (rejected) {
return rejected
}
consumed 函数:
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError(Already read))
}
body.bodyUsed = true
}
每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError(Already read)。这也遵循了原生植物fetch的原则:
因为 Responses 对象被增设为了 stream 的形式,所以它们只能被读取一次。
十、fetch 的坑点VUE的文档中对fetch有下面的描述:
采用fetch还有很多别的注意事项,这也是为甚么大家现阶段还是更喜欢axios 多一些。当然这个事情在未来可能会发生改变。
由于fetch是两个非常底层的API,它并没有被展开很多封装,还有许多难题需要处置:
不能直接传递JavaScript对象作为模块;
无法正常的捕获极度;
老版浏览器不会默认随身携带cookie;
不支持jsonp。
十一、对 fetch 的封装 允诺模块处置支持传至不同的模块类别:
function stringify(url, data) {
var dataString = url.indexOf(?) == -1 ? ? : &;
for (var key in data) {
dataString += key + = + data[key] + &;
};
return dataString;
}
if (request.formData) {
request.body = request.data;
} else if (/^get$/i.test(request.method)) {
request.url = `${request.url}${stringify(request.url, request.data)}`;
} else if (request.form) {
request.headers.set(Content-Type, application/x-www-form-urlencoded;charset=UTF-8);
request.body = stringify(request.data);
} else {
request.headers.set(Content-Type, application/json;charset=UTF-8);
request.body = JSON.stringify(request.data);
}
cookie 随身携带fetch在新版浏览器已经开始默认随身携带同源cookie,但在老版浏览器中不会默认随身携带,我们需要对他展开统一增设:
request.credentials = same-origin; // 同源随身携带
request.credentials = include; // 可布吕马随身携带
极度处置当接收到两个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性增设为 false ),仅当互联网故障时或允诺被阻止时,才会标记为 reject。
因此我们要对fetch的极度展开统一处置:
.then(response => {
if (response.ok) {
return Promise.resolve(response);
}else{
const error = new Error(`允诺失败! 状态码: ${response.status}, 失败信息: ${response.statusText}`);
error.response = response;
return Promise.reject(error);
}
});
返回值处置对不同的返回值
.then(response => {
let contentType = response.headers.get(content-type);
if (contentType.includes(application/json)) {
return response.json();
} else {
return response.text();
}
});
jsonpfetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的化解布吕马的形式,推荐采用cors或者nginx化解布吕马,具体请看下面的章节。
fetch 封装好了,可以愉快的采用了。
嗯,axios 真好用。
十二、布吕马总结谈到互联网允诺,就不得不提布吕马。
浏览器的同源策略限制了从同两个源加载的文档或脚本怎样与来自另两个源的资源展开交互。这是两个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。
布吕马条件:协议,域名,端口,有两个不同就算布吕马。
下面是化解布吕马的几种形式:
nginx采用nginx反向代理实现布吕马,参考我这篇文章:后端开发者必备的 nginx 知识。
corsCORS是两个W3C标准,全称是”布吕马资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求。
服务端增设Access-Control-Allow-Origin就可以开启CORS。 该属性表示哪些域名可以访问资源,如果增设通配符则表示所有网站都可以访问资源。
app.all(*, function (req, res, next) {
res.header(“Access-Control-Allow-Origin”, “*”);
res.header(“Access-Control-Allow-Headers”, “X-Requested-With”);
res.header(“Access-Control-Allow-Methods”, “PUT,POST,GET,DELETE,OPTIONS”);
next();
});
jsonpscript标签的src属性中的链接可以访问布吕马的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中展开了调用,这样实现了布吕马。
jquery对jsonp的支持:
$.ajax({
type : “get”,
url : “http://xxxx”
dataType: “jsonp”,
jsonp:”callback”,
jsonpCallback: “doo”,
success : function(data) {
console.log(data);
}
});
fetch、axios等并没有直接提供对jsonp的支持,如果需要采用这种形式,我们可以尝试展开手动封装:
(function (window,document) {
“use strict”;
var jsonp = function (url,data,callback) {
// 1. 将传至的 data 数据转化为 url 字符串形式
// {id:1,name:jack} => id=1&name=jack
var dataString = url.indexof(?) == -1? ?: &;
for(var key in data){
dataString += key + = + data[key] + &;
};
// 2 处置 url 中的回调函数
// cbFuncName 回调函数的名字 :my_json_cb_ 名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = my_json_cb_ + Math.random().toString().replace(.,);
dataString += callback= + cbFuncName;
// 3. 创建两个 script 标签并插入到网页中
var scriptEle = document.createElement(script);
scriptEle.src = url + dataString;
// 4. 挂载回调函数
window[cbFuncName] = function (data) {
callback(data);
// 处置完回调函数的数据之后,删除 jsonp 的 script 标签
document.body.removeChild(scriptEle);
}
document.body.appendChild(scriptEle);
}
window.$jsonp = jsonp;
})(window,document)
postMessage 布吕马postMessage()方法允许来自不同源的脚本采用触发器形式展开有限的通信,可以实现跨文本档、多窗口、布吕马消息传递。
// 捕获 iframe
var domain = http://scriptandstyle.com;
var iframe = document.getElementById(myIFrame).contentWindow;
// 发送消息
setInterval(function(){
var message = Hello! The time is: + (new Date().getTime());
console.log(blog.local: sending message: + message);
//send the message and target URI
iframe.postMessage(message,domain);
},6000);
// 响应事件
window.addEventListener(message,function(event) {
if(event.origin !== http://davidwalsh.name) return;
console.log(message received: + event.data,event);
event.source.postMessage(holla back youngin!,event.origin);
},false);
postMessage布吕马适用于以下场景:同浏览器多窗口间布吕马通信、iframe间布吕马通信。
WebSocketWebSocket 是一种双向通信协议,在建立连接之后,WebSocket的 server与 client都能主动向对方发送或接收数据而不受同源策略的限制。
function WebSocketTest(){
if (“WebSocket” in window){
alert(“您的浏览器支持 WebSocket!”);
// 打开两个 web socket
var ws = new WebSocket(“ws://localhost:3000/abcd”);
ws.onopen = function(){
// Web Socket 已连接上,采用 send() 方法发送数据
ws.send(“发送数据”);
alert(“数据发送中…”);
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert(“数据已接收…”);
};
ws.onclose = function(){
// 关闭 websocket
alert(“连接已关闭…”);
};
} else{
// 浏览器不支持 WebSocket
alert(“您的浏览器不支持 WebSocket!”);
}
}
文中如有错误,欢迎在评论区指正,谢谢阅读。
活动推荐
GMTC 全球大后端技术大会上,我们邀请到了来自 Google、BAT、美团、京东、滴滴、字节跳动等 60+ 一线技术专家与你共话后端那些事,涵盖小程序、Flutter、后端安全、工程化、性能优化等 20+ 热点技术,不可错过。欢迎点击“”了解详情。