# 网络请求

2005 年,Jesse James Garrett 撰写了一篇文章,“Ajax—A New Approach to Web Applications”。这篇文章中描绘了一个被他称作 Ajax(Asynchronous JavaScript+XML,即异步 JavaScript 加 XML)的技术。这个技术涉及发送服务器请求额外数据而不刷新页面,从而实现更好的用户体验。

把 Ajax 推到历史舞台上的关键技术是 XMLHttpRequest(XHR)对象。这个对象最早由微软发明,然后被其他浏览器所借鉴。

XHR 为发送服务器请求和获取响应提供了合理的接口。这个接口可以实现异步从服务器获取额外数据,意味着用户点击不用页面刷新也可以获取数据。通过 XHR 对象获取数据后,可以使用 DOM 方法把数据插入网页。虽然 Ajax 这个名称中包含 XML,但实际上 Ajax 通信与数据格式无关。这个技术主要是可以实现在不刷新页面的情况下从服务器获取数据,格式并不一定是 XML。

XHR 对象的 API 被普遍认为比较难用,而 Fetch API 自从诞生以后就迅速成为了 XHR 更现代的替代标准。Fetch API 支持期约(promise)和服务线程(service worker),已经成为极其强大的 Web 开发工具。

# 1、XMLHttpRequest 对象

# 1.1 封装简易版 ajax

const ajax = {
  createRequest(method, url, data, fn, contentType) {
    const xhr = new XMLHttpRequest(); // 创建 XMLHttpRequest 实例
    xhr.open(method, url, true);
    contentType && xhr.setRequestHeader('Content-Type', contentType);
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        fn(xhr.response);
      }
    };
    xhr.send(data); // 发送请求
  },
  get(url, fn) {
    ajax.createRequest('GET', url, null, fn);
  },
  post(url, data, fn) {
    ajax.createRequest('POST', url, data, fn, 'application/json');
  },
};

// 发送get
ajax.get('/api/test?a=1&b=2', res => {
  console.log(res);
});
// 发送post
ajax.post('/api/user', JSON.stringify({ a: 1, b: 2 }), res => {
  console.log(res);
});
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

# 1.2 XHR

open(method, url, async) 方法,接收三个参数:method 请求类型("get"、"post"等)、请求 url 和是否异步。调用 open() 只会为发送请求准备。

send() 方法接收一个参数,是作为请求体发送的数据。如果不需要发送请求体,则必须传 null,因为这个参数在某些浏览器中是必需的。调用 send() 之后,请求就会发送到服务器。

收到响应后,XHR 对象的以下属性会被填充上数据:

  • readyState 表示当前处在请求/响应过程的哪个阶段
    • 0 未初始化(Uninitialized)。尚未调用 open() 方法。
    • 1 已打开(Open)。已调用 open() 方法,尚未调用 send() 方法。
    • 2 已发送(Sent)。已调用 send() 方法,尚未收到响应。
    • 3 接收中(Receiving)。已经收到部分响应。
    • 4 完成(Complete)。已经收到所有响应,可以使用了。
  • responseText 作为响应体返回的文本。
  • responseXML 如果响应的内容类型是"text/xml"或"application/xml",那就是包含响应数据的 XML DOM 文档。
  • response 响应体
  • status:响应的 HTTP 状态。
  • statusText:响应的 HTTP 状态描述。

每次 readyState 从一个值变成另一个值,都会触发 readystatechange 事件。

在收到响应之前如果想取消异步请求,可以调用 abort() 方法:

xhr.abort();
1

# 1.2 HTTP 头部

默认情况下,XHR 请求会发送以下头部字段。

  • Accept:浏览器可以处理的内容类型。
  • Accept-Charset:浏览器可以显示的字符集。
  • Accept-Encoding:浏览器可以处理的压缩编码类型。
  • Accept-Language:浏览器使用的语言。
  • Connection:浏览器与服务器的连接类型。
  • Cookie:页面中设置的 Cookie。
  • Host:发送请求的页面所在的域。
  • Referer:发送请求的页面的 URI。注意,这个字段在 HTTP 规范中就拼错了,所以考虑到兼容性也必须将错就错。(正确的拼写应该是 Referrer。)
  • User-Agent:浏览器的用户代理字符串。

