请求 & 跨域

446 阅读19分钟

请求方式

在前后端分离项目中,前端请求后端接口得到后端数据,完成页面内容的渲染或功能状态的判断,已经成为常规操作。那么,关于前端如何请求后端接口获取并解析数据,主要有哪些方式呢:

  1. 刷新页面:最直接但是最体验最差的一种方式
  2. script标签的src属性(当使用JSONP(JSON with Padding)时,<script>标签的src属性可以用来获取跨域数据。JSONP只能发送GET请求)
  3. form表单:会触发页面跳转,无法实现页内重复请求
  4. ifream:解决jsonp只发get问题,可以发post, 但是比较消耗性能,且控制成本过高
  5. Ajax - 使用XMLHttpRequest对象进行异步请求,极大的提高了用户体验,实现了页内请求
  6. Fetch - Ajax的替代者,浏览器内置方法,封装了Promise机制,优化了异步问题
  7. jQuery - 一种前端框架,封装了数据请求模块,但体积较大
  8. axios、request等众多第三方开源库:对原生方法的二次封装,各有优劣势,百家争鸣

在请求过程中,一次HTTP请求对应一个页面

跨域

跨域就是向一个域发送请求,如果请求的域和当前的的域不是同一个域则称为跨域

浏览器的跨域问题主要指的是浏览器发起的请求。浏览器在发送请求时,会根据同源策略来判断该请求是否可以被发送到目标资源,如果不符合同源策略,则会被浏览器拦截,从而导致跨域问题

https(协议)://a.xxx.com(域名):8080(端口号)/flie/list(路径)

只要协议、域名、端口号,只要有任何一个不一样,都不是同一个域

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是浏览器会根据同源策略拦截响应结果。浏览器会根据同源策略拦截响应,阻止网页对响应结果的访问。

所以跨域问题更多地是指浏览器对跨域请求返回结果的处理限制。

同源策略

同源策略 是netScape(网景)提出的一个安全策略,它是浏览器最核心也最基本的安全功能。

不同源的客户端脚本在没有明确授权的情况下,浏览器禁止页面加载或执行与自身不同源的任何脚本。

浏览器在执行脚本前,会判断脚本是否与打开的网页是同源的。若请求跨域会在控制台报一个CORS异常,目的是为了保护本地数据不被JavaScript代码获取回来的数据所污染,因此拦截的是客户端发出请求的响应数据,即请求发送了,服务器响应了,但是响应的数据被浏览器拦截无法接收。

同源策略限制

  • DOM层面:限制了来⾃不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作
  • 网络层面: 限制了使用 XMLHttpRequest 向不同源的服务器发起 HTTP 请求 (ajax的响应被拦截)
  • 数据层面: 限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage数据

Cookie只和域名以及路径关联,如果是同个域名不同端口的源依然是共享同个域名下的Cookie的,

而LocalStorage则是以源为单位进行管理,相互独立,不同源之间无法相互访问LocalStorage中的内容。

Cookie 是浏览器根据请求的页面进行发送的,而与从哪个页面发送过去请求无关。

由于 Cookie 域的限制,浏览器在请求 q.com 的时候不会携带 a.com 的Cookie ,但是当 q.com 有一个向 a.com 发起的请求的时候,浏览器就会自动的在这个请求中带上 a.com 的 Cookie。

IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。不适合储存大量数据:Cookie 的大小不超过4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间

同源策略作用:

  1. 防止恶意网页可以获取其他网站的本地数据。
  2. 防止恶意网站iframe其他网站的时候,获取数据。
  3. 防止恶意网站在自已网站有访问其他网站的权利,以免通过cookie免登,拿到数据。

同源策略留有"后门"

  • 页面中的链接,重定向以及原生的表单提交是不会受到同源策略限制 ( 原生表单可直接跨域, ajax表单提交不行)
  • <script> <img> <link href='xxx's> 包含 src 、href属性的标签可以加载跨域资源

重定向在浏览器中是由服务器发出的一个响应,告诉浏览器将请求重定向到另一个 URL 上。因此,重定向并不会直接涉及到跨域问题,它只是告诉浏览器要去访问另一个 URL,由浏览器重新发起请求。在这个过程中,同源策略并不会生效。

为什么表单能发起跨域请求?

跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。

而跨域问题只是浏览器强加给js的规则。浏览器强制不允许js访问别的域,但是浏览器却没有限制它自己。

但是表单提交是浏览器行为,提交到另一个域名之后,原页面的脚本无法获取新页面中的内容,所以浏览器认为这是安全的可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF(跨站伪造请求攻击),因为请求毕竟是发出去了。

跨域的操作

  1. 跨域读: 同源策略是不允许这种事情发生的。

如果可以你就能使用 js 读取嵌入在 iframe 中的页面的 dom 元素,获取敏感信息。

  1. 跨域写: 大多数情况是不允许进行跨域写的,但只有在普通表单提交时(且没有 CSRF token 或者验证 referer 的情况下)同源策略不阻止这种操作,

