网络请求与远程资源

JavaScript网络请求与远程资源

  • XMLHttpRequest对象
  • XMLHttpRequest事件
  • 源域Ajax限制
  • Fetch API
  • Streams API

XMLHttpRequest对象

XHR

let xhr = XMLHttpRequest();
// 最后一个参数表示请求是否异步
xhr.open('get', 'example.php', false);
// send方法接收一个参数,是作为请求体发送的数据。如果不需要发送请求体,则必须传null。
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
  alert(xhr.)
} else {
  alert("Request was unsuccessful: " + xhr.status);
}

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

  • responseText:作为响应体返回的文本。
  • responseXML:如果响应的内容类型是text/html或application/xml,那就是包含响应数据的xml,Dom文档。
  • status:响应的HTTP状态。
  • statusText:响应的HTTP状态描述。
let xhr = new XMLHttpRequest();
xhr.open('get', 'example.php', true);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
  alert(xhr.responseText);
} else {
  alert("Request was unsuccessful: " + xhr.status);
}

XHR对象有一个readyState属性,表示当前在请求/响应过程的哪个阶段。

  • 0:未初始化(uninitialized)。尚未调用open()方法。
  • 1:已打开(Open)。已调用open()方法,尚未调用send()方法。
  • 2:已发送(Sent)。已调用send()方法,尚未收到响应。
  • 3:接收中(Receivering)。已收到部分响应。
  • 4:完成(Complete)。已经收到所有响应,可以使用了。

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

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

HTTP头部

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

  • Accept:
  • Accept-Charset
  • Accept-Encoding
  • Accept-Language
  • Connection
  • Cookie
  • Host:发送请求的页面所在的域
  • Referer:发送请求的页面的URL。
  • User-Agent
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
  }
};
xhr.open('get', 'example.txt', true);
xhr.send(null);

如果需要发送额外的请求头部,可以使用setRequestHeader()方法。xhr.setRequestHeader('myHeader', 'myValue'),为保证请求头部被发送,必须在open()之后,send()之前调用setRequestHeader()。

POST请求

客户端

let data = new FormData();
data.append('Name', 'Mr.Yao');
xhr.open('post', 'http://127.0.0.1:3000')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data)

服务端

var http = require('http');
var querystring = require('querystring');
var util = require('util');
 
http.createServer(function(req, res){
    // 定义了一个post变量,用于暂存请求体的信息
    res.setHeader("Access-Control-Allow-Origin", "*"); 
    var post = '';     
 
    // 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
    req.on('data', function(chunk){    
        post += chunk;
    });
    // 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
    req.on('end', function(){    
        post = querystring.parse(post);
        res.end(util.inspect(post));
    });
}).listen(3000);

超时

let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    try {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        alert(xhr.responseText);
      } else {
        alert('Request was unsuccessful: ' + xhr.status);
      }
    } catch(ex) {
      // 假设由ontimeout处理
    }
  }
}
xhr.timeout = 1000;
xhr.ontimeout = function() {
  alert('Request did not return in a second');
};

给timeout设置1000毫秒,如果请求没有在1秒钟内返回则会中断。此时则会触发ontimeout事件处理程序,readyState仍会变成4,因此也会调用onreadystatechange事件处理程序。不过,如果在超时之后访问status属性则会发生错误。

let xhr = new XMLHttpRequest();
xhr.open('get', 'text.php', true);
xhr.overrideMimeType('text/xml');
xhr.send(null);

进度事件

load事件在响应接收完成后立即触发,这样就不用检查readyState属性了。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);

progress事件在浏览器接收数据期间,这个事件会反复触发。

let xhr = new XMLHttpRequest();
xhr.onload = function(event) {
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    alert(xhr.responseText);
  } else {
    alert("Request was unsuccessful: " + xhr.status);
  }
};
xhr.onprogress = function(event) {
let divStatus = document.getElementById("status");
if (event.lengthComputable) { 21
    divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize + " bytes"; }
};
xhr.open("get", "altevents.php", true);
xhr.send(null);

loadstart:在接收到响应的第一个字节时触发。

error:在请求出错时触发。

abort:在调用abort()终止连接时触发。

每次请求都会首先触发loadstart事件,之后是一个或多个progress事件,接着是error、abort或load中的一个,最后以loadend事件结束。

跨域资源共享

跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。 CORS背后的基本思路就是使用自定义的HTTP头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。

对于简单的请求,比如GET或POST请求,没有自定义头部,而且请求体是text/plain类型,这样的请求在发送时会有一个额外的头部叫Origin。Origin头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。如果服务器决定响应请求,那么应该发送Access-Control-Allow-Origin头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。

图片探测

图片探测是利用<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";

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

JSONP

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

JSONP调用是通过动态创建<script>元素并为src属性指定跨域URL实现的。此时的<script><img>元素类似,能够不受限制地从其他域加载资源。因为JSONP是有效的JavaScript,所以JSONP响应在被加载完成之后会立即执行。

function handleResponse(response) {
	console.log(`You're at IP address ${response.ip}, which is in ${response.city}, ${response.region_name}`);
}
let script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

首先,JSONP是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。此时除了完全删除JSONP没有其他办法。在使用不受控的Web服务时,一定要保证是可以信任的。

第二个缺点是不好确定JSONP请求是否失败。虽然HTML5规定了<script>元素的onerror事件 处理程序,但还没有被任何浏览器实现。为此,开发者经常使用计时器来决定是否放弃等待响应。这种方式并不准确,毕竟不同用户的网络连接速度和带宽是不一样的。

Fetch API

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

分派请求

fetch()只有一个必需的参数input。多数情况下,这个参数是要获取资源的URL。这个方法返回一个期约。

fetch('bar.txt')
 	.then((response) => {
  console.log(response);
});
// Response { type: "basic", url: ... }

读取响应

读取响应内容的最简单方式是取得纯文本格式的内容,这要用到text()方法。这个方法返回一个期约,会解决为取得资源的完整内容。

fetch('bar.txt')
  .then((response) => response.text())
  .then((data) => console.log(data));
// bar.txt 的内容

处理状态码和请求失败

fetch('/bar')
  .then((response) => {
  console.log(response.status);     // 200
  console.log(response.statusText); // OK
});

可以显式地设置fetch()在遇到重定向时的行为,不过默认行为是跟随重定向并返回状态码不是300~399的响应。跟随重定向时,响应对象的redirected属性会被设置为true,而状态码仍然是200。

自定义选项

只使用URL时,fetch()会发送GET请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数init对象。

  • body
    指定使用请求体时请求体的内容,必须是Blob、BufferSource、FormData、URLSearchParams、ReadableStream或String的实例。

  • headers

    用于指定请求头部,必须是Headers对象实例或包含字符串格式键/值对的常规对象,默认值为不包含键/值对的Headers对象。这不意味着请求不包含任何头部,浏览器仍然会随请求发送一些头部。虽然这些头部对JavaScript不可见,但浏览器的网络检查器可以观察到。

  • method

    用于指定HTTP请求方法。

常见的fetch请求模式

发送JSON数据

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

在请求体中发送参数

let payload = 'foo=bar&baz=qux';
let paramHeaders = new Headers({
	'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
});
fetch('/send-me-params', {
	method:'POST', //发送请求体时必须使用一种HTTP方法body: payload,
	headers: paramHeaders
});

发送文件

let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file']");
imageFormData.append('image', imageInput.files[0]);
fetch('/img-upload', {
  method: 'POST',
  body: imageFormData
});

加载Blod文件

Fetch API也能提供Blob类型的响应,而Blob又可以兼容多种浏览器API。一种常见的做法是明确将图片文件加载到内存,然后将其添加到HTML图片元素。为此,可以使用响应对象上暴露的blob()方法。这个方法返回一个期约,解决为一个Blob的实例。然后,可以将这个实例传给URL.createObjectUrl()以生成可以添加给图片元素src属性的值。

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

发送跨源请求

从不同的源请求资源,响应要包含CORS头部才能保证浏览器收到响应。没有这些头部,跨源请求会失败并抛出错误。

如果代码不需要访问响应,也可以发送no-cors请求。此时响应的type属性值为opaque,因此无法读取响应内容。这种方式适合发送探测请求或者将响应缓存起来供以后使用。

fetch('//cross-origin.com', { method: 'no-cors' }) 
  .then((response) => console.log(response.type));
// opaque

中断请求

Fetch API支持通过AbortController/AbortSignal对中断请求。调用AbortController.abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况。中断进行中的fetch()请求会导致包含错误的拒绝。

Web Socket

Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。在JavaScript中创建WebSocket时,一个HTTP请求会发送到服务器以初始化连接。服务器响应后,连接使用HTTP的Upgrade头部从HTTP协议切换到Web Socket协议。这意味着Web Socket不能通过标准HTTP服务器实现,而必须使用支持该协议的专有服务器。因为Web Socket使用了自定义协议,所以URL方案(scheme)稍有变化:不能再使用http://或https://,而要使用ws://和wss://。前者是不安全的连接,后者是安全连接。在指定Web Socket URL时,必须包含URL方案,因为将来有可能再支持其他方案。

创建

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

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

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

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

socket.close();

发送和接收数据

打开Web Socket之后,可以通过连接发送和接收数据。要向服务器发送数据,使用send()方法并 传入一个字符串、ArrayBuffer或Blob。

服务器向客户端发送消息时,WebSocket对象上会触发message事件。这个message事件与其他消息协议类似,可以通过event.data属性访问到有效载荷。

其他事件

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

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

客户端

var ws = new WebSocket('ws://localhost:3000/');
// Web Socket 已连接上,使用 send() 方法发送数据
ws.onopen = function() {
  // 这里用一个延时器模拟事件
  setInterval(function() {
    ws.send('客户端消息');
  },2000);
}
// 这里接受服务器端发过来的消息
ws.onmessage = function(e) {
  console.log(e.data)
}

服务端

var ws = require('nodejs-websocket');
var server = ws.createServer(function(socket){
// 事件名称为text(读取字符串时,就叫做text),读取客户端传来的字符串
  var count = 1;
  socket.on('text', function(str) {
    // 在控制台输出前端传来的消息  
    console.log(str);
    //向前端回复消息
    socket.sendText('服务器端收到客户端端发来的消息了!' + count++);
  });
  socket.on('error', function(code) {
    // 某些情况如果客户端多次触发连接关闭,会导致connection.close()出现异常,这里try/catch一下
    try {
      socket.close()
    } catch (error) {
      console.log('close异常', error)
    }
    console.log('异常关闭', code)
  })
}).listen(3000);
posted @ 2021-09-07 15:12  Mr-Yao  阅读(105)  评论(0编辑  收藏  举报