setRequestHeader() 方法,发送额外的请求头部,接收两个参数:头部字段的名称和值。

为保证请求头部被发送,必须在 open() 之后、send() 之前调用 setRequestHeader()

getResponseHeader() 方法,从 XHR 对象获取响应头部,只要传入要获取头部的名称即可。

getAllResponseHeaders() 方法,这个方法会返回包含所有响应头部的字符串。

let myHeader = xhr.getResponseHeader('MyHeader');
let allHeaders = xhr.getAllResponseHeaders();
1
2

# 1.3 Content-Type

请求参数有 3 种格式:

(1)application/x-www-formurlencoded

const str = 'a=1&b=2';
const contentType = 'application/x-www-form-urlencoded';
ajax.createRequest(
  'POST',
  '/api/user',
  str,
  res => {
    console.log(res);
  },
  contentType,
);
1
2
3
4
5
6
7
8
9
10
11

(2)application/json

const str = JSON.stringify({ a: 1, b: 2 });
const contentType = 'application/json';
ajax.createRequest(
  'POST',
  '/api/user',
  str,
  res => {
    console.log(res);
  },
  contentType,
);
1
2
3
4
5
6
7
8
9
10
11

(3)multipart/form-data

const formData = new FormData();
formData.append('a', 1);
formData.append('b', 2);
ajax.createRequest('POST', '/api/user', formData, res => {
  console.log(res);
});
1
2
3
4
5
6

XHR 对象能够识别作为 FormData 实例传入的数据类型并自动配置相应的头部。

# 1.4 超时处理

xhr.timeout = 1000; // 设置 1 秒超时
xhr.ontimeout = function () {
  alert('Request did not return in a second.');
};
1
2
3
4

# 1.5 overrideMimeType()

overrideMimeType() 方法用于重写 XHR 响应的 MIME 类型。

let xhr = new XMLHttpRequest();
xhr.open('get', 'text.php', true);
xhr.overrideMimeType('text/xml'); // 在 send() 之前调用
xhr.send(null);
1
2
3
4

# 2、进度事件

有以下 6 个进度相关的事件。

  • loadstart:在接收到响应的第一个字节时触发。
  • progress:在接收响应期间反复触发。
  • error:在请求出错时触发。
  • abort:在调用 abort() 终止连接时触发。
  • load:在成功接收完响应时触发。
  • loadend:在通信完成时,且在 error、abort 或 load 之后触发。

# 2.1 load 事件

load 事件在响应接收完成后立即触发,onload 事件处理程序会收到一个 event 对象,其 target 属性设置为 XHR 实例,在这个实例上可以访问所有 XHR 对象属性和方法。

let xhr = new XMLHttpRequest();
xhr.onload = function () {
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    alert(xhr.responseText);
  } else {
    alert('Request was unsuccessful: ' + xhr.status);
  }
};
xhr.open('get', 'altevents.php', true);
xhr.send(null);
1
2
3
4
5
6
7
8
9
10

# 2.2 progress 事件

每次触发时,onprogress 事件处理程序都会收到 event 对象,其 target 属性是 XHR 对象,且包含 3 个额外属性 lengthComputable、position 和 totalSize。

  • lengthComputable 是一个布尔值,表示进度信息是否可用;
  • position 是接收到的字节数;
  • totalSize 是响应的 ContentLength 头部定义的总字节数。

# 2.3 上传文件进度