但是对于 ajax 这种方式也是默认不允许的,容易出现 CSRF 请求攻击的情况。

  1. 跨域嵌入 这种方式是默认允许的,我们可以在一个源中通过 iframe 嵌入 另一个源的页面,但是如果想限制这种操作的话,可以通过设置 x-frame-options 响应头,确保页面没有被嵌入到别人的站点里面,从而避免点击劫持攻击。

X-Frame-Options是一个HTTP标头(header),用来告诉浏览器这个网页是否可以放在iFrame内。

实现跨域请求

CORS实现跨域

CORS(Cross-Origin Resource Sharing,跨域资源共享)允许当前域的资源被其他域的脚本请求访问的机制。

CORS 基于HTTP头的机制,其中的HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应,该机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行

cors 如何实现跨域

1.当使用XMLHttpRequest发送请求时,浏览器如果发现违反了同源策略就会自动加上一个请求头:origin

2.后端在接受到请求后确定响应后会在Response Headers中加入一个属性:Access-Control-Allow-Origin,值为发起请求的源地址,

3.浏览器得到响应会进行判断Access-Control-Allow-Origin的值是否和当前的地址相同,只有匹配成功后才进行响应处理。

这是因为浏览器在发送跨域请求时,会先发送一个预检请求(OPTIONS请求),服务端需要对该请求进行响应,告诉浏览器是否允许跨域请求。因此,跨域请求需要在服务端进行设置。

在请求头中设置

res.setHeader('Access-Control-Allow-Origin', 'www.xxx.com')

    // 前端
    //http://127.0.0.1:8888/cors.html
    var xhr = new XMLHttpRequest();
    xhr.onload = function(data){
    var _data = JSON.parse(data.target.responseText)
    for(key in _data){
        console.log('key: ' + key +' value: ' + _data[key]);
    }
    };
    xhr.open('POST','http://127.0.0.1:2333/cors',true);
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xhr.send();



    //后端 在接收响应后再请求头中添加  Access-Control-Allow-Origin 允访问的源地址
    //http://127.0.0.1:2333/cors
    app.post('/cors',(req,res) => {
      if(req.headers.origin){
          res.writeHead(200,{
            "Content-Type": "text/html; charset=UTF-8",
            "Access-Control-Allow-Origin": *
          });
          let people = {
          type : 'cors',
          name : 'weapon-x'
          }
          res.end(JSON.stringify(people));
      }
    })

优点:

  1. CORS 支持所有类型的 HTTP 请求。
  2. 开发者可以是使用普通的 XMLHttpRequest 发起请求和获取数据,比起 JSONP 有更好的错误处理

JSONP

JSONP(JSON with Padding)是一种跨域数据传输的解决方案,其原理是利用<script> 标签的跨域特性来实现数据传输,但是 JSONP 的跨域只支持 GET 请求。

在同源策略下,某个服务器下的页面是无法获取到该服务器以外的数据的,但<img src="转存失败,建议直接上传图片文件 " alt="转存失败,建议直接上传图片文件">、<iframe>、<script>、<link>、<video>、<audio> 等标签不受同源策略约束,这些标签可以通过src属性请求到其他服务器上的数据。

JSONP 原理:

利用<script>的 src 不受同源策略约束来跨域获取数据,将前端方法作为参数传递到服务器端,再由服务器端注入参数后返回,实现服务器端向客户端通信

JSONP的使用

  1. 客户端(浏览器)通过
<script src="http://example.com/data?callback=handleData"></script>
  1. 服务端接收到请求后,将数据包装在回调函数中返回给客户端 例如后端返回
    handleData({"name": "John", "age": 30})

JSONP请求的关键:服务端要在返回的数据外层包裹一个客户端已经定义好的函数,然后该函数会被客户端调用执行(否则浏览器会将返回的数据当作js代码执行)

优点:JSONP的兼容性很好,在古老的浏览器中都可以运行。

缺点:

1.JSONP 只支持 GET 请求,不支持 POST 请求等其他类型的 HTTP 请求

2.请求过程无法终止,导致弱网络下处理超时请求比较麻烦

3.无法捕获服务端返回的异常信息

nginx 服务器代理

Nginx 是一款高性能轻量级的Web服务器、反向代理服务器,由于它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用。

在 nginx 服务器的默认配置文件 nginx.conf 中添加

    http {
    	server {
        listen       8088;
        #listen			 [::]:8088;
        server_name  47.100.62.167;
        #root				 /usr/share/nginx;
        #server_name  localhost;
        #server_name  192.168.1.3;

        location / {
          root   html;
          index  index.html index.htm;
          # 解决history路由模式下导致的404错误。
          try_files $uri $uri/ /index.html;
        }

        # 为项目配置反向代理
        location /api {
          proxy_set_header X-Real-IP $remote_addr;
          # 需要代理的目标url
          proxy_pass http://111.229.37.167/api/;
          # 以下配置关闭重定向,让服务端看到用户的IP,而不是nginx服务器的IP
          proxy_redirect off;
          proxy_set_header X-Forwarded_For $proxy_add_x_forwarded_for;
          proxy_set_header Host $http_host;
          proxy_set_header X-Real_IP $remote_addr;
          proxy_set_header X-Nginx-Proxy true;
        }
    	}
    }

