Ajax
一.概述
Web 程序最初的目的就是将信息(数据)放到公共的服务器,让所有网络用户都可以通过浏览器访问。
在次之前,我们可以通过以下几种方式让浏览器发出对服务端的请求,获取服务端的数据:
- 地址栏输入地址,回车,刷新
- 特定元素的href或src属性
- 标点提交
这些方式如果通过代码的方式进行编程,可以通过js直接发送网络请求,那么web的可能就会更多,至少不是“单机游戏”
AJAX(Asynchronous JavaScript and XML),最早出现在 2005 年的 Google Suggest,是在浏览器端进行网络编 程(发送请求、接收响应)的技术方案,它使我们可以通过 JavaScript 直接获取服务端最新的内容而不必重新加载 页面。让 Web 更能接近桌面应用的用户体验。
二.初步使用
使用Ajax可以类比我们访问页面的过程,Ajax其实就是一套API,核心类型:XMLHttpRequest。
为什么是XML,传输的不是JSON吗?历史原因,在JSON还没有流行起来之前,之前不便修改
//1.创建一个XMLHttpRequest类型对象,--安装浏览器(用户代理)
var xhr = new XMLHttpRequest()
//2.打开与一个网址之间的连接, --相当于在地址栏输入访问地址
xhr.open('GET','./time.php')
//3.通过连接发送一次请求 ---相当于回车或者点击访问发送请求
xhr.send(null)
//4.指定xhr状态变化事件处理函数, --相当于处理网页呈现后的操作
//因为客户端永远不知道服务端何时才能返回我们需要的响应
//所以Ajax API采用事件的机制(通知的感觉)
xhr.onreadystatechange = function(){
//通过xhr的readyState判断此次请求的响应是否接收完成
if(this.readyState ==4){
//通过xhr的reponseTexth获取到响应的响应体
console.log(this)
}
}
//onreadystatechange,并不是只在响应时触发,时状态改变就触发
2.1readyState
由于 readystatechange 事件是在 xhr 对象状态变化时触发(不单是在得到响应时),也就意味着这个事件会被 触发多次,所以我们有必要了解每一个状态值代表的含义:
readyState | 状态描述 | 说明 |
---|---|---|
0 | UNSENT | 代理(XHR)被创建,但尚未调用open()方法 |
1 | OPENED | open()方法已经被调用,建立了连接 |
2 | HEADERS_RECEIVED | send()方法已经被调用,并且已经可以获取状态行和响应头 |
3 | LOADING | 响应体下载中,responseText属性可能已经包含部分数据 |
4 | DONE | 响应体下载完成,可以直接使用responseText |
2.2 时间轴
var xhr =new XMLHttpRequest()
console.log(xhr.readyState) // 0 (初始化,请求代理对象)
xhr.open('GET','time.php')
console.log(xhr.readyState) //1 (open()方法已经调用,建立一个与服务端特定接口的连接)
xhr.send()
xhr.addEventListener('readystatechange',function(){
switch(this.readyState){
case 2: // 2 已经接收到了响应报文的响应头
//可以拿到头 console.log(this.getAllResponseHeaders())
console.log(this.getResponseHeader('server'))
//但是还没有拿到体
console.log(this.reponseText)
break;
case 3: //3 正在下载响应报文的响应体,有可能响应体为空,也有可能不完整
//在这里处理响应体不可靠
console.log(this.responseText)
break;
case 4: //4 ok(整个响应报文已经完整的下载下来了)
//这里处理响应体
console.log(this.responseText)
break;
}
})
-
通过理解每一个状态值的含义得出一个结论:一般我们都是在 readyState 值为 4 时,执行响应的后续逻辑。
-
xhr.onreadystatechange = function(){ if(this.readyState ===4){ //后续逻辑 } }
2,3遵循HTTP
本质上 XMLHttpRequest 就是 JavaScript 在 Web 平台中发送 HTTP 请求的手段,所以我们发送出去的请求任然是HTTP 请求,同样符合 HTTP 约定的格式:
//设置请求报文的请求行
xhr.open('GET','./time.php')
//设置请求头
xhr.setRequestHeader('Accept','text/plain')
//设置请求体
xhr.send(null)
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
// 获取响应状态码
console.log(this.status)
// 获取响应状态描述
console.log(this.statusText)
// 获取响应头信息
console.log(this.getResponseHeader('Content‐Type'))// 指定响应头
console.log(this.getAllResponseHeader()) // 全部响应头
// 获取响应体
console.log(this.responseText)// 文本形式
console.log(this.responseXML) // XML 形式,了解即可不用了
}
}
三.具体用法
3.1 GET 系列请求 (获取内容)
get
delete 一般应用于告诉服务器,从服务器上删除一点东西
head 只想获取响应头内容,告诉服务器响应主体内容不要了
options 试探性请求,发个请求给服务器,看看服务器能不能接收到,能不能返回
通常在一次 GET 请求过程中,参数传递都是通过 URL 地址中的 ? 参数传递。
var xhr = new XMLHttpRequest()
//GET请求传递参数通常使用的是问号传递
//这里可以在请求地址后面加上参数,从而传递数据到服务端
xhr.open('GET','./delete.php?id=1')
//一般在GET请求时无需设置响应体,可以传null或者干脆不传
xhr.send(null)
xhr.onreadystatechange=function(){
if(this.readyState===4){
console.log(this.responseText)
}
}
//一般情况下 URL 传递的都是参数性质的数据,而 POST 一般都是业务数据
3.2 POST请求(推送内容,多用于表单,需要推送一些内容)
post
put 和delete对应,一般是想让服务器把我传递的信息存储到服务器上(一般应用于文件和大的数据,图片等)
POST 请求过程中,都是采用请求体承载需要提交的数据。
var xhr = new XMLHttpRequest()
//open 方法的第一个参数的作用就是设置请求的method
xhr.open('POST', './add.php')
//设置请求头中的 Content‐Type 为 application/x‐www‐form‐urlencoded
//标识此次请求的请求体格式为 urlencoded 以便于服务端接收数据
xhr.setRequestHeader('Content‐Type', 'application/x‐www‐form‐urlencoded')
// 需要提交到服务端的数据可以通过 send 方法的参数传递
// 格式:key1=value1&key2=value2
xhr.send('key1=value1&key2=value2')
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
console.log(this.responseText)
}
}
3.3 同步和异步
关于同步和异步的概念在生活中有很多常见的场景
xhr.open() 方法第三个参数要求传入的是一个 bool 值,其作用就是设置此次请求是否采用异步方式执行,默认 为 true ,如果需要同步执行可以通过传递 false 实现:
console.log('before ajax')
var xhr = new XMLHttpRequest()
// 默认第三个参数为 true 意味着采用异步方式执行
xhr.open('GET', './time.php', true)
xhr.send(null)
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
// 这里的代码最后执行
console.log('request done')
}
}
console.log('after ajax')
如果采用同步方式执行,则代码会卡死在 xhr.send() 这一步:
console.log('before ajax')
var xhr = new XMLHttpRequest()
// 同步方式
xhr.open('GET', './time.php', false)
// 同步方式 执行需要 先注册事件再调用 send,否则 readystatechange 无法触发
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
// 这里的代码最后执行
console.log('request done')
}
}
xhr.send(null)
console.log('after ajax')
演示同步异步差异。
一定在发送请求 send() 之前注册 readystatechange (不管同步或者异步)
为了让这个事件可以更加可靠(一定触发),一定是先注册
了解同步模式即可,切记不要使用同步模式。
至此,我们已经大致了解了 AJAX 的基本 API 。
3.4 响应数据格式
提问:如果服务端返回一个复杂数据,该如何处理?
我们关心的就是服务端发出何种格式的数据,这种格式如何在客户端用Javascript解析
-
对于复杂的响应数据(如复杂的表格表单数据):
-
首先我们需要进行很多的DOM操作 //先创建行 //在创建列 //再将列添加到行 //再将行添加再tbody //拼接字符串 等等大量的操作 这里也引出:Vue,React等响应式数据框架的好处
3.5 处理响应数据渲染
模板引擎
模板引擎实际上就是一个 API,模板引擎有很多种,使用方式大同小异
目的为了可以更容易的将数据渲染到 HTML中
3.6 兼容方案
XMLHttpRequest 在老版本浏览器(IE5/6)中有兼容问题,可以通过另外一种方式代替
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
四.封装
4.1 AJAX请求封装
/**
* 发送一个 AJAX 请求
* @param {String} method 请求方法
* @param {String} url 请求地址
* @param {Object} params 请求参数
* @param {Function} done 请求完成过后需要做的事情(委托/回调)
*/
function ajax (method, url, params, done) {
// 统一转换为大写便于后续判断
method = method.toUpperCase()
// 对象形式的参数转换为 urlencoded 格式
var pairs = []
for (var key in params) {
pairs.push(key + '=' + params[key])
}
var querystring = pairs.join('&')
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
xhr.addEventListener('readystatechange', function () { if (this.readyState !== 4) return
// 尝试通过 JSON 格式解析响应体
try {
done(JSON.parse(this.responseText))
} catch (e) {
done(this.responseText)
}
})
// 如果是 GET 请求就设置 URL 地址 问号参数
if (method === 'GET') {
url += '?' + querystring
}
xhr.open(method, url)
// 如果是 POST 请求就设置请求体
var data = null if (method === 'POST') {
xhr.setRequestHeader('Content‐Type', 'application/x‐www‐form‐urlencoded') data = querystring
}
xhr.send(data)
}
ajax('get', './get.php', { id: 123 }, function (data) {
console.log(data)
})
ajax('post', './post.php', { foo: 'posted data' }, function (data) {
console.log(data)
})
4.2 jQuery中的AJAX
jQuery 中有一套专门针对 AJAX 的封装,功能十分完善,经常使用,需要着重注意。
$.ajax
$.ajax({
url: './get.php',
type: 'get',
dataType: 'json',
data: { id: 1 },
beforeSend: function (xhr) {
console.log('before send')
},
success: function (data) {
console.log(data)
},
error: function (err) {
console.log(err)
},
complete: function () {
console.log('request completed')
}
})
常用参数选项:
- url:请求地址
- type:请求方法,默认为 get
- dataType:服务端响应数据类型
- contentType:请求体内容类型,默认 application/x-www-form-urlencoded
- data:需要传递到服务端的数据,如果 GET 则通过 URL 传递,如果 POST 则通过请求体传递
- timeout:请求超时时间
- beforeSend:请求发起之前触发
- success:请求成功之后触发(响应状态码 200)
- error:请求失败触发
- complete:请求完成触发(不管成功与否都回进行的操作)
$.get
上面的$.ajax接近于底层API
jquery中还有高度封装的API
$.get('time.php',function(res){
console.log(res)
})
$post
$.post('time.php',function(res){
console.log(res)
})
4.3 其他
- $.getJSON() ----不管返回什么,都转成json格式
- $(selector).load() ---可实现局部加载
- $.getScript() ---
- 还有很多,查阅官方文档(也可查看别人翻译的中文文档)
五.跨域
跨域可以说跟ajax一点关系没有,也可以说与ajax有着绝对性的联系
5.1相关概念
同源策略是浏览器的一种安全策略,所谓同源是指,域名,协议,接口,完全相同,只有同源的地址才可以相互通过AJAX的方式请求。
同源或不同源说的是两个地址之间的关系,不同源地址之间请求我们称之为跨域请求
同源的页面数据能够相互请求
什么是同源?例如:http://www.example.com/detail.html 与一下地址对比
对比地址 | 是否同源 | 原因 |
---|---|---|
http://api.example.com/detail.html | 不同源 | 域名不同 |
https://www.example.com/detail.html | 不同源 | 协议不同 |
http://www.example.com:8080/detail.html | 不同源 | 端口不同 |
http://api.example.com:8080/detail.html | 不同源 | 域名,端口不同 |
https://api.example.com/detail.html | 不同源 | 协议、域名不同 |
https://www.example.com:8080/detail.html | 不同源 | 端口、协议不同 |
http://www.example.com/other.html | 同源 | 只是目录不同 |
5.2 解决方案
5.2.1 JSONP
JSON with Padding,(padding:边距,补充)(jsonp可理解为json的补充)是一种借助于 script 标签发送跨域请求的技巧。
其原理就是在客户端借助 script 标签请求服务端的一个动态网页(php 文件),服务端的这个动态网页返回一 段带有函数调用的 JavaScript 全局函数调用的脚本,将原本需要返回给客户端的数据传递进去。
以后绝大多数情况都是采用 JSONP 的手段完成不同源地址之间的跨域请求
客户端 http://www.zce.me/users-list.html
<script src="http://api.zce.me/users.php?callback=foo"></script>
服务端 http://api.zce.me/users.php?callback=foo 返回的结果
foo(['我', '是', '你', '原', '本', '需', '要', '的', '数', '据'])
总结一下:由于 XMLHttpRequest 无法发送不同源地址之间的跨域请求,所以我们必须要另寻他法,script 这种方 案就是我们最终选择的方式,我们把这种方式称之为 JSONP,如果你不了解原理,先记住怎么用,多用一段时间再 来看原理。
问题:
-
JSONP 需要服务端配合,服务端按照客户端的要求返回一段 JavaScript 调用客户端的函数
-
只能发送 GET 请求
注意:JSONP 用的是 script 标签,更 AJAX 提供的 XMLHttpRequest 没有任何关系!!!
jQuery 中使用 JSONP 就是将 dataType 设置为 jsonp 76
其他常见的 AJAX 封装 库: Axios
5.2.2 CORS
Cross Origin Resource Share ,跨域资源共享 (直接请求跨域资源)
但是因为版本比较新,存在一些兼容问题
// 允许远端访问
header('Access‐Control‐Allow‐Origin: *');
这种方案无需客户端作出任何变化(客户端不用改代码),只是在被请求的服务端响应的时候添加一个 Access- Control-Allow-Origin
的响应头,表示这个资源是否允许指定域请求。
六.ajax倒计时抢购案例
<script>
//new Date()获取客户端本地当前时间(不能拿它做重要依据,因为用户可以随时更改)
//倒计时抢购需要从服务器获取当前时间,而不是从本地获取 AJAX
//问题:时间差(从服务器把时间给客户端,到客户端获取到这个信息,中间经历的时间就是时间差,而时间差是不可避免的,我们应尽可能减少这个误差)
- 从响应头获取时间(AJAX异步)
-基于HEAD请求(只获取响应头信息)
let target = new Date('2019/09/14 18:00:00');
now = null ;
timer = null;
//我们不可能每一秒都从服务器请求最新时间,应该基于第一次获取的时间,用这个时间来记时
//=>从服务器获取时间,获取到时间后再做其他事情
function func(callback){
let xhr = new XMLHttpRequest;
xhr.open('HEAD','json/data.json');
xhr.onreadystatechange = function(){
if(!/^(2|3)d{2}$).test(xhr.status) return ;
if(xhr.readyState ===2){
now = new Date(xhr.getResponseHeader('Date'))
//如果传进来的是个函数,就把它执行
callback && callback();
}
}
xhr.send(null)
}
//=>开启倒计时模式
function computed (){
let spanTime = target -now;
if(spanTime <=0){
//=>到抢购点了:结束定时器
clearInterval(timer);
timer = null;
box.innerHTML = "开抢~";
return
}
//还剩多少个小时
let hours = Math.floor(spanTime / (60*60*1000) )
spanTime -= hours* 60*60*1000;
let minutes = Math.floor(spanTime / (60*1000));
spanTime -= minutes *60*1000
let seconds =Math.floor( spanTime /1000)
box.innerHTML = `距离抢购还剩:${hours}:${minutes}:${seconds}`
//=>每一次计算完,我们需要让now再原来的基础上增加一秒(第一次从服务器获取到时间,后期直接基于这个时间自己减即可,不要每隔一秒从服务器拿一次,服务器压力会特别大)
//每隔一秒,从服务器获取的时候,增加一
// 错误:now=new Date(now + 1000); 原因:里面编程字符串拼接了,now是字符串
now = new Date(now.getTime()+1000)
}
func(()=>{
//=>这个函数能执行,说明已经从服务器获取时间了
computed();
timer =setInterval(computed ,1000);
})
<script>
//除了完成这个功能外我们还应该收获到什么?再json传值,或者new Date()获取的时间的时候,很多时候都是字符串,这个时候我们不能直接使用它去做加法,因为可能会造成隐式转换,变成字符串拼接