盒子
盒子
文章目录
  1. 如何发请求
  2. AJAX出现
    1. 浏览器和服务器交互模式 V1.0
    2. 交互模式2.0
    3. XMLHttpRequest
    4. XMLHttpRequest实例的详解
    5. JSON
    6. CORS
    7. AJAX一些其他知识
      1. 获得请求和响应头
      2. 练习一下JQuery封装AJAX

AJAX的出现与跨域

XMLHttpRequest JSON AJAX CORS 四个名词来开会

如何发请求

在前端的世界里也逛荡了不少日子了,目前已经get到大约5种发起请求的方式,主流的、非主流的。

何种方式 请求方法
最常见的form表单 默认GET,多用POST,只此两种 会刷新页面或者新开页面
a 标签 GET请求 也会刷新页面或者新开页面
imgsrc属性 GET 只能以图片的形式展现
link标签 GET 只能以CSSfavicon的形式展现
script标签 GET 只能以脚本的形式运行

可是

  • 我们可能想用GET POST PUT DELETE 方法
  • 不想刷新整个页面,想用一种更易于理解的方式来响应

AJAX出现

浏览器和服务器交互模式 V1.0

AJAX未出现之前,浏览器想从服务器获得资源,注意是获取资源,会经过如下一个过程

  • 浏览器发起请求->服务器接到请求响应给你HTML文档->浏览器收到资源,刷新页面,加载获得的的HTML。简略的过程

我称这种交互方式是 V1.0,此时还是以获取资源为导向。后来随着时代的发展,人们日益增长的文化需求成为了社会的主要矛盾……有一天,小明看了一篇报道,他只是想在下面评论一下,发表对实事的亲切问候,问候完了,唉,你给我刷新页面干啥,我只是想评论一下啊。

什么鬼

大概那是网民们第一次对 良好的用户体验 提出了要求。后来的苹果爸爸,把大家惯坏了,天天嚷着 “你这产品用户体验太差了”……

彼时,微软还是对web做出了很大的贡献的。

交互模式2.0

大约1999年,微软发布IE 5.0版本,它允许JavaScript脚本向服务器发起HTTP请求。不过很遗憾,当时也没有火起来,直到2004年Gmail发布和2005年Google Map发布,才引起广泛重视。2005年,一个叫Jesse James Garrett的人提出了一个新术语—-AJAX,它是一系列技术的组合体,全称是 Asynchronous JavaScript + XML(异步的JS和XML)可以阻止页面整体刷新,只是动态响应用户的操作,快速显示到局部,用户就可以很愉快的继续上网了。

AJAX

可以看出IE当时还是很猛的,随着IE 6.0 市场份额进一步扩大,IE已经把火狐整的半死不活,放眼整个浏览器市场,微软是当之无愧的王者,后来微软就把浏览器团队解散了……不得不说这是一波神操作,能与之媲美的操作大概只有残血我能反杀 塔下我能秀他了。微软强行为后续各家浏览器的发展提供了优秀的工程师,尤其是08、09年出生的谷歌浏览器,再看如今的IE……

既然AJAX是一系列的技术的组合体,接下来认识一下其中的几位主角

XMLHttpRequest

XMLHttpRequest对象是用来在浏览器和服务器之间传输数据的。

古代的操作的是:

  1. 浏览器构造XMLHttpRequest实例化对象
  2. 用这个对象发起请求
  3. 服务器响应一个XML格式的字符串,是字符串,是字符串,是字符串,也就是说响应的第四部分是字符串。
  4. JS解析符合XML格式的字符串,更新局部页面。

什么是XML可扩展标记语言。