代理是在服务器和客户端之间假设的一层服务器,代理将接收客户端的请求并将它转发给服务器,然后将服务端的响应转发给客户端。

正向代理: 为客户端做代理,客户端可以根据正向代理访问到它本身无法访问到的服务器资源。

例如通过 VPN 访问外网,正向代理对我们是透明的,对服务端是非透明的,服务端并不知道自己收到的是来自代理的访问还是来自真实客户端的访问。

反向代理:为服务端服务,反向代理帮助服务器接收客户端的请求,帮助服务器做请求转发,负载均衡等。

例如当我们在外网访问百度的时候,其实会进行一个转发,代理到内网去,

以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

反向代理对服务端是透明的,对我们是非透明的,即我们并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。

总的来说,正向代理和反向代理都是代理服务器,它们的区别在于代理的对象不同。正向代理代理的是客户端,为客户端发送请求并将响应返回给客户端;反向代理代理的是服务器,为目标服务器接收请求并将响应返回给客户端。

iframe + postMessage 实现跨域通信

<iframe> 标签规定一个内联框架,用于在当前HTML页面嵌入另一个HTML页面

iframe 跨域的基本前提是: 1. 父页面和子页面必须处于不同的域名下 2. 子页面必须在父页面中通过src属性加载。如果子页面是通过JavaScript动态创建的iframe,则无法跨域访问。

在 iframe中通过postmessage实现跨域

该方法可以通过绑定window的message事件来监听发送跨文档消息传输内容。使用postMessage实现跨域的话原理就类似于jsonp,动态插入iframe标签,再从iframe里面拿回数据,所以用跨页面通信更加适合

postMessage()是基于消息事件机制来实现跨域通信,它隶属于消息窗体本身

发送消息:

    someWindow.postMessage(message, targetOrigin, [transfer]);

参数说明:

  • someWindow 窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
  • message 将要发送到其他 window的数据。意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化
  • targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)。不提供确切的目标将导致数据泄露等安全问题
  • transfer 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权

接收方监听消息:

    window.addEventListener("message", receiveMessage, false);

    function receiveMessage(event){
      let origin = event.origin || event.originalEvent.origin; 
      if (origin !== "http://aaa:8080")
        return;

      // ...
      console.log(event.data)
    }

event 中有如下几个核心的属性:

  • data 从其他 window 中传递过来的对象
  • origin 调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成
  • source 对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信

通过postMessage 实现跨域通信,

    // 发送方: 
    // 获取 token
    var token = result.data.token;

    // 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
    var iframe = document.createElement("iframe");
    iframe.src = "http://app1.com/localstorage.html";
    document.body.append(iframe);
    // 使用postMessage()方法将token传递给iframe
    setTimeout(function () {
      iframe.contentWindow.postMessage(token, "http://app1.com");
    }, 4000);
    setTimeout(function () {
      iframe.remove();
    }, 6000);



    // 在这个iframe所加载的HTML中绑定一个事件监听器,
    // 当事件被触发时,把接收到的token数据写入localStorage
    window.addEventListener('message', function (event) {
      localStorage.setItem('token', event.data)
    }, false);

Web Sockets

Web Sockets 可用于实现浏览器和服务端的全双工、双向通信。创建 WebSocket 对象后,首先会发送 http 请求到服务端;取得响应后,建立的连接会从 http 协议升级成 Web Sockets 协议,以支持双向通信。

与HTTP协议不同的是 Web Sockets 是一种基于帧的协议可以在一个TCP连接上发送多个消息帧,不受同源策略的影响,因此可以支持跨域通信。

    const socket = new WebSocket("ws://www.example.com/socket");// 必须是绝对路径
    socket.send(data);
    socket.onmessage = (event) => {
      console.log(event.data);
    };

图像 Ping

图像 Ping(Image Ping)技术是一种通过在浏览器中加载图像来进行网络通信的技术。鉴于加载图像不存在跨域问题,图像 Ping 技术通过动态的 Image 实例进行跨域通信。

它的基本原理是,通过创建一个Image对象并设置其src属性为目标URL,在浏览器中加载该图像,从而向目标服务器发送请求。与普通的图像请求不同的是,图像Ping请求不需要获取图像的内容

因此可以快速地向服务器发送数据,并且不会对页面的显示产生影响,但是它只能将请求通知给服务器,不能接受响应,最常用于收集点击信息,数据埋点。

<img src="http://example.com/ping?visit=1" style="display:none;">

通过在页面中插入一个隐藏的图像Ping请求,可以向服务器发送页面访问信息,从而实现统计页面访问量的功能。

JSONP

Jsonp(JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)获取资料,即跨域读取数据

JSONP起源

  1. 在服务器请求的过程中存在 跨域问题,当Ajax直接请求普通文件存在跨域无权限访问的问题,甭管你是静态页面、动态网页、web服务、WCF,只要是跨域请求,一律不准;
  1. 而在Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有"src"这个属性的标签都拥有跨域的能力,比如 <script>、<img>、<iframe>

  2. 于是可以判断,当前阶段如果想通过纯web端(ActiveX控件、服务端代理、属于未来的HTML5之Websocket等方式不算)跨域访问数据就只有一种可能, 那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理

  3. web客户端通过与调用脚本一模一样的方式,来调用跨域服务器上动态生成的js格式文件(一般以JSON为后缀)

    JSON的纯字符数据格式可以简洁的描述复杂数据,更妙的是JSON还被js原生支持

    服务器之所以要动态生成 JSON 文件,目的就在于把客户端需要的数据装入进去。

  4. 客户端在对JSON文件调用成功之后,也就获得了自己所需的数据

  5. 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作 JSONP。

该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

JSONP实现请求

`<script>` 元素用于嵌入或引用可执行脚本

JSONP通过script节点src调用跨域的请求,哪怕跨域js文件中的代码(符合web脚本安全策略的),web页面也是可以无条件执行的。

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <script type="text/javascript" src="http://remoteserver.com/remote.js"></script>
        <!-- 请求服务器,并返回 json "printData(data)" -->
        <!-- 把数据装到data里面去,返回到前端时就会自动调用 printData 函数(这个在前端自己写) -->
    </head>
    <body>

    </body>
    </html>

函数调用

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <script type="text/javascript" >
          var localHandler = function(data){
              alert('我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:' + data.result);
          };
          console.log('first')
          function showMe(str){
            console.log(str)
          }
        </script>
         <!--  在 引入的 js 中调用 localHandler({"result":"我是远程js带来的数据"}) -->
        <script type="text/javascript" src="./remote.js">
        </script>
        <script type="text/javascript" >
          console.log(123)
          showMe('haha')

        </script>
    </head>
    <body>

    </body>
    </html>

因此,JavaScript 代码在装载时的执行顺序也是根据脚本标签<script> 的出现顺序来确定的,按照script中的标签从上往下执行,当遇到错误时会返回,结束当前标签的执行,执行下一个script。

js的加载与执行

按块顺序加载

  • 按序加载
  • JavaScript的会阻塞DOM树的构建 所以将 js放在 body中
  • 后引入的 js文件可以调用先引入的js资源,下面的代码块可以访问上面代码块的资源,反之则不行。

首先会优先加载 function,但仅限于function xxx(){} 这种声明方式的,

var b = function(){}; 又或者匿名函数等是不会优先加载的


js 预处理后执行

(1)函数声明的提升优先于变量声明的提升;

(2)重复的var声明会被忽略掉,但是重复的function声明会覆盖前面的声明。

在预处理阶段,声明的变量的初始值是undefined, 采用function声明的函数的初始内容就是函数体的内容。

执行顺序

完成预处理之后,JavaScript代码会从上到下按顺序执行逻辑操作和函数的调用。

Ajax

Ajax就是在浏览器不重新加载网页的情况下,对页面的某部分进行更新。

Ajax(Asynchronous Javascript And XML),即是异步的JavaScript和XML,一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

Ajax其实就是浏览器与服务器之间的一种异步通信方式。

异步的JavaScript

异步地向服务器发送请求,在等待响应的过程中,不阻塞当前页面,在这种情况下,浏览器可以做自己的事情。直到成功获取响应后,浏览器才开始处理响应数据。

Ajax原理: 通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。

ajax实现过程

实现 Ajax 异步交互需要服务器逻辑进行配合,需要完成以下步骤:

  • 创建 Ajax 的核心对象 XMLHttpRequest 对象
  • 通过 XMLHttpRequest 对象的 open() 方法与服务端建立连接
  • 构建请求所需的数据内容,并通过 XMLHttpRequest 对象的 send() 方法发送给服务器端
  • 通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端你的通信状态
  • 接受并处理服务端向客户端响应的数据结果
  • 将处理结果更新到 HTML 页面中

创建XMLHttpRequest对象

通过XMLHttpRequest() 构造函数用于初始化一个 XMLHttpRequest 实例对象

    const xhr = new XMLHttpResuest()

与服务器建立链接

调用XMLHttpRequest对象的 open() 方法与服务器建立连接

    xhr.open(method,url,[async],[user],[password])

参数说明:

  • method:请求方式: post,get,delete,put,head,options,trace,connect
  • url:请求路径
  • async:是否为异步请求,默认为 true
  • user: 可选的用户名用于认证用途;默认为 null
  • password: 可选的密码用于认证用途,默认为 null

如果 Ajax 请求是异步的则这个方法发送请求后就会返回继续执行,如果Ajax请求是同步的,那么请求必须等待响应后才会返回执行后续操作。

给服务端发送数据

xhr.send([body])

body: 在 XHR 请求中要发送的数据体,如果不传递数据则为 null

GET 请求

如果使用GET请求发送数据的时候,需要注意如下:

  • 将请求数据添加到open()方法中的url地址中
  • 发送请求数据中的send()方法中参数设置为null
    var xhr = new XMLHttpRequest();
    xhr.open('get','https://www.baidu.com/getUserInfo?name=AAA&age=18');
    xhr.send();
    xhr.onreadystatechange = function() {
      if(xhr.readyState ==4 && xhr.status==200) {
        console.log('请求成功');
      }
    }

POST 请求

    var xhr = new XMLHttpRequest();
    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xhr.open('post','https://www.baidu.com/getUserInfo');
    xhr.send({name: 'haha', age: 18});
    xhr.onreadystatechange = function() {
      if(xhr.readyState ==4 && xhr.status==200) {
        console.log('请求成功');
      }
    }

监听服务器端的通信状态

通过 XMLHttpRequest 对象的 onreadystatechange 事件监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState 。

ajax请求的5种状态值(readyState)

  • 0 - (未初始化)还没有调用send()方法
  • 1 - (启动)已调用send()方法,正在发送请求
  • 2 - (发送)send()方法执行完成,已经接收到全部响应内容
  • 3 - (接收)已经接收部分响应数据,正在解析响应内容
  • 4 - (完成)已经接收全部响应数据,可以在客户端调用了

ajax请求返回的 状态码(status)

  • 200:服务器成功返回网页
  • 404:请求的网页不存在
  • 503:服务器暂时不可用

只要 readyState 属性值一变化,就会触发一次 readystatechange 事件,接受并处理服务端向客户端响应的数据结果。

XMLHttpRequest.responseText 属性为字符串形式,用于接收服务器端的响应结果

responseXML 以 XML 返回响应,返回 XML 文档对象,使用 DOM 来提取要显示的值。

例如:

    const request = new XMLHttpRequest()
    request.onreadystatechange = function(e){
        if(request.readyState === 4){ // 整个请求过程完毕
            if(request.status >= 200 && request.status <= 300){
                console.log(request.responseText) // 服务端返回的结果
            }else if(request.status >=400){
                console.log("错误信息:" + request.status)
            }
        }
    }
    request.open('POST','http://xxxx')
    request.send()

Ajax 实现表单提交

    <!DOCTYPE html>
    <html lang="en" dir="ltr">
      <head>
        <meta charset="utf-8">
        <title></title>
      </head>
      <body>
        <div id="div1">
          用户:<input type="text" id="user" /><br>
          密码:<input type="password" id="pass" /><br>
          文件:<input type="file" id="f1" /><br>
          <input id="btn1" type="button" value="提交">
        </div>
      </body>
      <script>
      let oBtn=document.querySelector('#btn1');
      oBtn.onclick=function (){
        let formdata=new FormData();

        formdata.append('username', document.querySelector('#user').value);
        formdata.append('password', document.querySelector('#pass').value);
        formdata.append('f1', document.querySelector('#f1').files[0]);

        //
        let xhr=new XMLHttpRequest();

        xhr.open('post', 'http://localhost:8080/', true);
        xhr.send(formdata);

        xhr.onreadystatechange=function (){
          if(xhr.readyState==4){
            if(xhr.status==200){
              alert('成功');
            }else{
              alert('失败');
            }
          }
        };
      };
      </script>
    </html>

封装 Ajax 请求

    function ajax(options){
      const xhr = new XMLHttpRequest()
      const options = options || {}
      //  请求路径
      options.type = (options.type || 'GET').toUpperCase()
      
      options.dataType = options.dataType || 'json'  
      
      const params = options.data   // 请求参数

      if(optionn.type === 'GET'){
        xhr.open('GET',options.url + '?' + params, true)
        xhr.send()
      } else{
        xhr.open(options.type,option.url,true)
        xhr.send(params)
      }

      xhr.onreadystatechange = function(){
        if(xhr.readyState === 4) {
          let status = xhr.status 
          if(status >=200 && status <=300){
            //  请求成功 并返回
            options.success && options.success(xhr.responseText,xhr.responseXML)
          } else {
            options.fail && options.fail(status)
          }
        }
      }
    }


    ajax({
        type: 'post',
        dataType: 'json',
        data: {},
        url: 'https://xxxx',
        success: function(text,xml){ //请求成功后的回调函数
            console.log(text)
        },
        fail: function(status){ //请求失败后的回调函数
            console.log(status)
        }
    })

Ajax的缺点

  • 本是针对MVC架构,不符合前端MVVM的浪潮
  • 基于原生的XHR开发
  • 配置和调用方式混乱

axios

axios是使用 promise 封装的 ajax,它内部有两个拦截器,分别是 request、response 拦截器。

  • 请求拦截器的作用是请求发送之前的操作,例如在每个请求体上加入 token
  • 响应拦截器的作用是接收到响应后的操作,例如登录失效后需要重新登录跳转到登录页

axios的特点

  • 由浏览器端发起请求,在浏览器中创建XHR
  • 支持promise API
  • 监听请求和返回
  • 更好的格式化,自动将数据转换为json数据
  • 安全性更高,可抵御CSRF攻击

请求方式

常用的请求方式: get 、post、put、patch、delete

其中 get 、put 返回 promise 对象,使用 promise 方法

patch 方法用来更新局部资源

put 适用于更新数据,但必须提供完整的资源对象

    //  get请求  传参params:
    axios.get('apiURL', {
        param: {
            id: 1
        }
        // param 中的的键值对最终会 ? 的形式,拼接到请求的链接上,发送到服务器。
    }).then(res => {
        console.log(res);
    })
    .catch( error => {
        console.log(error)
    }

    //  post请求  传参data:
    axios.post('apiURL',{
            user: '小新',
            age: 18}
    ).then( res => {
        console.log(res);
    })
    .catch( error => {
        console.log(error)
    }

    // delete  传参params:
    axios.delete('apiURL', {
        params: {
            id: 1
        },
        timeout: 1000
    })

axios 传参方式

  • params 中的参数是通过地址栏传参,一般用于get请求
  • data 是添加到请求体(body)中的, 一般用于post请求
  • get请求只能传query参数,query参数都是拼在请求地址上的
  • post可以传body和query两种形式的参数

axios相关配置

创建请求时可以用的配置选项。只有 url 是必需的。如果没有指定 method,请求将默认使用 GET 方法。

  • method: 请求方式 默认为 'get'
  • url:请求路径
  • baseURL:自动加到url前
  • proxy:用于配置代理
  • transformRequest:允许在服务器发送请求之前修改请求数据

您可以指定默认配置,它将作用于每个请求。

全局 axios 默认值

    axios.defaults.baseURL = 'https://api.example.com';
    axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
    axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 

自定义实例默认值

    // 创建实例时配置默认值
    const instance = axios.create({
      baseURL: 'https://api.example.com'
    });

    // 创建实例后修改默认值
    instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

配置的优先级

配置将会按优先级进行合并。

优先级:请求的 config 参数 > 实例的 defaults 属性 > lib/defaults.js 库中设置的默认值

    // 使用库提供的默认配置创建实例
    // 此时超时配置的默认值是 `0`
    const instance = axios.create();

    // 重写库的超时默认值
    // 现在,所有使用此实例的请求都将等待2.5秒,然后才会超时
    instance.defaults.timeout = 2500;

    // 重写此请求的超时时间,因为该请求需要很长时间
    instance.get('/longRequest', {
      timeout: 5000
    });

请求拦截器

请求拦截器: 在请求或响应被 then 或 catch 处理前拦截它们。

请求触发的过程: 请求拦截器、发送请求、响应拦截器、相应回调

    // 创建axios实例
    const service = axios.create({
      baseURL: '/ebc/kylinsite',
      // 请求超时时间
      timeout: 10 * 60 * 1000,
    })


    // 添加请求拦截器
    service.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      return config;
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error);
    });


    // 添加响应拦截器
    service.interceptors.response.use(function (response) {
      // 2xx 范围内的状态码都会触发该函数。
      // 对响应数据做点什么
      return response;
    }, function (error) {
      // 超出 2xx 范围的状态码都会触发该函数。
      // 对响应错误做点什么
      return Promise.reject(error);
    });

应用场景

  • Token 身份认证
  • Loading 效果
  • etc…

请求拦截器执行顺序

    axios.interceptors.request.use(config => {
      console.log(`请求拦截1`);
      return config;
    });

    axios.interceptors.request.use(config => {
      // 在发送请求之前做些什么 
      console.log(`请求拦截2`);
      return config;
    });

    // 添加响应拦截器 
    axios.interceptors.response.use(response => {
      // 对响应数据做点什么 
      console.log(`成功的响应拦截1`);
      return response.data;
    });

    // 添加响应拦截器 
    axios.interceptors.response.use(response => {
      // 对响应数据做点什么 
      console.log(`成功的响应拦截2`);
      return response;
    });

    // 发送请求 
    axios.get('/posts')
      .then(response => {
        console.log('成功了');
      }) 

当请求发出后执行的结果为

    console.log("请求拦截2");
    console.log("请求拦截1");
    console.log("成功的响应拦截1");
    console.log("成功的响应拦截2");
    console.log("成功了");

请求拦截:axios的请求拦截会先执行最后指定的回调函数先执行,依次向前面执行。

响应拦截:axios的响应拦截会先执行最先指定的回调函数先执行,依次向后面执行

通过分析axios源码:

    //  request方法就是axios发送请求的核心方法
    Axios.prototype.request = function request(config) {
      /*eslint no-param-reassign:0*/
      // Allow for axios('example/url'[, config]) a la fetch API
      if (typeof config === 'string') {
        config = arguments[1] || {};
        config.url = arguments[0];
      } else {
        config = config || {};
      }
      // 1. 代码开始构建了一个config配置对象,用于第一次执行Promise返回一个成功的Promise
      // 合并配置
      config = mergeConfig(this.defaults, config);
      // 添加method配置, 默认为get
      config.method = config.method ? config.method.toLowerCase() : 'get';

      /*
      2. 最核心的数组chain 
        创建用于保存请求/响应拦截函数的数组
        数组的中间放发送请求的函数
        数组的左边放请求拦截器函数(成功/失败)
        数组的右边放响应拦截器函数
      */
      var chain = [dispatchRequest, undefined];
      var promise = Promise.resolve(config);

      // 3. 往数组中添加请求拦截函数 unshift() 
      //    后添加的请求拦截器保存在数组的前面
      this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
        // 从头部插入 请求拦截函数
        chain.unshift(interceptor.fulfilled, interceptor.rejected);
      });

      // 4. 往数组中添加响应拦截函数 push() 
      // 后添加的响应拦截器保存在数组的后面
      this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
        // 从尾部放入 响应拦截函数
        chain.push(interceptor.fulfilled, interceptor.rejected);
      });
      

      // 5. Promise遍历执行,每次从chain中取出两个 函数执行(一个成功回调,一个失败回调)
      // 通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器
      while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
      }

      // 6. 返回promise
      // 返回用来指定我们的onResolved和onRejected的promise
      return promise;
    };

