Skip to content

Latest commit

 

History

History
617 lines (461 loc) · 25.2 KB

js11.md

File metadata and controls

617 lines (461 loc) · 25.2 KB

基础数据交互

Ajax

AJAX代表异步JavaScript和XML。简而言之,它是使用 XMLHttpRequest 对象与服务器端脚本进行通信。它可以发送以及接收各种格式的信息,包括JSON,XML,HTML,甚至文本文件。AJAX最吸引人的特性是,它的“异步”性质,这意味着它可以做所有这一切,而不必刷新页面。
Ajax的两个主要功能允许你执行以下操作:

  • 向服务器发出请求,而不要重新加载页面
  • 接受和处理服务器中的数据

发出HTTP请求

为了使用JavaScript向服务器发出 HTTP 请求,需要一个提供此功能的类的实例。这就是XMLHttpRequest的由来。这样的类最初是在Internet Explorer中作为一个名为XMLHTTP的ActiveX对象引入的。然后,Mozilla,Safari和其他浏览器,实现一个XMLHttpRequest类,支持Microsoft的原始ActiveX对象的方法和属性。

// Old compatibility code, no longer needed.
if (window.XMLHttpRequest) { // Mozilla, Safari, IE7+ ...
    httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE 6 and older
    httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}

// new
const httpRequest = new XMLHttpRequest();   

接下来,需要决定在收到服务器对您的请求的响应后您想要做什么。在这个阶段,你只需要告诉HTTP请求对象哪个JavaScript函数将处理响应。这是通过将对象的 onreadystatechange 属性设置为当请求的状态更改时应调用的JavaScript函数的名称,如下:

httpRequest.onreadystatechange = nameOfTheFunction;

接下来,在声明接收到响应后会发生什么,需要实际提出请求。调用HTTP请求类的open( ) 和 send( ) 方法,如下:

httpRequest.open('GET', 'http://www.example.org/db.json', true);
httpRequest.send(null);
  • 调用open()的第一个参数是HTTP请求方法 - GET,POST,HEAD或任何其他要使用的服务器支持的方法。保持方法大写按照HTTP标准;否则某些浏览器(如Firefox)可能不会处理请求。
  • 第二个参数是要请求的网页的网址。作为安全功能,不能调用第三方网域上的网页。确保在所有网页上使用确切的域名,否则当您调用open()时,将收到“权限被拒绝”错误。一个常见的陷阱是通过domain.tld访问你的网站,但尝试调用www.domain.tld的网页。如果真的需要向另一个域发送请求,请参考下一节跨域数据交互
  • 可选的第三个参数设置请求是否是异步的。如果为TRUE (默认值),则JavaScript函数的执行将继续,而服务器的响应尚未到达。这就是AJAX中的A(不推荐使用同步方法)

send( ) 方法的参数可以是任何要发送到服务器的数据,如果POST请求。表单数据应以服务器可以轻松解析的格式发送。这可以作为查询字符串,如:

"name=value&anothername="+encodeURIComponent(myVar)+"&so=on"

或以其他几种格式,包括JSON,SOAP等。

// setRequestHeader & urlencoded
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

处理服务器响应

当发送请求时,你提供了用于处理响应的JavaScript函数的名称。

httpRequest.onreadystatechange = nameOfTheFunction;  

首先,该函数需要检查请求的状态。如果状态的值为XMLHttpRequest.DONE(计算为4),这意味着已收到完整的服务器响应,可以继续处理它。

if (httpRequest.readyState === XMLHttpRequest.DONE) {
    // 一切都很好,收到回复
} else {
    // 还没准备好
}

readyState值的完整列表记录在 XMLHTTPRequest.readyState,如下:

  • 0 (未初始化)
  • 1 (正在加载)
  • 2 (加载完毕)
  • 3 (交互中)
  • 4 (完成)

接下来要检查的是HTTP服务器响应的响应代码。通常 大于等于200小于300或者等于304的状态吗,我们视为请求成功,如下:

if ((httpRequest.status >= 200 && httpRequest.status < 300) || httpRequest.status === 304) {
    // 完美!
} else {
    // 有一个问题的请求
    //例如响应可能包含404(找不到)
    //或500(内部服务器错误)响应代码
}  

现在,在检查请求的状态和响应的HTTP状态代码后,就可以根据需要对服务器发送给您的数据进行任何操作。有两个选项可以访问该数据:

  • http_request.responseText – 以文本字符串形式返回服务器响应
  • http_request.responseXML – 将响应作为XMLDocument对象返回,您可以使用JavaScript DOM函数遍历

请注意,上述步骤仅在使用异步请求(open()的第三个参数设置为true)时有效。如果使用同步请求,则不需要指定函数,可以在调用send()后立即访问服务器返回的数据,因为脚本将停止并等待服务器应答。

处理数据

分析并操作 responseXML属性

如果你使用 XMLHttpRequest 来获得一个远程的 XML 文档的内容,responseXML 属性将会是一个由 XML 文档解析而来的 DOM 对象,这很难被操作和分析。这里有五种主要的分析 XML 文档的方式:

  1. 使用 XPath 定位到文档的制定部分。
  2. 使用 JXON 将其转换成 JavaScript 对象树。
  3. 手工的 解析和序列化 XML 为字符串或对象。
  4. 使用 XMLSerializer 把 DOM 树序列化成字符串或文件。
  5. 如果你预先知道 XML 文档的内容,你可以使用 RegExp。如果你用 RegExp 扫描时受到换行符的影响,你也许想要删除所有的换行符。然而,这种方法是"最后手段",因为如果 XML 代码发生轻微变化,该方法将可能失败。

分析并操作 responseText属性

如果使用 XMLHttpRequest 从远端获取一个 HTML 页面,则所有 HTML 标记会以字符串的形式存放在responseText 属性里,这样就使得操作和解析这些标记变得困难。解析这些HTML标记主要有三种方式:

  1. 使用 XMLHttpRequest.responseXML 属性。
  2. 将内容通过 fragment.body.innerHTML 注入到一个 文档片段 中,并遍历 DOM 中的片段。
  3. 如果你预先知道 HTML 文档的内容,你可以使用 RegExp 。如果你用 RegExp 扫描时受到换行符的影响,你也许想要删除所有的换行符。 然而,这种方法是"最后手段",因为如果 HTML 代码发生轻微变化,该方法将可能失败。

处理二进制数据

尽管 XMLHttpRequest 一般用来发送和接收文本数据,但其实也可以发送和接受二进制内容。有许多经过良好测试的方法来强制使用 XMLHttpRequest 发送二进制数据。利用 XMLHttpRequest 的 .overrideMimeType() 方法是一个解决方案,虽然它并不是一个标准方法。

var oReq = new XMLHttpRequest();
oReq.open("GET", url, true);
// retrieve data unprocessed as a binary string
oReq.overrideMimeType("text/plain; charset=x-user-defined");

在 XMLHttpRequest Level 2 规范中新加入了 responseType 属性 ,使得发送和接收二进制数据变得更加容易。

var oReq = new XMLHttpRequest();

oReq.onload = function(e) {
  var arraybuffer = xhr.response; // not responseText
}
oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.send();

更多内容,参考下面附加部分。

监测进度

XMLHttpRequest 提供了各种在请求被处理期间发生的事件以供监听。这包括定期进度通知、 错误通知,等等。
支持 DOM 的 progress 事件监测之于 XMLHttpRequest 传输,遵循 Web API 进度事件规范 : 这些事件实现了 ProgressEvent 接口。

var req = new XMLHttpRequest();

req.addEventListener("progress", updateProgress, false);
req.addEventListener("load", transferComplete, false);
req.addEventListener("error", transferFailed, false);
req.addEventListener("abort", transferCanceled, false);

