18. Js脚本化HTTP

343 阅读10分钟

超文本传输协议(HyperText Transfer Protocol,HTTP)规定Web浏览器如何从Web服务器获取文档和向Web服务器提交表单内容,以及Web服务器如何响应这些请求和提交。

  • Ajax应用的主要特点是使用脚本操纵HTTP和Web服务器进行数据交换,不会导致页面重载。(Asynchronous JavaScript and XML)
  • "Ajax"中的X表示XML,这些名字只是XML流行时的遗迹。
  • Comet和Ajax相反。在Comet中,Web服务器发起通信并异步发送消息到客户端。
  • 在Ajax中,客户端从服务端“拉”数据,而在Comet中,服务端向客户端“推”数据。
  • 脚本通过设置<img>元素的src属性,且把信息作为图片URL的查询字符串部分,就把能经过编码信息传递给Web服务器。
  • 为了把<iframe>作为Ajax传输协议使用,脚本首先要把发送给Web服务器的信息编码到URL中,然后设置<iframe>的src属性为该URL。服务器能创建一个包含响应内容的HTML文档,并把它返回给Web浏览器,并且在<iframe>中显示它。
  • <script>元素的src属性能设置URL并发起HTTP GET请求。它们可以跨域通信而不受限于同源策略。使用基于<script>的Ajax传输协议时,服务器的响应采用JSON编码的数据格式,当执行脚本时,JavaScript解析器能自动将其“解码”,因此这种Ajax传输协议也叫做"JSONP"。
  • XMLHttpRequest对象,它定义了用脚本操纵HTTP的API。除了常用的GET请求,这个API还包含实现POST请求的能力,它能获取任何类型的文本文档。

1. 使用XMLHttpRequest

XMLHttpRequest类的每个实例都表示一个独立的请求/响应对,并且这个对象的属性和方法允许指定请求细节和提取响应数据。

image.png

  • 第一件事就是实例化XMLHttpRequest对象:
var request=new XMLHttpRequest();

一个HTTP请求由4部分组成:

  1. HTTP请求方法或“动作”(verb)
  2. 正在请求的URL
  3. 一个可选的请求头集合,其中可能包括身份验证信息
  4. 一个可选的请求主体 服务器返回的HTTP响应包含3部分:
  5. 一个数字和文字组成的状态码,用来显示请求的成功和失败
  6. 一个响应头集合
  7. 响应主体
  • XMLHttpRequest不是协议级的HTTP API而是浏览器级的API。浏览器需要考虑cookie、重定向、缓存和代理,但代码只需要担心请求和响应。
  • 如果从本地文件中加载网页,那么该页面中的脚本将无法通过相对URL使用XMLHttpRequest,因为这些URL将相对于file://URL而不是http://URL。而同源策略通常会阻止使用绝对http://URL。因此当使用XMLHttpRequest时,为了测试它们通常必须把文件上传到Web服务器(或运行一个本地服务器)

1.1 指定请求

创建XMLHttpRequest对象之后,发起HTTP请求的下一步是调用XMLHttpRequest对象的open()方法去指定这个请求的两个必需部分:方法和URL。

request.open("GET",//开始一个HTTP GET请求
"data.csv");//URL的内容

open()的第一个参数指定HTTP方法或动作

  • GET"用于常规请求,它适用于当URL完全指定请求资源,当请求对服务器没有任何副作用以及当服务器的响应是可缓存时。
  • "POST"方法常用于HTML表单。它在请求主体中包含额外数据(表单数据)且这些数据常存储到服务器上的数据库中(副作用)。
  • 除了"GET"和"POST"之外,XMLHttpRequest规范也允许把"DELETE"、"HEAD"、"OPTIONS"和"PUT"作为open()的第1个参数。 open()的第2个参数是URL,它是请求的主题。这是相对于文档的URL
  • 如果有请求头的话,请求进程的下个步骤是设置它。例如,POST请求需要"Content-Type"头指定请求主题的MIME类型:
request.setRequestHeader("Content-Type","text/plain");

image.png

  • 如果对相同的请求头调用setRequestHeader()多次,新值不会取代之前指定的值,相反,HTTP请求将包含这个头的多个副本或这个头将指定多个值。 使用XMLHttpRequest发起HTTP请求的最后一步是指定可选的请求主体并使用send()方法向服务器发送它