请求拦截 后响应的在前, 响应拦截 后响应的在后

request方法是axios发送请求的核心方法,将发送请求分为了几个部分(请求拦截器,发送请求,响应拦截器,响应回调),通过Promise的链式调用将这些部分有机地结合了起来,这样就构成了发送请求拿到数据处理的全部过程。

axios原理

  1. 使用axios.create创建单独的实例,或直接使用axios实例
  2. 对于axios调用进入到request()中进行处理
  3. 执行请求拦截器
  4. 请求数据转换器,将传入的数据进行处理,比如JSON.stringify(data)
  5. 执行适配器,判断是浏览器端还是node端,以执行不同的方法
  6. 响应数据转换器,对服务器端的数据进行处理,比如JSON.parse(data)
  7. 执行响应拦截器,对服务器端数据进行处理,比如token失效跳转到登录页
  8. 返回数据

fetch

fetch 是http请求数据的方式,使用js脚本发出网络请求

fetch() 是一个全局方法,提供一种简单,合理的方式跨网络获取资源。它的请求基于 Promise 但不使用回调函数

  • fetch() 可以接收跨域 cookies;也可以使用 fetch() 建立起跨域会话。
  • 但是 fetch 默认不带cookies,传递cookie时,必须在header参数内加上 credentials:'include',才会像 xhr 将当前cookie 带有请求中。

优点:

  • 采用模块化思想,将输入、输出、状态跟踪分离
  • 基于promise,返回一个promise对象

缺点:

  • 过于底层,有很多状态码没有进行封装
  • 无法阻断请求
  • 兼容性差
  • 无法检测请求进度

fetch使用方式

fetch用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。

只有当遇到网络错误时,fetch() 返回的 promise 才会被 reject,并传回 TypeError。