req.open();
// progress on transfers from the server to the client (downloads)
function updateProgress(evt) {
  if (evt.lengthComputable) {
    var percentComplete = evt.loaded / evt.total;
    ...
  } else {
    // Unable to compute progress information since the total size is unknown
  }
}

function transferComplete(evt) {
  alert("The transfer is complete.");
}

function transferFailed(evt) {
  alert("An error occurred while transferring the file.");
}

function transferCanceled(evt) {
  alert("The transfer has been canceled by the user.");
}

在上个例子中,progress 事件被指定由 updateProgress() 函数处理,并接收到传输的总字节数和已经传输的字节数,它们分别在事件对象的 total 和 loaded 属性里。但是如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。

progress 事件同时存在于下载和上传的传输。下载相关事件在 XMLHttpRequest 对象上被触发,就像上面的例子一样。上传相关事件在 XMLHttpRequest.upload 对象上被触发,像下面这样:

var req = new XMLHttpRequest();

req.upload.addEventListener("progress", updateProgress);
req.upload.addEventListener("load", transferComplete);
req.upload.addEventListener("error", transferFailed);
req.upload.addEventListener("abort", transferCanceled);

req.open();

使用 loadend 事件可以侦测到所有的三种加载结束条件(abort、load、error):

req.addEventListener("loadend", loadEnd, false);

function loadEnd(evt) {
  alert("The transfer finished (although we don't know if it succeeded or not).");
}

需要注意的是,没有方法可以确切的知道 loadend 事件接收到的信息是来自何种条件引起的操作终止;但是你可以在所有传输结束的时候使用这个事件处理。

提交表单和上传文件

XMLHttpRequest 的实例有两种方式提交表单:

  • 使用 AJAX
  • 使用 FormData API

第二种方式( 使用 FormData API )是最简单最快捷的,但是缺点是被收集的数据不是字符串形式的。
第一种方式反而是最复杂的但也是最灵活和最强大。

只使用 XMLHttpRequest

在大多数用例中,提交表单时即便不使用 FormData API 也不会要求其他的 API。唯一的例外情况是,如果你要上传一个或多个文件,你需要额外的 FileReader API。

一个 html <form> 可以用以下四种方式发送:

  • 使用 POST 方法,并设置 enctype 属性为 application/x-www-form-urlencoded (默认)
  • 使用 POST 方法,并设置 enctype 属性为 text/plain
  • 使用 POST 方法,并设置 enctype 属性为 multipart/form-data
  • 使用 GET 方法(这种情况下 enctype 属性会被忽略)

现在,我们提交一个表单,它里面有两个字段,分别被命名为 foo 和 baz。如果你用 POST 方法,那么服务器将会接收到一个字符串类似于下面三种情况之一,其中的区别依赖于你采用何种编码类型:

  • 方法: POST;
  • 编码类型: application/x-www-form-urlencoded (default):
Content-Type: application/x-www-form-urlencoded

foo=bar&baz=The+first+line.&#37;0D%0AThe+second+line.%0D%0A 
  • 方法: POST;
  • 编码类型: text/plain:
Content-Type: text/plain

foo=bar
baz=The first line.
The second line.
  • 方法: POST;
  • 编码类型: multipart/form-data:
Content-Type: multipart/form-data; boundary=---------------------------314911788813839

-----------------------------314911788813839
Content-Disposition: form-data; name="foo"

bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"

The first line.
The second line.

-----------------------------314911788813839--  

相反的,如果你用 GET 方法,一个像下面这样的字符串将被简单的附加到 URL:

?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.  

绕过缓存

一般地,如果缓存中有相应内容, XMLHttpRequest 会试图从缓存中读取内容。绕过缓存的方法见下述代码:

var req = new XMLHttpRequest();
req.open('GET', url, false);
req.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
req.send(null);

或者还有一个跨浏览器兼容的方法,就是给 URL 添加时间戳。请确保你酌情地添加了 "?" or "&" 。例如,将:

http://foo.com/bar.html

改为:

http://foo.com/bar.html?12345