request.send(null);
  • GET请求绝对没有主体,所以应该传递null或省略这个参数。POST请求通常拥有主体,同时它应该匹配使用setRequestHeader()指定的"Content-Type"头。
  • HTTP请求的各部分有指定顺序:请求方法和URL首先到达,然后是请求头,最后是请求主体。
  • 调用XMLHttpRequest方法的顺序必须匹配HTTP请求的架构。例如,setRequestHeader()方法的调用必须在调用open()之前但在调用send()之后,否则它将抛出异常。
// 用POST方法发送纯文本给服务器
function postMessage(msg) {
  var request = new XMLHttpRequest(); //新请求
  request.open("POST", "/log.php"); //用POST向服务器端发送脚本
  //用请求主体发送纯文本消息
  request.setRequestHeader(
    "Content-Type", //请求主体将是纯文本
    "text/plain;charset=UTF-8"
  );
  request.send(msg); //把msg作为请求主体发送
  //请求完成,我们将忽略任何响应和任何错误
}

1.2 取得响应

一个完整的HTTP响应由状态码、响应头集合和响应主体组成。这些都可以通过XMLHttpRequest对象的属性和方法使用:

  • status和statusText属性以数字和文本的形式返回HTTP状态码。这些属性保存标准的HTTP值,像200和"OK"表示成功请求,404和"Not Found"表示URL不能匹配服务器上的任何资源。
  • 使用getResponseHeader()和getAllResponseHeaders()能查询响应头
  • 响应主体可以从responseText属性中得到文本形式的 XMLHttpRequest对象通常异步使用:发送请求后,send()方法立即返回,直到响应返回,它的响应方法和属性才有效。为了在响应准备就绪时得到通知,必须监听XMLHttpRequest对象上的readystatechange事件
  • 为了监听readystatechange事件,请把事件处理函数设置为XMLHttpRequest对象的onreadystatechange属性。也能使用addEventListener()
  • readyState是一个整数,它指定了HTTP请求的状态

image.png

1.2.1 同步响应

XMLHttpRequest也支持同步响应。如果把false作为第3个参数传递给open(),那么send()方法将阻塞直到请求完成。

1.2.2 响应解码

如果服务器想发送诸如对象或数组这样的结构化数据作为其响应,它应该传输JSON编码的字符串数据。此时该响应的"Content-Type"头为"application/json"。

  • Web服务端通常使用二进制数据(例如,图片文件)响应HTTP请求。
  • 在调用send()之前把类型传递给overrideMimeType(),这将使XMLHttpRequest忽略"Content-Type"头而使用指定的类型:
//重新指定响应的处理类型
request.overrideMimeType("text/plain;charset=utf-8")

1.3 编码请求主体

HTTP POST请求包括一个请求主体,它包含客户端传递给服务器的数据

1.3.1 表单编码请求

对表单数据使用的编码方案相对简单:对每个表单元素的名字和值执行普通的URL编码(使用十六进制转义码替换特殊字符),使用等号把编码后的名字和值分开,并使用“&”符号分开名/值对。

find=pizza&zipcode=02134&radius=1km

表单数据编码格式有一个正式的MIME类型,当使用POST方法提交这种顺序的表单数据时,必须设置"Content-Type"请求头为这个值:

application/x-www-form-urlencoded

GET请求从来没有主体,所以需要发送给服务器的表单编码数据“负载”要作为URL(后跟一个问号)的查询部分。

1.3.2 JSON编码请求

使用JSON.stringify()编码请求主体

request.setRequestHeader("Content-Type","application/json");
request.send(JSON.stringify(data));

1.3.3 multipart/form-data请求

当HTML表单同时包含文件上传元素和其他元素时,浏览器不能使用普通的表单编码而必须使用称为"multipart/form-data"的特殊Content-Type来用POST方法提交表单。

XHR2(XMLHttpRequest2)定义了新的FormData API,它容易实现多部分请求主体。首先,使用FormData()构造函数创建FormData对象,然后按需多次调用这个对象的append()方法把个体“部分”(可以是字符串、File或Blob对象)添加到请求中。最后,把FormData对象传递给send()方法。

var formdata=new FormData();
formdata.append(name,value);//作为一部分添加名/值对
request.send(formdata);

image.png

1.4 HTTP进度事件

  • 当正在加载服务器的响应时,XMLHttpRequest对象会发生progress事件
  • 当事件完成,会触发load事件。
  • HTTP请求无法完成有3种情况,对应3种事件。如果请求超时,会触发timeout事件。如果请求中止,会触发abort事件。最后,像太多重定向这样的网络错误会阻止请求完成,但这些情况发生时会触发error事件。

1.4.1 上传进度事件

XMLHttpRequest对象将有upload属性。upload属性值是一个对象,它定义了addEventListener()方法和整个progress事件集合,比如onprogress和onload。

image.png 对于XMLHttpRequest对象x,设置x.onprogress以监控响应的下载进度,并且设置x.upload.onprogress以监控请求的上传进度。

1.5 中止请求和超时

  • 可以通过调用XMLHttpRequest对象的abort()方法来取消正在进行的HTTP请求。
  • HR2定义了timeout属性来指定请求自动中止后的毫秒数,也定义了timeout事件用于当超时发生时触发

1.6 跨域HTTP请求

XHR2通过在HTTP响应中选择发送合适的CORS(Cross-Origin Resource Sharing,跨域资源共享)允许跨域访问网站。

跨域请求通常也不会包含其他任何的用户证书:cookie和HTTP身份验证令牌(token)通常不会作为请求的内容部分发送且任何作为跨域响应来接收的cookie都会丢弃。

作为Web程序员,使用这个功能并不需要做什么额外的工作:如果浏览器支持XMLHttpRequest的CORS且实现跨域请求的网站决定使用CORS允许跨域请求,那么同源策略将不放宽而跨域请求就会正常工作

测试withCredentials的存在性是测试浏览器是否支持CORS的一种方法

var supportsCORS=(new XMLHttpRequest()).withCredentials!==undefined;

2. 借助<script>发送HTTP请求:JSONP

只须设置<script>元素的src属性(假如它还没插入到document中,需要插入进去),然后浏览器就会发送一个HTTP请求以下载src属性所指向的URL。

  • 使用<script>元素进行Ajax传输的一个主要原因是,它不受同源策略的影响
  • 需要注意的是,这种方式普遍用于可信的第三方脚本
  • 这种使用<script>元素作为Ajax传输的技术称为JSONP,P代表“填充”或“前缀”

3. 基于服务器端推送事件的Comet技术

在服务器端推送事件的标准草案中定义了一个EventSource对象,简化了Comet应用程序的编写可以传递一个URL给EventSource()构造函数,然后在返回的实例上监听消息事件。

image.png

var ticker = new EventSource("stockprices.php");
ticker.onmessage = function(e) {
  var type = e.type;
  var data = e.data; //现在处理事件类型和事件的字符串数据
};

服务器端推送事件的协议很简单。客户端(创建一个EventSource对象时会)建立一个到服务器的连接,服务器保持这个连接处于打开状态。

一个使用EventSource的简易聊天客户端

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
    <style></style>
  </head>
  <body>
    <!--聊天的UI只是一个单行文本域-->
    <!--新的聊天消息会插入input域之前-->
    <input id="input" style="width:100%" />

    <script>
      window.onload = function() {
        //注意一些UI细节
        var nick = prompt("Enter your nickname"); //获取用户昵称
        var input = document.getElementById("input"); //找出input表单元素
        input.focus(); //设置键盘焦点
        //通过EventSource注册新消息的通知
        var chat = new EventSource("/chat");
        chat.onmessage = function(event) {
          //当捕获一条消息时
          var msg = event.data; //从事件对象中取得文本数据
          var node = document.createTextNode(msg); //把它放入一个文本节点
          var div = document.createElement("div"); //创建一个<div>
          div.appendChild(node); //将文本节点插入div中
          document.body.insertBefore(div, input); //将div插入input之前
          input.scrollIntoView(); //保证input元素可见
        };
        //使用XMLHttpRequest把用户的消息发送给服务器
        input.onchange = function() {
          //用户完成输入
          var msg = nick + ":" + input.value; //组合用户名和用户输入的信息
          var xhr = new XMLHttpRequest(); //创建新的XHR
          xhr.open("POST", "/chat"); //发送到/chat
          xhr.setRequestHeader(
            "Content-Type", //指明为普通的UTF-8文本
            "text/plain;charset=UTF-8"
          );
          xhr.send(msg); //发送消息
          input.value = ""; //准备下次输入
        };
      };
    </script>
  </body>
</html>