成功的 fetch() 不仅要检查 promise 被 resolve,还要检查 Response.ok 属性为 true。因为HTTP 404 状态并不被认为是网络错误。

    fetch(url,options).then((response)=>{
    //处理http响应
    },(error)=>{
    //处理错误

参数形式:

url :是发送网络请求的地址。

options:发送请求参数,

  • body - http请求参数
  • mode - 指定请求模式。默认值为cros:允许跨域;same-origin:只允许同源请求;no-cros:只限于get、post和head,并且只能使用有限的几个简单标头。
  • cache - 用户指定缓存。
  • method - 请求方法,默认GET
  • signal - 用于取消 fetch
  • headers - http请求头设置
  • keepalive - 用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。
  • credentials - cookie设置,默认omit,忽略不带cookie,same-origin同源请求带cookie,inclue无论跨域还是同源都会带cookie。

response对象

当fech请求成功后会 响应response 对象

  • status - http状态码,范围在100-599之间
  • statusText - 服务器返回状态文字描述
  • ok - 返回布尔值,如果状态码2开头的,则true,反之false
  • headers - 响应头
  • body - 响应体。响应体内的数据,根据类型各自处理。
  • type - 返回请求类型。
  • redirected - 返回布尔值,表示是否发生过跳转。

response 对象根据服务器返回的不同类型数据,提供了不同的读取方法:

  1. response.text() -- 得到文本字符串
  2. response.json() - 得到 json 对象
  3. response.blob() - 得到二进制 blob 对象
  4. response.formData() - 得到 fromData 表单对象
  5. response.arrayBuffer() - 得到二进制 arrayBuffer 对象

因为fetch 是基于 promise封装的,所以需要等待异步操作结束后才能获取到response对象

    const res = await fetch(`/auth/password/check?phoneNumber=${value}`, 
      { method: 'Get' }
     )

    const resData = await res.json()    // 通过 json() 解析接口返回值


    // 或者

    fetch(`/auth/password/check?phoneNumber=${value}`, { method: 'Get' })
      .then((data)=>{
        return data.json()
      })
      .then((res)=>{
        // 获取请求 返回的数据
        console.log(res)
      })

reaponse.clone

stream 对象只能读取一次,读取完就没了,这意味着,上边的五种读取方法,只能使用一个,否则会报错。

因此 response 对象提供了 clone() 方法,创建 respons 对象副本,实现多次读取。如下:将一张图片,读取两次:

    const response1 = await fetch('flowers.jpg');
    const response2 = response1.clone();

    const myBlob1 = await response1.blob();
    const myBlob2 = await response2.blob();

    image1.src = URL.createObjectURL(myBlob1);
    image2.src = URL.createObjectURL(myBlob2);

fetch请求

请求方式不同,传值方式也不同。xhr 会分别处理 get 和 post 数据传输,还有请求头设置,同样 fetch 也需要分别处理。

get请求query传参

  • params 中的参数是通过地址栏传参,一般用于get请求
  • get请求只能传query参数,query参数都是拼在请求地址上的

只需要在url中加入传输数据,options中加入请求方式

    const res = await fetch(`/auth/password/check?phoneNumber=${value}`, 
      { method: 'Get' }
     )

    const resData = await res.json()    // 通过 json() 解析接口返回值


    // 或者

    fetch(`/auth/password/check?phoneNumber=${value}`, { method: 'Get' })
      .then((data)=>{
        return data.json()
      })
      .then((res)=>{
        // 获取请求 返回的数据
        console.log(res)
      })

参数查看方式

参数

post请求body传参

post 请求传参方式:

  • data 是添加到请求体(body)中的, 一般用于post请求
  • post可以传body和query两种形式的参数

使用 post 发送请求时,需要设置请求头、请求数据等

  • 如果是提交json数据时,需要把json转换成字符串。
  • 如果提交的是表单数据,使用 formData转化下。
const params = {
  phoneNumber: form.value.userPhone,
  code: form.value.msgCode,
  principal: form.value.principal,
  password: form.value.userPassword,
}


const res = await fetch('/auth/login', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(params),
})

form 表单提交

  • form 的提交行为需要通过type=submit实现
  • form 中的method 属性不指定时, form 默认的提交方式为 get请求。
  • 表单提交时表单内容会被浏览器封装为HTTP请求报,里面包含了所有表单元素的name属性值和value属性的值,形式为name=value。
  • form 表单的提交后会有默认行为,会跳转刷新到action 的页面
  • 当一个form 表单内部,所有的input 中只有一个 type=‘text’ 的时候,enter 键会有默认的提交行为(注意前提条件)
  • 阻止默认行为可以在submit事件时return false
const submitForm = new FormData()
  submitForm.append('client_id', process.env.VUE_APP_CLIENT_ID)
  submitForm.append('grant_type', 'authorization_code')
  submitForm.append('redirect_uri', `${process.env.VUE_APP_DOMAIN_URL}/getAuth`)
  submitForm.append('code_verifier', localStorage.getItem('LOGIN_UNI_STRING')) // 获取发起授权请求的 code_verifier)
  submitForm.append('code', code)
  const res = await fetch(`${process.env.VUE_APP_AUTH_API_ROOT}/auth/oauth2/token`, {
    method: 'POST',
    headers:{
   		// 注意这里不要设置 Content-Type 请求头,否则会导致错误
    },
    body: submitForm
  })

form表单为何没有跨域限制

跨域问题只是浏览器强加给js的规则。浏览器强制不允许js访问别的域,但是浏览器却没有限制它自己。

比如说img标签可以加载任何域的图片,script可以加载任何域的js。

form 提交是浏览器行为,提交到另一个域名之后,原页面的脚本无法获取新页面中的内容。所以浏览器认为这是安全的。

而 Ajax是可以读取响应内容的,因此浏览器不能允许你这样做。如果你细心的话你会发现,其实请求已经发送出去了,你只是拿不到响应而已。

所以浏览器这个策略的本质是,一个域名的 JS ,在未经允许的情况下,不得读取另一个域名的内容。但浏览器并不阻止你向另一个域名发送请求。

总结

Fetch、ajax与axios的关系

Ajax是一种web数据交互的方式,它可以使页面在不重新加载的情况下请求数据并进行局部更新,它内部使用XHR来进行异步请求

Ajax在使用XHR发起异步请求时得到的是XML格式的数据,如果想要JSON格式,需要进行额外的转换;Ajax本身针对的是MVC框架,不符合现在的MVVM架构;Ajax有回调地狱问题;Ajax的配置复杂

而Fetch是XHR的代替品,它基于Promise实现的,并且不使用回调函数,它采用模块化结构设计,并使用数据流进行传输,对于大文件和网速慢的情况非常友好。

但是Fetch不会对请求和响应进行监听;不能阻断请求;过于底层,对一些状态码没有封装;兼容性差。

axios是基于Promise对XHR进行封装,它内部封装了两个拦截器,分别是请求拦截器和响应拦截器。请求拦截器用于在请求发出之前进行一些操作,比如:设置请求体,携带Cookie、token等;响应拦截器用于在得到响应后进行一些操作,比如:登录失效后跳转到登录页面重新登录。axios有get、post、put、patch、delete等方法。axios可以对请求和响应进行监听;返回Promise对象,可以使用Promise的API;返回JSON格式的数据;由浏览器发起请求;安全性更高,可以抵御CSRF攻击。

Fetch、ajax与axios的区别

传统的ajax利用的是HMLHttpRequest这个对象,和后端进行交互。而 JQury、ajax 是对原生XHR的封装,多请求间有嵌套的话就会出现回调地狱的问题。

axios使用 promise封装XHR,解决了回调地狱的问题。

而 Fetch没有使用XHR,使用的是promise

Fetch和Ajax比有什么优点

Fetch使用的是promise,方便使用异步,没有回调地狱的问题。

参考

  1. 【什么是同源策略?】:blog.csdn.net/qq_39465116…
  2. 【实现跨域的几种方法】:blog.csdn.net/weixin_4335…
  3. 【ajax、fetch和axios的比较】:blog.csdn.net/qq_43539854…