因为本地缓存都是以 URL 作为索引的,这样就可以使每个请求都是唯一的,也就可以这样来绕开缓存。

你也可以用下面的方法自动更改缓存:

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

Fetch API

Fetch 提供了对 Request 和 Response (以及其他与网络请求有关的)对象的通用定义。使之今后可以被使用到更多地应用场景中:无论是service workers、Cache API、又或者是其他处理请求和响应的方式,甚至是任何一种需要你自己在程序中生成响应的方式。

它还提供了一种定义,将 CORS 和 HTTP 原生的头信息结合起来,取代了原来那种分离的定义。

发送请求或者获取资源,需要使用 GlobalFetch.fetch 方法。它在很多接口中都被实现了,比如 Window 和 WorkerGlobalScope。所以在各种环境中都可以用这个方法获取到资源。

fetch() 必须接受一个参数——资源的路径。无论请求成功与否,它都返回一个 promise 对象,resolve 对应请求的 Response。你也可以传一个可选的第二个参数—— init(参考 Request)。

一旦 Response 被返回,就有一些方法可以使用了,比如定义内容或者处理方法(参考 Body)。

你也可以通过 Request() 和 Response() 的构造函数直接创建请求和响应,但是我们不建议这么做。他们应该被用于创建其他 API 的结果(比如,service workers 中的 FetchEvent.respondWith)。

兼容性如下:

发起 fetch 请求

发起一个基本的 fetch 请求很简单。看如下代码:

var myImage = document.querySelector('img');

fetch('flowers.jpg')
.then(function(response) {
  return response.blob();
})
.then(function(myBlob) {
  var objectURL = URL.createObjectURL(myBlob);
  myImage.src = objectURL;
}); 

以上代码中,我们通过网络获取了一个图片,然后将它插入到一个 标签中。这个最简单的用法中,fetch() 接受了一个参数——请求的地址——然后返回一个包含 response(一个 Response 对象)的 promise 对象。

当然它只是一个 HTTP 响应,而不是真的图片。为了获取图片的内容,我们需要使用 blob() 方法(在 Body mixin 中定义,被 Request 和 Response 对象实现)。
接着,从 Blob 中获取 objectURL,之后再插入到 img 中。

最好使用符合内容安全策略 (CSP)的链接而不是使用直接指向资源地址的方式来进行Fetch的请求。

自定义请求的参数

fetch() 接受第二个可选参数,一个可以控制不同配置的 init 对象:

var myHeaders = new Headers();

var myInit = { method: 'GET',
               headers: myHeaders,
               mode: 'cors',
               cache: 'default' };

fetch('flowers.jpg',myInit)
.then(function(response) {
  return response.blob();
})
.then(function(myBlob) {
  var objectURL = URL.createObjectURL(myBlob);
  myImage.src = objectURL;
});

检测请求是否成功

如果遇到网络故障,fetch() promise 将会 reject,带上一个 TypeError 对象。虽然这个情况经常是遇到了权限问题或类似问题——比如 404 不是一个网络故障。想要精确的判断 fetch() 是否成功,需要包含 promise resolved 的情况,此时再判断 Response.ok 是不是为 true。类似一下代码:

fetch('flowers.jpg').then(function(response) {
  if(response.ok) {
    response.blob().then(function(myBlob) {
      var objectURL = URL.createObjectURL(myBlob);
      myImage.src = objectURL;
    });
  } else {
    console.log('Network response was not ok.');
  }
})
.catch(function(error) {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

自定义请求对象

除了传给 fetch() 一个资源的地址,你还可以通过使用 Request() 构造函数来创建一个 request 对象,然后再作为参数传给 fetch():

var myHeaders = new Headers();

var myInit = { method: 'GET',
               headers: myHeaders,
               mode: 'cors',
               cache: 'default' };

var myRequest = new Request('flowers.jpg',myInit);

fetch(myRequest,myInit)
.then(function(response) {
  return response.blob();
})
.then(function(myBlob) {
  var objectURL = URL.createObjectURL(myBlob);
  myImage.src = objectURL;
});

Request() 和 fetch() 接受同样的参数。你甚至可以传入一个已存在的 request 对象来创造一个拷贝:

var anotherRequest = new Request(myRequest,myInit);  

这个很有用,因为 request 和 response bodies 只能被使用一次(译者注:这里的意思是因为设计成了 stream 的方式,所以它们只能被读取一次)。创建一个拷贝就可以再次使用 request/response 了,当然也可以使用不同的 init 参数。

Headers

使用 Headers 的接口,你可以通过 Headers() 构造函数来创建一个你自己的 headers 对象。一个 headers 对象是一个简单的多名值对:

var content = "Hello World";
var myHeaders = new Headers();
myHeaders.append("Content-Type", "text/plain");
myHeaders.append("Content-Length", content.length.toString());
myHeaders.append("X-Custom-Header", "ProcessThisImmediately");

也可以传一个多维数组或者对象字面量:

myHeaders = new Headers({
  "Content-Type": "text/plain",
  "Content-Length": content.length.toString(),
  "X-Custom-Header": "ProcessThisImmediately",
});

他的内容可以被获取:

console.log(myHeaders.has("Content-Type")); // true
console.log(myHeaders.has("Set-Cookie")); // false
myHeaders.set("Content-Type", "text/html");
myHeaders.append("X-Custom-Header", "AnotherValue");
 
console.log(myHeaders.get("Content-Length")); // 11
console.log(myHeaders.getAll("X-Custom-Header")); // ["ProcessThisImmediately", "AnotherValue"]
 
myHeaders.delete("X-Custom-Header");
console.log(myHeaders.getAll("X-Custom-Header")); // [ ]

虽然一些操作只能在 ServiceWorkers 中使用,但是它提供了更方便的操作 Headers 的 API。

如果使用了一个不合法的HTTP Header属性名,那么Headers的方法通常都抛出 TypeError 异常。如果不小心写入了一个不可写的属性,也会抛出一个 TypeError 异常。除此以外的情况,失败了并不抛出异常。例如:

var myResponse = Response.error();
try {
  myResponse.headers.set("Origin", "http://mybank.com");
} catch(e) {
  console.log("Cannot pretend to be a bank!");
} 

最佳实践是在使用之前检查 content type 是否正确,比如:

fetch(myRequest).then(function(response) {
  if(response.headers.get("content-type") === "application/json") {
    return response.json().then(function(json) {
      // process your JSON further
    });
  } else {
    console.log("Oops, we haven't got JSON!");
  }
});

Guard

由于 Headers 可以在 request 请求中被发送或者在 response 请求中被接收,并且规定了哪些参数是可写的,Headers 对象有一个特殊的 guard 属性。这个属性没有暴露给 Web,但是它影响到哪些内容可以在 Headers 对象中被操作。

可能的值如下:

  • none:默认的
  • request:从 request 中获得的 headers(Request.headers)只读
  • request-no-cors:从不同域(Request.mode no-cors)的 request 中获得的 headers 只读
  • response:从 response 中获得的 headers(Response.headers)只读
  • immutable:在 ServiceWorkers 中最常用的,所有的 headers 都只读

Response 对象

如上述, Response 实例是在fentch()处理完promises之后返回的.

它的实例也可用通过JavaScript来创建, 但只有在ServiceWorkers中才真正有用,当使用respondWith()方法并提供了一个自定义的response来接受request时:

var myBody = new Blob();

addEventListener('fetch', function(event) {
  event.respondWith(new Response(myBody, {
    headers: { "Content-Type" : "text/plain" }
  });
});

Response() 构造方法接受两个可选参数—response的数据体和一个初始化对象(与Request()所接受的init参数类似.)

你会用到的最常见的response属性有:

  • Response.status — 整数(默认值为200) 为response的状态码.
  • Response.statusText — 字符串(默认值为"OK"),该值与HTTP状态码消息对应.
  • Response.ok — 如上所示, 该属性是来检查response的状态是否在200-299(包括200,299)这个范围内.该属性返回一个Boolean值.

Body

不管是请求还是响应都能够包含body对象. body也可以是以下任意类型的实例.

  • ArrayBuffer
  • ArrayBufferView (Uint8Array and friends)
  • Blob/File
  • string
  • URLSearchParams
  • FormData

Body 类定义了以下方法 (这些方法都被 Request 和Response所实现)以获取body内容. 这些方法都会返回一个被解析后的promise对象和数据.

  • arrayBuffer()
  • blob()
  • json()
  • text()
  • formData()

比起XHR来,这些方法让非文本化的数据使用起来更加简单。

请求体可以由传入body参数来进行设置:

var form = new FormData(document.getElementById('login-form'));
fetch("/login", {
  method: "POST",
  body: form
}) 

request 和response (也包括fetch() 方法)都会试着自动设置content type.如果没有设置Content-Type值,发送的请求也会自动设值.

附加内容

二进制数据的发送和接受

使用JavaScript类型数组接受二进制数据

可以通过设置一个XMLHttpRequest对象的responseType属性来改变一个从服务器上返回的响应的数据类型.可用的属性值为空字符串 (默认), "arraybuffer", "blob", "document", 和 "text". response属性的值会根据responseType属性的值的不同而不同, 可能会是一个 ArrayBuffer, Blob, Document, string,或者为NULL(如果请求未完成或失败)

下例读取了一个二进制图像文件,并且由该文件的二进制原生字节创建了一个8位无符号整数的数组.

var oReq = new XMLHttpRequest();
oReq.open("GET", "/myfile.png", true);
oReq.responseType = "arraybuffer";

oReq.onload = function (oEvent) {
  var arrayBuffer = oReq.response; // 注意:不是oReq.responseText
  if (arrayBuffer) {
    var byteArray = new Uint8Array(arrayBuffer);
    for (var i = 0; i < byteArray.byteLength; i++) {
      // 对数组中的每个字节进行操作
    }
  }
};

oReq.send(null); 

除了上面的方法,还可以使用 BlobBuilder API 直接将arraybuffer数据添加进一个Blob对象中, 由于该API还在试验阶段,所以需要加上特定的前缀:

var BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder || window.BlobBuilder;
var oReq = new XMLHttpRequest();
oReq.open("GET", "/myfile.png", true);
oReq.responseType = "arraybuffer";

oReq.onload = function(oEvent) {
  var blobBuilder = new BlobBuilder();
  blobBuilder.append(oReq.response);
  var blob = blobBuilder.getBlob("image/png");
};

oReq.send(); 

Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) 给XMLHttpRequest对象添加了一个私有的非标准属性mozResponseArrayBuffer,在Gecko 6 (Firefox 6 / Thunderbird 6 / SeaMonkey 2.3)之后不应该再使用了.

发送二进制数据

XMLHttpRequest对象的send方法已被增强,可以通过简单的传入一个ArrayBuffer, Blob, 或者 File对象来发送二进制数据.
下例创建了一个文本文件,并使用POST方法将该文件发送到了服务器上.你也可以使用文本文件之外的其他二进制数据类型:

var oReq = new XMLHttpRequest();
oReq.open("POST", url, true);
oReq.onload = function (oEvent) {
  // 上传完成后.
};

var bb = new BlobBuilder(); // 需要合适的前缀: window.MozBlobBuilder 或者 window.WebKitBlobBuilder
bb.append('abc123');

oReq.send(bb.getBlob('text/plain')); 

将类型数组作为二进制数据发送

你可以将JavaScript类型数组作为二进制数据发送出去.

var myArray = new ArrayBuffer(512);
var longInt8View = new Uint8Array(myArray);

for (var i=0; i< longInt8View.length; i++) {
  longInt8View[i] = i % 255;
}

var xhr = new XMLHttpRequest;
xhr.open("POST", url, false);
xhr.send(myArray);

附录

参考资料如下: