Ajax和跨域请求和倒计时抢购案例

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 时间轴

graph LR 初始化-->建立连接 建立连接-->接收响应头 接收响应头-->响应体加载 响应体加载 -->加载完成
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,如果你不了解原理,先记住怎么用,多用一段时间再 来看原理。

问题:

  1. JSONP 需要服务端配合,服务端按照客户端的要求返回一段 JavaScript 调用客户端的函数

  2. 只能发送 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()获取的时间的时候,很多时候都是字符串,这个时候我们不能直接使用它去做加法,因为可能会造成隐式转换,变成字符串拼接
原文地址:https://www.cnblogs.com/JCDXH/p/11603416.html