const uploads = async (uploadUrl: string, file: File) => {
  try {
    return await new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('put', uploadUrl);
      xhr.setRequestHeader(
        'Requesttoken',
        localStorage.getItem(LOGIN_CONF.requesttoken),
      );
      xhr.upload.onprogress = event => {
        if (event.lengthComputable) {
          console.log('上传进度', event.loaded / event.total);
        }
      };
      xhr.onerror = () => {
        reject('上传失败!');
      };
      xhr.onabort = () => {
        reject('已取消上传');
      };
      xhr.upload.onabort = () => {
        reject('已取消上传');
      };
      xhr.onload = () => {
        console.log('ok', xhr.status);
        if (xhr.status === 200) {
          resolve(true);
        }
      };
      xhr.send(file);
    });
  } catch (e) {
    return Promise.reject(e);
  }
};
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

axios

export const uploadFile = (
  pathName: string,
  data: File,
  onUploadProgress: any,
) => {
  return axios.request({
    method: 'post',
    url: pathName,
    onUploadProgress,
    data,
  });
};
1
2
3
4
5
6
7
8
9
10
11
12

# 3、跨源资源共享

默认情况下,XHR 只能访问与发起请求的页面在同一个域内的资源。

跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。

CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。

对于简单的请求,比如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型,这样的请求在发送时会有一个额外的头部叫 Origin。Origin 头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。

Origin: http://www.nczonline.net
1

如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。比如:

Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Origin: *
1
2

如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个请求。注意,无论请求还是响应都不会包含 cookie 信息。

现代浏览器通过 XMLHttpRequest 对象原生支持 CORS。在尝试访问不同源的资源时,这个行为会被自动触发。要向不同域的源发送请求,可以使用标准 XHR 对象并给 open() 方法传入一个绝对 URL

跨域 XHR 对象允许访问 status 和 statusText 属性,也允许同步请求。出于安全考虑,跨域 XHR 对象也施加了一些额外限制。

  • 不能使用 setRequestHeader()设置自定义头部。
  • 不能发送和接收 cookie。
  • getAllResponseHeaders()方法始终返回空字符串。

# 3.1 预检请求

CORS 通过一种叫预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除 GET 和 POST 之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。这个请求使用 OPTIONS 方法发送并包含以下头部

  • Origin:与简单请求相同。
  • Access-Control-Request-Method:请求希望使用的方法。
  • Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
1
2
3

在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。

  • Access-Control-Allow-Origin:与简单请求相同。
  • Access-Control-Allow-Methods:允许的方法(逗号分隔的列表)。
  • Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)。
  • Access-Control-Max-Age:缓存预检请求的秒数。

例如:

Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
1
2
3
4

预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的 HTTP 请求。

# 3.2 凭据请求

默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将 withCredentials 属性设置为 true 来表明请求会发送凭据。

如果服务器允许带凭据的请求,那么可以在响应中包含如下 HTTP 头部:

Access-Control-Allow-Credentials: true

如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给 JavaScript(responseText 是空字符串,status 是 0,onerror()被调用)。注意,服务器也可以在预检请求的响应中发送这个 HTTP 头部,以表明这个源允许发送凭据请求。

# 4、替代性跨源技术

CORS 出现之前,实现跨源 Ajax 通信是有点麻烦的。开发者需要依赖能够执行跨源请求的 DOM 特性,在不使用 XHR 对象情况下发送某种类型的请求。虽然 CORS 目前已经得到广泛支持,但这些技术仍然没有过时,因为它们不需要修改服务器。

# 4.1 图片探测

图片探测是利用<img>标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onload 和 onerror 事件处理程序得知何时收到响应。

这种动态创建图片的技术经常用于图片探测(image pings)。图片探测是与服务器之间简单、跨域、单向的通信。数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为 204 的状态码。浏览器通过图片探测拿不到任何数据,但可以通过监听 onload 和 onerror 事件知道什么时候能接收到响应。

let img = new Image();
img.onload = img.onerror = function () {
  alert('Done!');
};
img.src = 'http://www.example.com/test?name=Nicholas';
1
2
3
4
5

设置完 src 属性之后请求就开始了,这个例子向服务器发送了一个 name 值。

图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送 GET 请求和无法获取服务器响应的内容。这也是只能利用图片探测实现浏览器与服务器单向通信的原因。

# 4.2 JSONP

JSONP 是“JSON with padding”的简写,是在 Web 服务上流行的一种 JSON 变体。JSONP 看起来跟 JSON 一样,只是会被包在一个函数调用里,比如:

callback({ name: 'Nicholas' }); // 服务器返回格式必须包含回调方法名
1

JSONP 格式包含两个部分:回调和数据。回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的 JSON 数据。

function handleResponse(response) {
  console.log(response);
}
document.getElementById('send').onclick = function () {
  let script = document.createElement('script');
  script.src =
    'http://10.10.9.1:8888/api/userInfor/companyList2?callback=handleResponse';
  document.body.insertBefore(script, document.body.firstChild);
};
1
2
3
4
5
6
7
8
9

# 5、Fetch API

Fetch API 能够执行 XMLHttpRequest 对象的所有任务,但更容易使用,接口也更现代化,能够在 Web 工作线程等现代 Web 工具中使用。XMLHttpRequest 可以选择异步,而 Fetch API 则必须是异步。

Fetch API 本身是使用 JavaScript 请求资源的优秀工具,同时这个 API 也能够应用在服务线程(service worker)中,提供拦截、重定向和修改通过 fetch() 生成的请求接口。

# 5.1 基本用法

function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>

const r = fetch('/api/test');
r.then(res => res.text()).then(text => {
  console.log(text);
});
1
2
3
4

两个参数:

  • input,要获取资源的 URL
  • init,传入的配置对象,包含 body、headers、method 等信息

# 5.2 常见 Fetch 请求模式

与 XMLHttpRequest 一样,fetch() 既可以发送数据也可以接收数据。使用 init 对象参数,可以配置 fetch() 在请求体中发送各种序列化的数据。

// 发送 json 数据
const init = {
  method: 'POST', // 发送请求体时必须使用一种 HTTP 方法
  body: JSON.stringify({ foo: 'bar' }),
  headers: new Headers({ 'Content-Type': 'application/json' }),
};

// 发送参数
const init = {
  method: 'POST',
  body: 'foo=bar&baz=qux',
  headers: new Headers({
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  }),
};

// 发送FormData(支持发送文件)
const formData = new FormData();
let inputFile = document.querySelector("input[type='file']");
formData.append('file', inputFile.files[0]);
formData.append('user', '111');
const init = {
  method: 'POST',
  body: formData,
};

// 调用
fetch('/api/user', init)
  .then(res => res.text())
  .then(text => {
    console.log(text);
  });

// 加载 blob
const imageElement = document.querySelector('img');
fetch('my-image.png')
  .then(response => response.blob())
  .then(blob => {
    imageElement.src = URL.createObjectURL(blob);
  });

// 发送跨源请求
fetch('//cross-origin.com'); // 响应要包含 CORS 头部才能保证浏览器收到响应。
// TypeError: Failed to fetch
// No 'Access-Control-Allow-Origin' header is present on the requested resource.

// 发送探测请求
fetch('//cross-origin.com', { method: 'no-cors' }).then(response =>
  console.log(response.type),
);
// opaque
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

中断请求:

let abortController = new AbortController();
fetch('wikipedia.zip', { signal: abortController.signal })
 .catch(() => console.log('aborted!');
// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);
// 已经中断
1
2
3
4
5
6

# 5.3 Headers 对象

Headers 对象是所有外发请求和入站响应头部的容器。每个外发的 Request 实例都包含一个空的 Headers 实例,可以通过 Request.prototype.headers 访问,每个入站 Response 实例也可以通过 Response.prototype.headers 访问包含着响应头部的 Headers 对象。这两个属性都是可修改属性。另外,使用 new Headers() 也可以创建一个新实例。

(1)Headers 与 Map 的相似之处

Headers 与 Map 类型都有 get()、set()、has() 和 delete() 等实例方法

let h = new Headers();
let m = new Map();
// 设置键
h.set('foo', 'bar');
m.set('foo', 'bar');
1
2
3
4
5

Headers 和 Map 都可以使用一个可迭代对象来初始化

let seed = [['foo', 'bar']];
let h = new Headers(seed);
let m = new Map(seed);
console.log(h.get('foo')); // bar
console.log(m.get('foo')); // bar
1
2
3
4
5

而且,它们也都有相同的 keys()、values() 和 entries() 迭代器接口:

let seed = [
  ['foo', 'bar'],
  ['baz', 'qux'],
];
let h = new Headers(seed);
let m = new Map(seed);
console.log(...h.keys()); // foo, baz
console.log(...m.keys()); // foo, baz
1
2
3
4
5
6
7
8

(2)Headers 独有的特性

在初始化 Headers 对象时,也可以使用键/值对形式的对象

let seed = { foo: 'bar' };
let h = new Headers(seed);
console.log(h.get('foo')); // bar
let m = new Map(seed);
// TypeError: object is not iterable
1
2
3
4
5

一个 HTTP 头部字段可以有多个值,而 Headers 对象通过 append() 方法支持添加多个值。

let h = new Headers();
h.append('foo', 'bar');
console.log(h.get('foo')); // "bar"
h.append('foo', 'baz');
console.log(h.get('foo')); // "bar, baz"
1
2
3
4
5

某些情况下,并非所有 HTTP 头部都可以被客户端修改,而 Headers 对象使用护卫来防止不被允许的修改。不同的护卫设置会改变 set()、append()和 delete()的行为。违反护卫限制会抛出 TypeError。

# 5.4 Request 对象

Request 对象是获取资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。

(1)创建 Request 对象

let r = new Request('https://foo.com');
console.log(r);
// Request {...}
1
2
3

Request 构造函数也接收第二个参数——一个 init 对象。这个 init 对象与前面介绍的 fetch() 的 init 对象一样。没有在 init 对象中涉及的值则会使用默认值。

(2)克隆 Request 对象

Fetch API 提供了两种方式用于创建 Request 对象的副本:使用 Request 构造函数和使用 clone() 方法。

let r1 = new Request('https://foo.com');
let r2 = new Request(r1);
console.log(r2.url); // https://foo.com/

console.log(r1.bodyUsed); // true 已使用
console.log(r2.bodyUsed); // false

let r1 = new Request('https://foo.com', { method: 'POST', body: 'foobar' });
let r2 = r1.clone();
console.log(r1.url); // https://foo.com/
console.log(r2.url); // https://foo.com/

console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这种克隆方式并不总能得到一模一样的副本。最明显的是,第一个请求的请求体会被标记为“已使用”

多次调用 Request

let r = new Request('https://foo.com', { method: 'POST', body: 'foobar' });
fetch(r);
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already been used.

// 使用 clone
let r = new Request('https://foo.com', { method: 'POST', body: 'foobar' });
// 3 个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);
1
2
3
4
5
6
7
8
9
10
11

# 5.5 Response 对象

Response 对象是获取资源响应的接口。这个接口暴露了响应的相关信息,也暴露了使用响应体的不同方式。

let r = new Response();
1

# 5.6 body

body 提供了五个方法:

  • body.text() 方法返回 UTF-8 格式字符串
  • body.json() 方法返回 JSON
  • body.formData() 方法返回 FormData 实例
  • body.arrayBuffer() 方法返回 ArrayBuffer 实例
  • body.blob() 方法返回 Blob 实例

# 6、Beacon API

在 unload 事件触发时,分析工具要停止收集信息并把收集到的数据发给服务器。这时候有一个问题,因为 unload 事件对浏览器意味着没有理由再发送任何结果未知的网络请求(因为页面都要被销毁了)。例如,在 unload 事件处理程序中创建的任何异步请求都会被浏览器取消。为此,异步 XMLHttpRequest 或 fetch()不适合这个任务。分析工具可以使用同步 XMLHttpRequest 强制发送请求,但这样做会导致用户体验问题。浏览器会因为要等待 unload 事件处理程序完成而延迟导航到下一个页面。

W3C 引入了补充性的 Beacon API。这个 API 给 navigator 对象增加了一个 sendBeacon() 方法。这个简单的方法接收一个 URL 和一个数据有效载荷参数,并会发送一个 POST 请求。可选的数据有效载荷参数有 ArrayBufferView、Blob、DOMString、FormData 实例。如果请求成功进入了最终要发送的任务队列,则这个方法返回 true,否则返回 false。

// 发送 POST 请求
// URL: 'https://example.com/analytics-reporting-url'
// 请求负载:'{foo: "bar"}'
navigator.sendBeacon(
  'https://example.com/analytics-reporting-url',
  '{foo: "bar"}',
);
1
2
3
4
5
6
7

sendBeacon() 方法特性:

  • sendBeacon() 并不是只能在页面生命周期末尾使用,而是任何时候都可以使用。
  • 调用 sendBeacon() 后,浏览器会把请求添加到一个内部的请求队列。浏览器会主动地发送队列中的请求。
  • 浏览器保证在原始页面已经关闭的情况下也会发送请求。
  • 状态码、超时和其他网络原因造成的失败完全是不透明的,不能通过编程方式处理。
  • 信标(beacon)请求会携带调用 sendBeacon() 时所有相关的 cookie。

# 7、Web Socket

Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。

在 JavaScript 中创建 Web Socket 时,一个 HTTP 请求会发送到服务器以初始化连接。服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP 协议切换到 Web Socket 协议。

因为 Web Socket 使用了自定义协议,所以 URL 方案(scheme)稍有变化:不能再使用 http:// 或 https://,而要使用 ws:// 和 wss://。前者是不安全的连接,后者是安全连接。

封装 WebSocket:传送门

# 7.1 API

let socket = new WebSocket('ws://www.example.com/');
1

浏览器会在初始化 WebSocket 对象之后立即创建连接。与 XHR 类似,WebSocket 也有一个 readyState 属性表示当前状态。不过,这个值与 XHR 中相应的值不一样。

  • WebSocket.OPENING(0):连接正在建立。
  • WebSocket.OPEN(1):连接已经建立。
  • WebSocket.CLOSING(2):连接正在关闭。
  • WebSocket.CLOSE(3):连接已经关闭。

WebSocket 对象没有 readystatechange 事件,而是有与上述不同状态对应的其他事件。readyState 值从 0 开始。

任何时候都可以调用 close() 方法关闭 Web Socket 连接:socket.close();

调用 close() 之后,readyState 立即变为 2(连接正在关闭),并会在关闭后变为 3(连接已经关闭)。

# 7.2 发送和接收数据

let socket = new WebSocket('ws://www.example.com/server.php');

// 发送数据
let stringData = 'Hello world!';
let arrayBufferData = Uint8Array.from(['f', 'o', 'o']);
let blobData = new Blob(['f', 'o', 'o']);
socket.send(stringData);
socket.send(arrayBufferData.buffer);
socket.send(blobData);

// 接收数据
socket.onmessage = function (event) {
  let data = event.data;
  // 对数据执行某些操作
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3 其他事件

WebSocket 对象在连接生命周期中有可能触发 3 个其他事件。

  • open:在连接成功建立时触发。
  • error:在发生错误时触发。连接无法存续。
  • close:在连接关闭时触发。

# 8、安全

在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF,cross-site request forgery)攻击。未授权系统会按照处理请求的服务器的要求伪装自己。Ajax 应用程序,无论大小,都会受到 CSRF 攻击的影响,包括无害的漏洞验证攻击和恶意的数据盗窃或数据破坏攻击。

关于安全防护 Ajax 相关 URL 的一般理论认为,需要验证请求发送者拥有对资源的访问权限。可以通过如下方式实现。

  • 要求通过 SSL 访问能够被 Ajax 访问的资源。
  • 要求每个请求都发送一个按约定算法计算好的令牌(token)。

注意,以下手段对防护 CSRF 攻击是无效的。

  • 要求 POST 而非 GET 请求(很容易修改请求方法)。
  • 使用来源 URL 验证来源(来源 URL 很容易伪造)。
  • 基于 cookie 验证(同样很容易伪造)。
上次更新: 3/25/2024, 10:07:22 PM