以上是最初的用法,用的是XML,前端代码片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let request = new XMLHttpRequest() //实例化XMLHttpRequest对象
request.onreadystatechange = () => {
if (request.readyState === 4) {
console.log('请求和响应都完毕了')

if (request.status >= 200 && request.status <= 300) {
console.log('说明请求成功了')
console.log(request.responseText)
let parser = new DOMParser()
let xmlDoc = parser.parseFromString(request.responseText, "text/xml")
//用parser解析request.responseText
let c = xmlDoc.getElementsByTagName('body')[0].textContent
console.log(c)
} else if (request.status >= 400) {
console.log('说明请求失败了')

}
}

}
request.open('GET', '/xxx') //配置request
request.send()

服务器端的对应代码片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
response.statusCode = 200
response.setHeader('Content-Type', 'text/xml;charset=utf-8')
response.write(`

<note>
<to>木木</to>
<from>少少</from>
<heading>你好哇</heading>
<body>好久不见啊</body>
</note>
`)
response.end()
...

本地模拟的话,一定要记得开俩不同的端口
例如:

1
2
node server.js 8001
node server.js 8002

XMLHttpRequest实例的详解

正如上面的前端代码片段写的一样,主要用到了open() send()方法, onreadystatechange readyState 属性。

  1. request.open(method, URL, async)方法。
    • 一般用三个参数,第一个参数是请求的方法,可以用GET POST DELETE PUT等等,URL是用访问的路径,async是是否使用同步,默认true,开启异步,不需要做修改即可,所以实际中只写前两个参数
  1. request.send()方法。
    • 发送请求. 如果该请求是异步模式(默认),该方法会立刻返回. 相反,如果请求是同步模式,则直到请求的响应完全接受以后,该方法才会返回
  2. readyState属性。
    • 描述请求的五个状态。
      • 0 === 常量 UNSENT(未打开) open()方法未调用
      • 1 === OPENED (未发送) 只是open()方法调用了
      • 2 === HEADERS_RECEIVED (已获取响应头) send()方法调用了,响应头和响应状态已经返回了
      • 3 === LOADING (正在下载响应体) 响应体下载中,responseText已经获取了部分数据
      • 4 === DONE (请求完成) 整个响应过程完毕了。 这个值是实际中用到的。
      • 只要不等于4,就表示请求还在进行中。
  3. responseText属性是此次响应的文本内容。
  4. onreadystatechange属性。
    • readyState属性的值发生改变,就会触发readyStateChange事件。
    • 我们可以通过onReadyStateChange属性,指定这个事件的回调函数,对不同状态进行不同处理。尤其是当状态变为4的时候,表示通信成功,这时回调函数就可以处理服务器传送回来的数据。即前面的代码片段的处理方式。
  5. 其他的方法、属性、事件详见阮一峰博客MDN文档

习惯用javaScript的前端是不想和XML打交道的,应该用一种符合js风格的数据格式语言。


JSON

后来一个美国程序员道格拉斯·克罗克福特发明了JSON,解决了上面的问题,这货还写了一本蝴蝶书JavaScript语言精粹,还发明了一个JS校验器 —-JSLint。

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

以上是JSON官网的简介,可以看出它是一门全新的语言,不是JavaScript的子集

  1. JSON很简单,数据类型和JS有点不同的地方。
JavaScript JSON
string “string” 必须写双引号
number number
object {“object”: “name”} 必须双引号
undefined 没有
null null
boolean 直接写true false
array array
function 没有
variable
  1. 浏览器的全局对象window上有JSON对象,直接使用window.JSON.parse(string)
1
2
let string = request.responseText
let json = window.JSON.parse(string) //string 要符合JSON的格式

以上是JSON解析部分的代码。

此时服务器端代码是

1
2
3
4
5
6
7
8
9
10
11
12
response.statusCode = 200
response.setHeader('Content-Type', 'text/json;charset=utf-8')
response.write(`
{
"note" : {
"to" : "木木",
"from" : "少少",
"heading" : "你好哇",
"content" : "好久不见啊"
}
}
`)
  1. 我们浏览器有同源政策,不是同协议 同域名 同端口 的网页无法相互访问。

4.AJAX恰好是同源政策的拥趸

CORS

  1. 如果AJAX向非同源的地址发起请求,会报错。
    • 这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200,也就是说即使你看到了200的正确码,也没有用
  2. 但是form表单无视同源政策,可以发起跨域请求。
1
2
3
4
5
<button id="myButton">点我</button>
<form action="https://www.baidu.com" method="get">
<input type="password" name="password">
<input type="submit" value="提交">
</form>

上述请求响应都没有问题
然而对于AJAX就不行

1
2
3
...
request.open('GET', 'http://www.baidu.com')
...

同源的保护

  • 这是为什么呢,因为

原页面用 form 提交到另一个域名之后,原页面的脚本无法获取新页面中的内容,所以浏览器认为这是安全的。
而 AJAX 是可以读取响应内容的,因此浏览器不能允许你这样做。如果你细心的话你会发现,其实请求已经发送出去了,你只是拿不到响应而已。
所以浏览器这个策略的本质是,一个域名的 JS ,在未经允许的情况下,不得读取另一个域名的内容。但浏览器并不阻止你向另一个域名发送请求。

作者:方应杭
链接:https://www.zhihu.com/question/31592553/answer/190789780
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


那么如何让AJAX跨域发起请求呢。
答案是CORS

  1. CORS目前是W3C的标准,它允许浏览器跨域发起XMLHttpRequest请求,而且可以发起多种请求,不像JSONP只能发起GET请求,全称是”跨域/源资源共享”(Cross-origin resource sharing)。
1
request.open('GET', 'http://wushao.com:8001/xxx') //配置request
  • 服务器端的代码需要做如下处理
1
response.setHeader('Access-Control-Allow-Origin', 'http://shaolin.com:8002')

一定要注意是谁去访问谁,8001去访问8002,那么8001的前端代码要告诉8002的后端代码,咱们是一家人,你和浏览器说说别让它禁我了。

AJAX一些其他知识

既然可以发请求,那么请求头的四部分如何获得的,响应的四部分又是如何获得呢

获得请求和响应头
  1. 获得请求头的方法
1
2
3
4
5
request.open('GET', 'http://shaolin.com:8002/xxx')// 请求的第一部分
request.setRequestHeader('Content-Type', 'x-www-form-urlencoded')//请求的第二部分
request.setRequestHeader('wushao', '18') //请求的第二部分
request.send('我要设置请求的第四部分') //请求的第四部分
request.send('name=wushao&password=wushao') //请求的第四部分

对应的典型的http请求四部分

1
2
3
4
5
6
GET /xxx HTTP/1.1
HOST: http://shaolin.com:8002
Content-Type: x-www-form-urlencoded
wushao: 18

name=wushao&password=wushao
  1. 获得响应的方法
1
2
3
4
5
request.status //响应的第一部分 200
request.statusText //响应的第一部分 OK
request.getAllResponseHeaders //响应的第二部分,这个方法好啊,全部的响应头
request.getResponseHeader('Content-Type') //响应的第二部分具体的
request.responseText //响应的第四部分

对应的典型的http响应的四部分

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Content-Type: text/json;charset=utf-8

{
"note" : {
"to" : "木木",
"from" : "少少",
"heading" : "你好哇",
"content" : "好久不见啊"
}
}

回顾一下各个status对应的意思

1
2
3
4
5
6
7
8
9
100
200 === OK,请求成功
301 === 被请求的资源已永久移动到新位置
302 === 请求临时重定向,要求客户端执行临时重定向
304 === 和上次请求一样,未改变
403 === 服务器已经理解请求,但是拒绝访问
404 === 请求失败,服务器上没有这个资源
502 === 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
503 === Service Unavailable 由于临时的服务器维护或者过载,服务器当前无法处理请求。
练习一下JQuery封装AJAX
  1. 初级的jq封装
    这是一个很简陋的效果,首先我还是把jq假设的很简单,就是一个window的属性,请轻喷……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
window.jQuery = function (nodeOrSelector) {
let nodes = {}
nodes.addClass = function () {}
nodes.html = function () {}
return nodes
}

window.jQuery.ajax = function (options) {
let url = options.url
let method = options.method
let headers = options.headers
let body = options.body
let successFn = options.successFn
let failFn = options.failFn

let request = new XMLHttpRequest() //实例化XMLHttpRequest对象
request.open(method, url)
for (let key in headers) {
let value = headers[key]
request.setRequestHeader(key, value)
}
request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status >= 200 && request.status <= 300) {
successFn.call(undefined, request.responseText)
} else if (request.status >= 400) {
failFn.call(undefined, request)
}
}
}
request.send(body)
}

以上就是jq对ajax的简陋的封装,ajax()方法接受一个对象作为参数,这个对象有很多键。这些键就是http请求的头的各个部分,以及一个成功函数和一个失败函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
myButton.addEventListener('click', (e) => {

window.jQuery.ajax ({
url: '/xxx',
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'wushao': '18'
},
body: 'a=1&b=6',
successFn: (x) => {
...
},
failFn: (x) => {
...
}
})
})

以上就是简化后的使用方法,给button绑定事件的时候,函数体直接就是ajax()

  1. 目前你会发现options这个对象傻傻的,因为总有一些用户不希望只传一个参数。所以我们稍微改造一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
let url
if (arguments.length === 1) {
url = options.url
} else if (arguments.length === 2) {
url = arguments[0]
options = arguments[1]
}

let method = options.method
let headers = options.headers
let body = options.body
let successFn = options.successFn
let failFn = options.failFn

加了一点,判断ajax()的参数个数。

  1. 一千个人有一千零一个成功或失败函数的写法,所以为了维护世界和平,大家约定俗成了一套理论 Promise then( )
1
2
3
4
5
6
7
8
//Promise这个对象呢,大概长这个样子,真实面目我是没见过
//简单的写一下promise
window.Promise = function (fn) {
//...一些其他代码
return {
then: function () {}
}
}

Promise这个构造函数呢,又会返回一个函数,这个返回的函数一个then属性,value又是一个函数。处处都体现着函数是第一公民的地位!!!
那我们可以利用这个强大的Promise对象搞一些事情了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//第一步的代码改造成这样,第一步用到了ES6的解构赋值法
window.jQuery.ajax = function ({url, method, body, headers}) {
return new Promise(function (resolve, reject) {
let request = new XMLHttpRequest()
request.open(method, url)

for(let key in headers) {
let value = headers[key]
request.setRequestHeader(key, value)
}

request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status >= 200 && request.status <= 300) {
resolve.call(undefined, request.responseText)
} else if (request.status >= 400) {
reject.call(undefined, request)
}
}
}
request.send(body)
})
}

关于解构赋值:ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)
详见ES6解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//经过上面这么一折腾,可以很简单的使用了
myButton.addEventListener('click', (e) => {
let promise = window.jQuery.ajax({
url: '/xxx',
method: 'get',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'wushao': '18'
}
})

promise.then(
(responseText) => {
console.log(responseText)
},
(request) => {
console.log(request)
}
)
})

注意then可以传入两个函数,第一个函数表示成功了执行这个,第二个函数表示失败了执行这个,而且可以进行链式调用,一直点下去。

  1. 所以实际上jq的写法大多是这么写的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
myButton.addEventListener('click', (e) => {
$.ajax({
url: '/xxx',
type: 'GET',
}).then(
(responseText) => {
console.log(responseText)
return responseText
},
(request) => {
console.log('error')
return '已经处理'
}
).then(
(responseText) => {
console.log(responseText)
},
(request) => {
console.log(error2)
}
)

})

链式调用的意思就是:成功函数成功了,就执行第二个then的第一个函数;成功函数失败了,就执行第二个then的第二个函数。

完整代码详见我的gitHub