Flintx

JavaScript学习笔记06(Ajax 与 JSON)

参考 & 学习:

锋利的jQuery

廖老师的JavaScript教程

jQuery 教程 | 菜鸟教程

阮一峰的网络日志

JavaScript 参考文档

W3School JavaScript 教程

由衷感谢这些资料的提供者与撰写者!

Ajax 全称是 Asynchronous JavaScript and XML(异步的JavaScript与XML) , 它的出现开启了无刷新即可更新页面的时代,并使用户操作与服务器响应异步化(使之不需要相互等待),大大减轻了服务器与带宽的负担。

XMLHttpRequest对象

Ajax 技术的核心是 XMLHttpRequest 对象,用以发送异步请求、接收服务器响应与执行回调函数。

一个demo(learn-ajax-01.html ):

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
function ajax() {
var xmlHttpReq = null;
if (window.XMLHttpRequest) { // 兼容IE5 & IE6
xmlHttpReq = new XMLHttpRequest();
} else {
xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
// 第一个参数:请求方法
// 第二个参数:请求url
// 第三个参数:是否异步加载,默认为true。如果为false,则网页需要等待响应后才能继续加载
xmlHttpReq.open("GET", "test-01.php", true);
// 设置回调函数
xmlHttpReq.onreadystatechange = RequestCallback;
// 发送请求,GET请求不需要参数,POST请求需要把body部分以字符串或者FormData对象传进去
xmlHttpReq.send(null);
function RequestCallback() {
if (xmlHttpReq.readyState === 4) { // 请求加载完成(readyState值为4)
if (xmlHttpReq.status === 200) { // 响应成功(HTTP状态码为200)
document.getElementById("id1").innerHTML = xmlHttpReq.responseText;
}
}
}
}
</script>
</head>
<body>
<div id="id1" class="divclass"></div>
<button id="btn1" onclick="ajax();">Send</button>
</body>
</html>

同目录下的 test-01.php 文件:

1
2
3
<?php
echo "Hello, Ajax";
?>

点击send按钮,会有如下反应:

同源策略与跨域请求问题

什么是同源与跨域?

  • 同源:域名要相同(www.example.comexample.com不同),协议要相同(httphttps不同),端口号要相同(:80端口和:8088不同)。
  • 跨域:同源的反义词,一般有这几种情况。
    • 端口不同
    • 协议不同
    • 主域相同,子域不同(可通过插入iframe + 设置 document.domain 来解决)
    • 主域不相同
    • 还有一种情况,端口、协议均相同,但ip地址与域名两者之间也算跨域(哪怕二者是映射关系)

下面说一下 Ajax 的跨域请求问题。

浏览器出于安全考虑,会限制 JavaScript (即脚本层面)发起的跨域请求,这里做个示例:

  1. 在硬盘任意位置新建一个文件夹(我的是 9000/ ),在文件夹中新建一个php文件(例如 index.php ),内容如下:

    1
    2
    3
    <?php
    echo 'text in 127.0.0.1:9000';
    ?>
  2. 9000 文件夹下打开命令行,输入如下命令(启动PHP自带的Web Server,后面跟网络地址及监听的端口号,默认的网站根目录为当前目录):

    1
    2
    3
    4
    5
    $ php -S 127.0.0.1:9000
    PHP 5.6.30 Development Server started at Mon Sep 04 13:20:23 2017
    Listening on http://127.0.0.1:9000
    Document root is F:\...\9000
    Press Ctrl-C to quit.
  3. 在浏览器中访问 http://127.0.0.1:9000/index.phphttp://localhost:9000/index.php 会得到如下内容:

    1
    text in 127.0.0.1:9000
  4. 将之前 learn-ajax-01.html 中的 xmlHttpReq.open("GET", "test-01.php", true); 修改为 xmlHttpReq.open("GET", "http://localhost:9000/index.php", true); ,点击 Send 按钮无任何反应,打开 Chrome 开发者工具,发现如下错误:

    这就是跨域(http://localhost:80http://localhost:9000 ,不同端口域)请求错误。

值得一提的是,对跨域请求的限制是浏览器的行为,并非 JavaScript 本身的限制,不同浏览器限制策略可能有所不一。

一般来说,现代浏览器在平衡安全性和可用性的基础上,有所选择地为跨域请求开放后门。 例如 img script style 等标签,都允许跨域引用资源。但对 Ajax 等利用脚本进行不同域之间的数据通信技术来说,浏览器是坚决把同源策略执行到底的,如果不做任何处理直接发出跨域请求,将会报错。

要实现 Ajax 的跨域请求,有多种hack方法或者h5新标准引入的方法,但核心原则是:

是否允许 Ajax 跨域请求的控制权在服务器端。

即没有什么方法能绝对实现跨域请求,能否实现关键在于服务器端的控制。接下来要讲的几种跨域方法也大多需要服务器端的配合或环境条件支持。

一般实现 Ajax 跨域通信的方法主要有两类:

  1. Hack类:即通过具有src属性的HTML标签来传递消息,比较常见的有 JSONP 方法。
  2. H5新标准:HTML5引入了跨域资源共享(CORS)的事实标准,还提供了 window.postMessage() 方法让窗口之间来交换信息。

JSONP

从上面的例子中可以看出,由于浏览器的限制,我们不能用 Ajax 来直接请求不同域上的数据。

然而,引入不同域上的 js 文件却是可以的,JSONP 正是利用这个特性来实现。

JSONP 全称是 JSON with Padding ,它将返回的 JSON 数据包装成了以 JSON 数据为参数的回调函数。

为了介绍 JSONP,先将之前在 9000 文件夹下的 index.php 修改为:

1
2
3
4
<?php
$arr = array('name' => 'flintx', 'age' => '22', 'text' => 'text in 127.0.0.1:9000');
echo json_encode($arr);
?>

在浏览器访问 http://localhost:9000/index.php ,返回的信息如下:

1
{"name":"flintx","age":"22","text":"text in 127.0.0.1:9000"}

如果我们修改 index.php ,将返回的 JSON 数据包装成了以 JSON 数据为参数的回调函数:

1
2
3
4
5
<?php
$callback = $_GET['callback'];
$arr = array('name' => 'flintx', 'age' => '22', 'text' => 'text in 127.0.0.1:9000');
echo $callback . "(" . json_encode($arr) . ");";
?>

在浏览器访问 http://localhost:9000/index.php?callback=deal_data ,返回的信息如下:

1
deal_data({"name":"flintx","age":"22","text":"text in 127.0.0.1:9000"});

如果我们在之前的 learn-ajax-01.html 中增加一个 script 标签:

1
<script src="http://localhost:9000/index.php?callback=deal_data"></script>

那么就相当于在另一个域中,执行了以返回的 JSON 数据为参数的回调函数 deal_data({"name":"flintx","age":"22","text":"text in 127.0.0.1:9000"});

如果提前在 learn-ajax-01.html 中定义了 deal_data(json_data) 函数,就意味着完成了对跨域请求返回数据的调用。

这就是 JSONP 方式。

下面是完整的 demo:

  • localhost 默认端口下的 learn-ajax-01.html
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
function jsonp() {
var url = "http://localhost:9000/index.php?callback=deal_data";
var scriptDom = document.createElement("script");
scriptDom.src = url;
document.getElementsByTagName("body")[0].appendChild(scriptDom);
}
function deal_data(json_data) {
document.getElementById("id1").innerHTML = "name: " + json_data["name"] +
"; age: " + json_data["age"] +
"; text: " + json_data["text"];
}
</script>
</head>
<body>
<div id="id1" class="divclass"></div>
<button id="btn1" onclick="jsonp();">Send</button>
</body>
</html>
  • localhost 9000 端口下的 index.php
1
2
3
4
5
<?php
$callback = $_GET['callback'];
$arr = array('name' => 'flintx', 'age' => '22', 'text' => 'text in 127.0.0.1:9000');
echo $callback . "(" . json_encode($arr) . ");";
?>

运行效果:

事实上,插入 script 标签与定义回调函数的操作是可以通过一些库中的方法来自动完成的,例如 jQuery 中的 getJSON() 方法,下面的代码执行效果和之前纯 js 的版本一致:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.js"></script>
</head>
<body>
<div id="id1"></div>
<button id="btn1">Send</button>
<script>
$("#btn1").click(function() {
$.getJSON("http://localhost:9000/index.php?callback=?", function(json_data) {
var innerHtml = "name: " + json_data["name"] +
"; age: " + json_data["age"] +
"; text: " + json_data["text"];
$("#id1").html(innerHtml);
});
});
</script>
</body>
</html>

getJSON() 方法会自动判断请求是否跨域,不跨域调用普通的 Ajax 方法;跨域则使用 JSONP 方法,临时生成一个回调函数名字来替代 callback=?? 部分(一般是 jQuery32107324169903315518_1504591602038 之类的)。

除了 $.getJSON() 方法外,也可以直接用 $.ajax() 方法来完成 JSONP 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$("#btn1").click(function() {
// $.getJSON("http://localhost:9000/index.php?callback=?", function(json_data) {
// var innerHtml = "name: " + json_data["name"] +
// "; age: " + json_data["age"] +
// "; text: " + json_data["text"];
// $("#id1").html(innerHtml);
// });
// 与注释部分代码等价
$.ajax({
type: "GET", // 请求方式
url: "http://localhost:9000/index.php",
dataType: "jsonp", // 返回数据类型
crossDomain: true, // 是否跨域
success: function(json_data) { // 请求成功后的回调函数
var innerHtml = "name: " + json_data["name"] +
"; age: " + json_data["age"] +
"; text: " + json_data["text"];
$("#id1").html(innerHtml);
}
})
});

JSONP 是一种很巧妙的跨域请求方式,但和所有依赖于创建HTML标签的方式一样,JSONP只支持GET请求。即使用 $.ajax() 方法将 type 设置为其他请求方式,只要 dataTypejsonp ,还是会换回来。

而GET的数据是放在URL里的。 虽然 RFC 2616 没有提到限制到多少, 但提到了服务器可以对自己认为比较长的URL返回414状态码。一般来讲URL限长是在2000字符左右。所以JSONP方式对跨域请求较大数据比较无力。

CROS

CROS 是 HTML5 引入的新标准,关于它的介绍我直接引用 阮一峰老师的文章 :

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS 支持所有类型的 HTTP 请求,是跨域请求的王道方式。

一个Demo:

修改 9000 端口下的 index.php 文件为:

1
2
3
4
5
6
7
8
9
10
11
<?php
// 必要字段。值为请求时Origin字段的值,或者是一个*,表示接受任意域名的请求。
header('Access-Control-Allow-Origin: *');
// 必要字段。值为逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。
header('Access-Control-Allow-Methods: GET, POST');
// 可选字段。值为本次预检请求的有效期,单位为秒。
header('Access-Control-Max-Age: 1000');
$arr = array('name' => 'flintx', 'age' => '22', 'text' => 'text in 127.0.0.1:9000');
echo json_encode($arr);
?>

然后 80 端口下的 learn-ajax-01.html 文件用普通 Ajax 请求 9000 端口下的 index.php :

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.js"></script>
</head>
<body>
<div id="id1"></div>
<button id="btn1">Send</button>
<script>
$("#btn1").click(function() {
$.ajax({
type: "GET", // 请求方式
url: "http://localhost:9000/index.php",
dataType: "json", // 返回数据类型
crossDomain: true, // 是否跨域
success: function(json_data) { // 请求成功后的回调函数
var innerHtml = "name: " + json_data["name"] +
"; age: " + json_data["age"] +
"; text: " + json_data["text"];
$("#id1").html(innerHtml);
}
});
});
</script>
</body>
</html>

请求成功:

其他方法

jQuery 中的 Ajax

jQuery 对 Ajax 操作进行了封装,根据封装的程度可以将 jQuery 中的 Ajax 方法分为三个层次,分别是:

  • 第一层:$.ajax()

    $.ajax(options) 是 jQuery 中最底层的 Ajax 方法实现,参数是一个由 key-value 构成的 Object,参数详情见 jQuery.ajax() .

  • 第二层:$.get() , $.post() , load()

    这三个方法是最为常用的方法。

  • 第三层:$.getScript() , $.getJSON()

load()

先编写一个 learn-ajax-02.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="container1">
</div>
<button id="btn1">Load</button>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.js"></script>
<script>
$("#btn1").click(function() {
$("#container1").load("content.html");
});
</script>
</body>
</html>

以及用于加载的同目录下的 content.html

1
2
3
4
5
6
7
8
9
<div class="div1">
<p>张三</p>
</div>
<div class="div2">
<p>李四</p>
</div>
<div class="div3">
<p>王麻子</p>
</div>

执行效果:

事实上,还可以为参数 url 指定选择器,以加载指定的 html 内容,url 选择器格式为 url selector

修改 learn-ajax-02.htmlcontent.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="container1">
</div>
<button id="btn1">Load div1</button>
<button id="btn2">Load id-1</button>
<button id="btn3">Load divx</button>
<button id="btn4">Load all</button>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.js"></script>
<script>
$("#btn1").click(function() {
$("#container1").load("content.html .div1");
});
$("#btn2").click(function() {
$("#container1").load("content.html #id-1");
});
$("#btn3").click(function() {
$("#container1").load("content.html [class^=div]");
});
$("#btn4").click(function() {
$("#container1").load("content.html");
});
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
<div class="div1">
<p>class=div1的张三</p>
</div>
<div class="div2">
<p>class=div2的李四</p>
</div>
<div class="div3">
<p>class=div3的王麻子</p>
</div>
<div id="id-1">
<p>id=id-1的结衣酱</p>
</div>

执行效果:

load() 方法中,如果没有要发送的 data 数据(即第二个参数不是 data),则以 GET 方式来发出请求,反之则以 POST 来发出请求,即:

  • load(url, function(){...}); => GET 方式
  • load(url, {key1:value1, key2:value2, ...}, function(){...}); => POST 方式

其中 function(){…} 是回调函数,无论 Ajax 请求是否 成功, 只要请求完成,均会执行。

get() 和 post()

load() 方法一般用于加载静态资源,如果与服务器有数据交互,最好还是使用 $.get()$.post() 方法。

两个方法的参数均有4个:

  • url,请求的url;

  • data(可选),发送至服务器的 key-value 数据,GET 方式以拼接字符串的方式构造查询 url,POST 方式以消息内容实体的方式来发送 data;

  • callback(可选),回调函数,只有当请求成功时才执行;如果要分别执行请求成功、请求失败、请求完成等情景下不同的回调函数,可使用 jqXHR.success(), jqXHR.error()jqXHR.complete() (jQuery 1.5 ~ 1.8,3.0后已删除)或 jqXHR.done() (表示成功), jqXHR.fail() (表示错误)和 jqXHR.always() (jQuery 1.6+)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $.get("example.php", function () {
    alert("success");
    }).success(function () { // .done()
    alert("second success");
    }).error(function () { // .fail()
    alert("error");
    }).complete(function () { // .always()
    alert("complete");
    });
  • type(可选),返回的数据内容格式,包括 xml , html , json , script , text , …

demo: learn-ajax-03.html + test.php

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="#" id="form1">
<p><strong>Comment</strong></p>
<p>Nickname: <input type="text" name="name" id="name"> </p>
<p>Content: <textarea name="content" id="content" cols="30" rows="10"></textarea></p>
<p><input type="button" id="get" value="Send by GET"> &nbsp&nbsp&nbsp&nbsp
<input type="button" id="post" value="Send by POST"></p>
</form>
<div id="comment">
</div>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.js"></script>
<script>
$("#get").click(function () {
console.log($("#name").val());
$.get("test.php", {
name : $("#name").val(),
content : $("#content").val(),
}, function (data) {
$("#comment").append("<p>" + data.name + " says: " + data.content + "</p>");
}, "json");
});
$("#post").click(function () {
$.post("test.php", {
name : $("#name").val(),
content : $("#content").val(),
}, function (data) {
$("#comment").append("<p>" + data.name + " says: " + data.content + "</p>");
}, "json");
});
</script>
</body>
</html>
1
2
3
4
5
6
<?php
$name = $_GET['name']? $_GET['name'] : $_POST['name'];
$content = $_GET['content']? $_GET['content'] : $_POST['content'];
$arr = array('name' => $name, 'content' => $content . " by " . $_SERVER['REQUEST_METHOD']);
echo json_encode($arr);
?>

其他方法

文档 吧 : )