Jacleklm's Blog

前后端交互&跨域

2019/10/31

前后端数据交互

Cookie 是一些数据, 存储于电脑上的文本文件中,只要客户端 cookie 开放且有数据,每一次请求都会自动添加到 http 报文中,后台可以实时接收观察获取这些 Cookie 。

Cookie 的作用就是用于解决 “如何记录客户端的用户信息”:

  • 当用户访问 web 页面时,他的名字可以记录在 cookie 中。
  • 在用户下一次访问该页面时,可以在 cookie 中读取用户访问记录。

利用 Session 对象

session 对象表示特定会话 session 的用户数据。

客户第一次访问支持 session 的 JSP 网页,服务器会创建一个 session 对象记录客户的信息。当客户访问同一网站的不同网页时,仍处于同一个 session 中。

request.getSession().setAttribute();
request.getSession().getAttribute();
只要浏览器不关闭,就能使用。所以用户访问网站整个生命都会用到的数据一般都用 session 来存储,比如用户名、登录状态之类的

Ajax

PS:相对有点老了,用 fetch 方便很多
Ajax = 异步 JavaScript 和 XML。是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术

XHR 对象

XMLHttpRequest(XHR) 对象是 Ajax 的基础,用于在后台与服务器交换数据。用 var 变量名 = new XMLHttpRequest(); 创建

XHR 的方法

如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的

  • open(method, url, async)方法。第一个参数是’GET’/‘POST’;第三个参数是一个布尔值,同步/异步
    eg. request.open('GET', '/api/categories',true);
  • send(string)方法。用于将请求及参数发送到服务器,有参数时仅用于 POST。对于一些没带参数的请求方法,可以写成 send(null)

获取响应

需获得来自服务器的响应,请使用 XMLHttpRequest 对象的 responseText 或 responseXML 属性。
eg. document.getElementById("myDiv").innerHTML=xmlhttp.responseText;

状态信息及监听

readyState属性存有 XMLHttpRequest 的状态信息,有以下几个属性值:

  • 0: (未初始化)XHR 对象已创建,open()未调用
  • 1: (载入)send()已调用,正在发送请求
  • 2: (载入完成)send()方法执行完成,已经收到全部响应的内容
  • 3: (交互)正在解析内容
  • 4: (完成)响应内容解析完成,可以在客户端使用了

status 属性,该属性有两个属性值:200,404
当 readyState 改变时,就会触发 onreadystatechange 事件;此外 XHR 对象还提供了其他监听事件:process 事件,load 事件(加载完成),error 事件,abort 事件(加载终止)。可以用 addEventListener 来监听。
一般 open()并监听(监听后执行异步代码)后还会 send(),这才是一个完整的流程

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//  普通版
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https:www.baidu.com', true)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
alert(xhr.responseText)
}
}
}
xhr.send()

// 上面的 onreadystatechange 部分也可以写成如下:
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};

// Promise版
const myAjax = function(url) {
const promise = new Promise(function(resolve, reject) {
const handler = () => {
if (this.readystate !== 4) return
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statustext))
}
}
const client = new XMLHttpRequest()
client.open('GET', url)
client.onreadystatechange = handle
client.send()
})
return promise
}

myAjax('/url')
.then(res => {
console.log(res)
})
.cathch(e => {
console.log(e)
})

XHR 请求的缓存问题的处理方案

由于浏览器的缓存机制,当我们使用 XMLHttpRequest 发出请求的时候,浏览器会将请求的地址与缓存中的地址进行比较,如果存在相同记录则根据不向服务器发出请求而直接返回与上一次请求相同内容。但我们有时候其实希望每次都请求而不读缓存,解决方法如下:

方法一:URL 加时间戳

在每次请求的 url 后面加上 ‘?’ or ‘&’ 再加当前时间的字符串或其他类似的不会重复的随机字符串,这样浏览器每次发出的是不同的 url,即会当做不同的请求来处理,而不会从缓存中读取

1
2
3
var oReq = new XMLHttpRequest();
oReq.open("GET", url + ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime());
oReq.send(null);

方法二:HTTP 的缓存策略

可以把 Cache-ontrol: no-cache + If-Modified-Since: 0。写成 XHR 代码就是

1
2
3
XMLHttpRequest.setRequestHeader("If-Modified-Since", "0");
XMLHttpRequest.setRequestHeader("Cache-ontrol", "no-cache");
XMLHttpRequest.send(null);

实现一个前端缓存模块

实现一个前端缓存模块,主要用于缓存 xhr 返回的结果,避免多余的网络请求浪费,要求:

  • 生命周期为一次页面打开
  • 如果有相同的请求同时并行发起,要求其中一个能挂起并且等待另外一个请求返回并读取该缓存

代码实现

参考了封装原生 ajax(缓存+xhr 单例),有所删改。还没完成

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export class XAjaxRequester {
constructor() {
this.cacheUrl = {} // 缓存
}
updataStatus(url, res) {
this.cacheUrl[url] = res
}
get(url, successCallBack) {
let _self = this
let xhr = new XMLHttpRequest()
// 发起请求但是没响应的时候标记为 1
_self.cacheUrl[url] = 1
if (!_self.cacheUrl[url]) {
// 没缓存,正常执行
xhr.open('get', url, true)
xhr.onreadystatechange = function() {
if (this.readyState === 4) {
if (
(this.status >= 200 && this.status < 300) ||
this.status === 304
) {
let res = this.responseText
if (typeof successCallBack === 'function') {
successCallBack(res)
_self.updataStatus(url, res) // 更新缓存,发布消息给订阅者
}
}
}
}
xhr.send(null)
} else if (_self.cacheUrl[url] === 1) {
// 有url,但还没缓存资源。整一个发布订阅?

} else {
// 有缓存,直接拿
data = _self.cacheUrl[url]
successCallBack(data)
}
}
}

// 使用demo
let cacheAjax = new XAjaxRequester()
cacheAjax.get('/url', function(res) {
/* 成功回调 */
})
cacheAjax.get('/url', function(res) {
/* 成功回调 */
})

杂的知识点

在ajax应用中,通常一个页面要同时发送多个请求,如果只有一个XMLHttpRequest对象,前面的请求还未完成,后面的就会把前面的覆盖 掉,如果每次都创建一个新的XMLHttpRequest对象,也会造成浪费。解决的办法就是创建一个XMLHttpRequset的对象池,如果池里有 空闲的对象,则使用此对象,否则将创建一个新的对象。见这篇远古贴。也有另一种方案是每次用完之后就delete这个变量再让变量 = null

axios

axios 是一个基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生 XHR 的封装,只不过它是 Promise 的实现版本,符合最新的 ES 规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
.then(function(response) {
console.log(response)
})
.catch(function(error) {
console.log(error)
})

fetch

fetch 能实现以前 XHR 对象能做的事情。还提供了 全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。但是 fetch不是 ajax 的进一步封装,而是原生 js,没有使用 XMLHttpRequest 对象
简单的例子:

1
2
3
4
5
6
7
fetch('http://example.com/movies.json')
.then(function(response) {
return response.json()
})
.then(function(myJson) {
console.log(myJson)
})

fetch(url, init)

参数

该方法第一个参数是 url/一个 Request 对象,第二个参数(可选)是是用来自定义请求信息的init 对象,该对象有 method,headers(可以是一个 Headers 对象),body(可以是一个 Body 对象),cache 等可选的属性。

返回值

一个 Promise,resolve 时回传 Response 对象
例外:
AbortError:请求 abort 的情况
TypeError:请求的 url 有证书问题
注意,最好使用符合内容安全策略 (CSP)的链接而不是使用直接指向资源地址的方式来进行 Fetch 的请求

Headers 对象

可以通过 Headers() 构造函数来创建一个你自己的 headers 对象

1
2
3
4
5
var myHeaders = new Headers({
'Content-Type': 'text/plain',
'Content-Length': content.length.toString(),
'X-Custom-Header': 'ProcessThisImmediately'
})

Headers.prototype.has(key 名)检测是否含有某个 key 名,返回布尔值;Header.prototype.get(key 名)/getAll(key 名)获取 key 值

Response 对象

Response 实例是在 fetch() 处理完 promises 之后返回的
PS:其实 Headers 和 Response 的 new 下都能接受两个参数,第一个参数是数据体,第二个参数(可选)是 init 对象

常见属性

  • Response.status:返回的状态码
  • Response.statusText:返回的状态信息。eg,200 的时候是 OK
  • Response.ok:检查 response 的状态是否在 200-299(包括 200,299)这个范围内.该属性返回一个 Boolean 值。eg.
1
2
3
4
5
.then(function(response) {
if(response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');

Body

请求和响应都能有一个 body 属性包含一个 Body 对象。body 也可以是很多种数据类型,eg. FormData

FormData 提供了一种表示表单数据键值对的构造方式。有很多简单实用的方法。见下面例题

Body 的方法

这些方法都能被 Request 和 Response 使用,用来转变数据类型

  • Body.json()。接收一个 Response 流,并将其读取完成。它返回一个 Promise,Promise 的解析 resolve 结果是将文本体解析为 JSON
  • Body.formData()。将 Response 对象中的所承载的数据流读取并封装成为一个对象,该方法将返回一个 Promise 对象,该对象将产生一个 FormData 对象

兼容性检测

可以通过检测 Headers, Request, Response 或 fetch()是否在 Window 或 Worker 域中。例如

1
2
3
4
5
if (self.fetch) {
// run my fetch request here
} else {
// do something with XMLHttpRequest?
}

例题

实现一个提交文件不刷新页面的方案,给出关键代码
MDN有更多实用例子(eg. 发送带凭据的请求,上传 JSON 数据,上传文件/多个文件,检测请求是否成功,自定义请求对象)

1
2
<input type="file" id="upload">
<button id="btn"> upload </button>
1
2
3
4
5
6
7
8
9
10
11
12
13
document.getElementById('upload-btn').onclick = funcion() {
var formData = new FormData();
var fileField = document.querySelector("input[type='file']");
formData.append('file', fileField.files[0]);

fetch('https://example.com/profile/avatar', {
method: 'POST',
body: formData
})
.then(response => response.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));
}

WebSocket

Websocket 是一个全新的、独立的协议,基于 TCP 协议,与 http 协议兼容、却不会融入 http 协议。
由于HTTP通信只能由客户端发起,举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。要解决这种场景只能轮询,但是很浪费资源。
所以Websocket设计出来的目的就是要取代轮询(每隔一段时间浏览器对服务器发起请求,服务器返回新数据)和 Comet 技术。
WebSocket在单个 TCP 连接上进行全双工通讯的协议。WebSocket API 可在用户的浏览器和服务器之间进行双向通信。用户可以向服务器发送消息并接收事件驱动的响应,而无需轮询服务器。 它可以让多个用户连接到同一个实时服务器,并通过 API 进行通信并立即获得响应。
它的最大特点就是,【服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种

允许用户和服务器之间的流连接,并允许即时信息交换。在聊天应用程序的示例中,通过套接字汇集消息,可以实时与一个或多个用户交换,具体取决于谁在服务器上“监听”(连接)。
WebSockets 适用于需要实时更新和即时信息交换的任何应用程序。一些示例包括但不限于:现场体育更新,股票行情,多人游戏,聊天应用,社交媒体等

1
2
3
4
5
6
var socket=new WebSocket("/url");
socket.send("hello world"); // send() 方法来向服务器发送数据
socket.onmessage=function(event){ // onmessage 事件来接收服务器返回的数据
console.log(event.data);
console.log(event.readyState);
}

详见MDN及阮一峰

跨域

同源策略:当一个资源从与该资源本身所在服务器中不同域、协议、端口请求一个资源时,就是跨域。出于安全原因,浏览器限制从脚本内发起的跨域 HTTP 请求。XMLHttpRequest 和 Fetch 的请求都会失败(response.status = 0)
引入这个同源策略主要是用来防止 CSRF 攻击(没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然同源策略并不能完全阻止 CSRF))

解决跨域方式如下:

JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。
通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。此时使服务器返回的是一段代码就可以了,例如下面的代码服务端可以返回一段 jsonp('I am data') 去调用本来就写在<script>中的函数

1
2
3
4
5
6
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>

优缺点

优点是操作方便,兼容性好
缺点:

  • 只限于 get 请求
  • url 限制了参数大小
  • 错误处理机制不好,调用失败的时候不会返回各种 HTTP 状态码
  • 安全性。假如提供 jsonp 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的,结果是所有调用这个 jsonp 的网站都会存在漏洞,于是无法把危险控制在用一个域名下

回调函数名同名的解决方案

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function jsonp(url, jsonpCallbackName, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallbackName] = function(data) {
success && success(data)
}
document.body.appendChild(script)
}
jsonp('', 'callback', function(value) {
console.log(value)
})

CORS

  • CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和预检请求

简单请求

有一些条件,eg. method 是 GET, HEAD, POST。详情见 MDN
简单请求的时候,服务看请求的 Orgin 字段(请求网站的 URL),看自己的 Access-Control-Allow-Origin 字段(值为*,表示该资源可以被任意外域访问,也可以是某个具体域名),比较两者是否相符决定定要不要给资源了

预检请求

另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务器是否允许该跨域请求

服务器确认允许之后,才发起实际的 HTTP 请求。 在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 cookie 和 HTTP 认证相关数据)

如果发起请求时设置 WithCredentials 标志设置为 true,从而向服务器发送 cookie,但是如果服务器的响应中未携带Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者

附带身份凭证的请求

Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证
服务器不得设置 Access-Control-Allow-Origin 的值为*,必须是某个具体的域名。
注意,简单 GET 请求不会被预检;如果此类带有身份凭证请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

1
2
3
4
5
6
7
8
9
10
// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://test.com') {
console.log('验证通过')
}
})

参考资料
W3School
MDN
呓语
poetries
JSONP 原理及使用

CATALOG
  1. 1. 前后端数据交互
    1. 1.1. 利用 Cookie
    2. 1.2. 利用 Session 对象
    3. 1.3. Ajax
      1. 1.3.1. XHR 对象
        1. 1.3.1.1. XHR 的方法
        2. 1.3.1.2. 获取响应
        3. 1.3.1.3. 状态信息及监听
      2. 1.3.2. XHR 请求的缓存问题的处理方案
        1. 1.3.2.1. 方法一:URL 加时间戳
        2. 1.3.2.2. 方法二:HTTP 的缓存策略
      3. 1.3.3. 实现一个前端缓存模块
        1. 1.3.3.1. 代码实现
      4. 1.3.4. 杂的知识点
    4. 1.4. axios
    5. 1.5. fetch
      1. 1.5.1. fetch(url, init)
        1. 1.5.1.1. 参数
        2. 1.5.1.2. 返回值
      2. 1.5.2. Headers 对象
      3. 1.5.3. Response 对象
        1. 1.5.3.1. 常见属性
      4. 1.5.4. Body
        1. 1.5.4.1. Body 的方法
      5. 1.5.5. 兼容性检测
      6. 1.5.6. 例题
    6. 1.6. WebSocket
  2. 2. 跨域
    1. 2.1. JSONP
      1. 2.1.1. 优缺点
      2. 2.1.2. 回调函数名同名的解决方案
    2. 2.2. CORS
      1. 2.2.1. 简单请求
      2. 2.2.2. 预检请求
      3. 2.2.3. 附带身份凭证的请求
    3. 2.3. document.domain
    4. 2.4. postMessage