美文网首页
前端网络请求

前端网络请求

作者: 悄敲 | 来源:发表于2019-04-06 22:15 被阅读0次

    前端网络请求的方式主要有 Ajax ,jQuery封装的 Ajax,fetch,axios、request 等开源库等。
    这里插一下http response status,指示一个 http 请求是否成功完成,具体分为五类:
    (1) informational responses,1XX,临时回应,表示客户端请继续:
    100 Continue: 代表目前一切正常,客户端应该继续等待请求完成,或者直接忽略如果请求已经完成。
    101 Switching Protocol: 对客户端发送的 Upgrade请求头的回应,请求者已要求服务器切换协议,服务器已确认并准备切换。
    102 Processing: 代表服务器已经收到请求并正在处理,不过此时仍没有响应返回。
    103 Early Hints: 主要用于 Link 头以允许客户端开始预加载资源,而此时服务器仍在准备一个响应。

    (2) successful responses:
    200 OK: 请求成功,但具体含义依请求方法而异,具体参考MDN
    201 Created: 请求成功,并且因该请求而生成了新的资源,通常是由于发送了 POST 或者 PUT 请求。
    202 Accepted: 请求已被收到,但是没有据此进行进一步操作,这表示无法返回一个异步响应来代表请求处理的结果,这通常出现在另一个进程或者服务器处理请求或者批处理中。
    203 Non-Authoritative Information: 非权威性信息,表示返回的元信息并非源于目标服务器,而来自本地或者第三方副本。
    204 No Content: 对于请求没有可返回的内容,即没有新文档,浏览器应该继续显示原来的文档。不过返回的响应头可能有用,客户端可以据此更新对应资源的缓存头。
    205 Reset Content:没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容。
    206 Partial Content: 因为客户端发送了一个带有Range头的请求,而Ranger头用于将下载分为多个流。
    还有207、208、226自己查资料吧;

    (3) redirects,3xx,表示请求的目标有变化,希望客户端进一步处理。
    300 Multiple Choice: 请求可以有多个响应,客户端自己挑一个。即客户端请求的资源可以在多个位置找到,这些位置已经在返回的文档内列出。如果服务器要提出优先选择,则应该在Location应答头指明。
    301 Moved Permanently: 这意味着被请求资源对应的URI已被永久改变了,旧地址A上的资源已被永久移除了而不可访问了。在可通过 Location 响应头给出新地址的URL。
    302 Found:这意味着被请求资源对应的URI已被暂时改变了,旧地址A的资源仍可以访问,重定向只是临时地从地址A跳转到地址B(详细请看这篇文章).
    (Note: 关于301、302状态码,参考这篇文章,它们都表示重定向,即浏览器在拿到服务器返回的这个状态码后,会自动跳转到一个新的URL(可从响应的Location头部中获取,用户看到的效果就是他输入的地址A变成了另一个地址B),尽量使用301跳转)

    303 See Other: 服务器返回该状态码指示客户端通过GET方法使用另一个URI去获取被请求的资源。
    304 Not Modified: 出于缓存目的,告诉客户端被请求的资源没被修改,可以继续使用对应的缓存。即客户端本地已经有缓存的版本,并且在 Request 中告诉了服务器,当服务器通过 if-modified-since 或 e-tag 发现对应资源没更新时,就会返回一个不包含响应体的304状态。 (这是在ajax请求中,唯一一个可以成功处理的重定向类状态码,其他3XX都不行)
    307 Temporary Redirect: 语义上与 302一致,只不过限制了客户端不能改变使用的请求方法类型,例如第一次请求使用的是 POST 方法,那么后续请求只能使用 POST 方法。
    308 Permanent Redirect: 语义上与 301 一致,不过限制了客户端不能改变使用的请求方法类型。

    (4) client errors:
    400 Bad Request: 由于语法错误,服务器无法理解请求。
    401 Unauthorized: 语义上应该是"unauthenticated",未经验证的,也有“未授权”的叫法,要求身份验证。对于需要登录的网页,服务器可能返回该响应(服务器:你要我提供服务,先告诉我你是谁)。
    403 Forbidden: 客户端无权限获取相应内容,即未得到授权因此服务器拒绝返回相应内容。与401不同,服务器知道客户端的身份,但不好意思,人家就是不想给你提供服务。
    404 Not Found: 这个太常见了,服务器找不到被请求的资源(亲,我这边没有你要的东西哦)。不过服务器有时候也会用404代替403返回,以此来向无权限的客户端隐藏对应资源的存在(我有,但骗你说没有,哈哈)。
    405 Method Not Allowed: 服务器知道请求使用的方法,但不允许。例如禁止 Delete 方法防止删除资源。不过 GET 和 HEAD 这两个方法不可以被禁止,即使用这两个方法去请求资源,不应该被返回 405.
    406 Not Acceptable: 无法根据客户端给定的规则找到内容。
    407 Proxy Authentication Required: 与401类似,但是需要通过代理来完成验证。
    408 Request Timeout: 某些服务器会对空闲的连接(即没有传输任务在进行)发送该响应,即使客户端之前并未发送任何请求(即该响应可以是服务器主动发送的),意味着服务器想要关闭该连接。
    还有还多4XX,这里就不介绍了。

    (5) servers eerors:
    500 Internal Server Error: 服务器内部错误。
    501 Not Implemented: 请求使用的方法类型不被服务器所支持而无法处理。不过服务器一般都支持 get 、head 方法。
    502 Bad Gateway: 服务器作为网关或代理,从上游服务器收到无效响应。
    503 Service Unavailable: 服务器目前无法使用(由于超载或停机维护),通常只是暂时状态。
    504 Gateway Timeout: 网关超时,服务器作为网关或者代理,没有及时从上游服务器收到请求。
    505 HTTP Version Not Supported: 请求使用的HTTP版本不被服务器所支持。

    1.原生 Ajax
    Ajax: asynchoronous javascript and xml,这种技术能够向服务器请求数据而无须重新加载整个页面,带来更好的用户体验,关于ajax的超详细介绍请移步这里
    本人画了下面这张示意图

    ajax请求.jpg
    说明:
    (1)发出请求(xhr.send())之前的语句都是同步执行,从 send 方法内部开始, 浏览器为将要发生的网络请求创建新的http请求线程(这个线程独立于js引擎线程)。网络请求异步被发送出去后,js 引擎并不会等待 ajax 发起的http请求收到结果, 而是直接顺序往下执行。
    (2)当ajax请求被服务器响应并且收到 response 后,浏览器事件触发线程捕获到了ajax的相应事件,并将 onreadystatechange (也可能是 onload 或者 onerror 等等)对应的事件处理程序添加到 任务队列 的末尾。
    (3)一次ajax请求, 并非所有的部分都是异步进行的,例如 "readyState==1"的 onreadystatechange 回调以及 onloadstart 回调就是同步执行的代码。
    xhr.send()刚开始执行,onloadstart 回调方法就被触发。
    使用原生 ajax 的代码如下:
     let xhr=new XMLHttpRequest();
        //状态监听
        xhr.onreadystatechange=function () {
            console.log(xhr.readyState);
            if(xhr.readyState===4){
                if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
                    console.log(xhr.responseText);
                }
            }
        }
        //异常处理
        xhr.onerror=function(){
            console.log("Ajax request failed.");
        }
    
        //设置相关事件回调方法
        // onloadstart 在开始执行 xhr.send()就会被触发,属于同步执行。
        xhr.onloadstart=function(){
            console.log('loadstart');
        }
        //超时处理
        xhr.ontimeout=function(){
            console.log("请求超时!")
        }
    
        //处理请求参数
        postData={"name1":"value1","name2":"value2"};
        postData=(function (value) {
            let dataString="";
            for(let key in value){
                dataString+=key+"="+value[key]+"&";
            }
            return dataString;
        })(postData);
    
        xhr.open('get','https://www.qq.com',true);
    
        //设置请求头,要在 open 与 send 之间
        // 推测是设置请求头的时候,xhr需要已经完成初始化
        xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
        //跨域携带cookie
        xhr.withCredentials=true;
    
        // 发出请求,之前的语句都是同步执行
        // 从send方法内部开始, 浏览器为将要发生的网络请求创建了新的http请求线程,
        xhr.send(postData);
    

    2.jQuery对 Ajax 的封装

    $.ajax({
        dataType: 'json', // 设置返回值类型
        contentType: 'application/json', // 设置参数类型
        headers: {'Content-Type','application/json'},// 设置请求头
        xhrFields: { withCredentials: true }, // 跨域携带 cookie
        data: JSON.stringify({a: [{b:1, a:1}]}), // 传递参数
        error:function(xhr,status){  // 错误处理
           console.log(xhr,status);
        },
        success: function (data,status) {  // 获取结果
           console.log(data,status);
        }
    })
    

    3.fetch
    Fetch API是一个用用于访问和操纵 HTTP 管道的强大的原生 API。
    这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch 还提供了单个逻辑位置来定义其他 HTTP 相关概念,例如 CORS 和 HTTP 的扩展。
    fetch是作为XMLHttpRequest的替代品出现的。
    优点:
    (1)符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里;
    (2)更好更方便的写法;
    (3)基于标准 Promise 实现,支持 async/await;
    (4)更加底层,提供的API丰富(request, response);
    (5)脱离了XHR,是ES规范里新的实现方式。
    使用fetch,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以仍然需要一个polyfill。
    一个基本的fetch请求如下:

    const options = {
        method: "POST", // 请求参数
        headers: { "Content-Type": "application/json"}, // 设置请求头
        body: JSON.stringify({name:'123'}), // 请求参数
        credentials: "same-origin", // cookie 设置
        mode: "cors", // 跨域
    }
    // fetch方法返回的是一个 promise
    // fetch(input, init)
    fetch('http://www.xxx.com', options)
      .then(function(response) {
        return response.json();
      })
      .then(function(myJson) {
        console.log(myJson); // 响应数据
      })
      .catch(function(err){
        console.log(err); // 异常处理
      })
    

    fetch polyfill 源码
    polyfill 主要对 fetch API 的 Headers, Request, Response, fetch 进行了封装。

    fetch polyfill代码结构
    (1) fetch
    (a)构造一个Promise对象并返回;
    (b)创建一个Request对象;
    (c)创建一个XMLHttpRequest对象(注意 原生 fetch 并未使用 XHR 对象);
    (d)取出Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给 xhr;
    (e)xhr onload后取出response的status、headers、body封装Response对象,调用resolve。
    // fetch 方法的封装
        function fetch(input, init) {
            return new Promise(function (resolve, reject) {
                let request=new Request(input,init);
                let xhr=new XMLHttpRequest();
                xhr.open(request.method,request.url,true);
    
                //响应获取完毕
                xhr.onload=function () {
                    let options={
                        status:xhr.status,
                        statusText:xhr.statusText,
                        headers:parseHeaders(xhr.getAllResponseHeaders() || '')
                    }
                    options.url='responseURL' in xhr? xhr.responseURL:options.headers.get('X-Request-URL');
                    let body='response' in xhr? xhr.response: xhr.responseText;
                    resolve(new Response(body,options));
                }
    
                //异常处理
                function abortXhr(){
                    xhr.abort();
                }
                // 1.请求失败,网络故障或请求被阻止才触发,
                //而收到服务器异常状态码如404,500不会触发
                xhr.onerror=function(){
                    reject(new TypeError('Network request failed.'));
                }
                //2.请求超时
                xhr.ontimeout=function(){
                    reject(new TypeError('请求超时'));
                }
                xhr.onabort=function(){
                    reject(new DOMException('Aborted','AbortError'));
                }
                //3.手动终止
                if(request.signal){
                    request.signal.addEventListener('abort',abortXhr);
                    xhr.onreadystatechange=function () {
                        if(xhr.readyState===4){
                            request.signal.removeEventListener('abort',abortXhr);
                        }
                    }
                }
                // forEach 的具体实现在 Headers.prototype 中
                request.headers.forEach(function (value, name) {
                    xhr.setRequestHeader(name,value);
                });
    
                xhr.send();
            })
        }
    
    

    (2) Request
    Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。

     // 2.Request对象封装
        function Request(input, options) {
            options=options || {}
            let body=options.body
    
            if(input instanceof Request){
                this.url=input.url
                this.method=input.method
                //...
            }
            else{
                this.url=String(input)
            }
            this.credentials=options.credentials || this.credentials || 'same-origin'
            if(options.headers || !this.headers){
                this.headers=new Headers(options.headers)
            }
            this.method=normalizeMethod(options.method || this.method || 'GET')
            this.mode=options.mode || this.mode || null
            this.signal=options.signal || this.signal
            this.referrer=null
            //...
            this._initBody(body)
        }
    

    (3) Headers

     // 3.Headers 封装
        function Headers(headers) {
            this.map={}
            // (1)传入的参数headers本身就是 Headers 的实例
            if(headers instanceof Headers){
                headers.forEach(function (value, name) {
                    this.append(name,value)
                },this)
            }
            //  (2)传入的参数headers是数组类型(二维)
            else if(Array.isArray(headers)){
                headers.forEach(function (header) {
                    this.append(header[0],header[1])
                },this)
            }
            // (3)传入的参数headers是普通对象
            else if(headers){
                Object.getOwnPropertyNames(headers).forEach(function (name) {
                    this.append(name,headers[name])
                })
            }
        }
    

    在 Headers 中维护了一个map对象,构造函数中可以传入Headers对象、数组、普通对象类型的header,并将所有的值维护到map中。
    fetch及上段代码中都有 headers.forEach,这个 forEach 方法的具体实现如下:

        Headers.prototype.forEach=function (callback, thisArg) {
            for(let name in this.map){
                if(this.map.hasOwnProperty(name)){
                    callback.call(thisArg,this.map[name],name,this)
                }
            }
        }
    

    可见header的遍历即其内部map的遍历。
    另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。

    (4) Response
    在 fetch中有对 Response 的操作:

      xhr.onload=function(){
        ···
        resolve(new Response(body,options))
      }
    
     function Response(bodyInit,options) {
            options=options || {}
            this.type='default'
            this.status=options.status===undefined? 200 : options.status
            this.ok=this.status>=200 && this.status<300
            this.statusText='statusText' in options?options.statusText:'OK'
            this.headers=new Headers(options.headers)
            this.url=options.url || ''
            // _initBody 是在 Body 函数中挂载到 Response.prototype 对象上的
            this._initBody(bodyInit)
        }
        Body.call(Response.prototype)
    

    可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。
    构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看Body函数:

    function Body() {
            this.bodyUsed=false;
            // 这就是 Response 中的 _initBody
            this._initBody=function (body) {
                //具体代码见后面
            }
    
            this.text=function () {
                // _bodyText...
            }
            
            this.json=function () {
                this.text().then(JSON.parse)
            }
            
            if(support.blob){
                this.blob=function () {
                    //_bodyBlob...
                }
            }
            if(support.formData){
                this.formData=function () {
                    //_bodyFormData...
                }
            }
            return this
        }
    

    Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将 _initBody 中得到的不同类型的返回值返回。
    这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。
    再来看看_initBody函数

     function Body() {
            this.bodyUsed=false;
            // 这就是 Response 中的 _initBody
            this._initBody=function (body) {
                if(!body){
                    this._bodyText=''
                }
                else if(typeof body==='string'){
                    this._bodyText=body
                }
                else if(support.blob && Blob.prototype.isPrototypeOf(body)){
                    this._bodyBlob=body
                }
                else if(support.formData && FormData.prototype.isPrototypeOf(body)){
                    this._bodyFormData=body
                }
                // else if ...
                else{
                    this._bodyText=body=Object.prototype.toString.call(body)
                }
            }
           // ...
    }
    

    可见,_initBody函数根据xhr.response的类型(Blob、FormData、String...),为不同的参数进行赋值,这些参数在Body方法中得到不同的应用.
    这里还有一点需要说明:Response相关的几个函数中都有类似下面的逻辑:

        var rejected = consumed(this)
        if (rejected) {
          return rejected
        }
    
       function consumed(body) {
          if (body.bodyUsed) {
              return Promise.reject(new TypeError('Already read'))
           }
           body.bodyUsed = true
       }
    

    每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError('Already read')。这也遵循了原生fetch的原则:
    因为 Responses 对象被设置为了 stream 的方式,所以它们只能被读取一次。
    fetch 的缺点
    (1)不能直接传递JavaScript对象作为参数;
    (2)需要自己判断返回值类型,并执行响应获取返回值的方法;
    (3)获取返回值方法只能调用一次,不能多次调用;
    (4)无法正常的捕获异常(服务器返回 400,500 错误码时并不会 reject,而是会将 resolve 的返回值的 ok 属性设置为 false;仅当网络故障时或请求被阻止时,才会标记为 reject。);
    (5)老版浏览器不会默认携带cookie;
    (6)不支持jsonp;
    (7)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费;
    (8)fetch没有办法原生监测请求的进度,而XHR可以.

    所以说半天, fetch并不是那么好用,偏底层,想要用的顺手还要自己封装,进行诸如 请求参数处理、cookie携带、异常处理、返回值处理等。

    4.jsonp
    fetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式。不过呢,多了解一种方式也不是坏事。jsonp 本身很简单,就是利用 script 标签的 src 属性不受同源策略约束,可进行跨域请求,不过服务端需要进行对应的设置才行。

    // 服务器端
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       response.setCharacterEncoding("UTF-8");
       response.setContentType("text/html;charset=UTF-8");
       //数据
       List<Student> studentList = getStudentList();
       JSONArray jsonArray = JSONArray.fromObject(studentList);
       String result = jsonArray.toString();
       //前端传过来的回调函数名称
       String callback = request.getParameter("callback");
       //用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
       result = callback + "(" + result + ")";
       response.getWriter().write(result);
     }
    

    上段代码就是服务器端对 jsonp 请求的简单处理(摘自
    下面就是对 jsonp 的封装。

      (function (window, document) {
            "use strict";
            let jsonp=function (url, data, callback) {
                // 1.将传入的data数据转化为url字符串形式
                // {id:1,name:'Jack'} => id=1&name=Jack
                let dataString=url.indexOf('?')==-1? '?': '&';
                for(let key in data){
                    dataString+=key+'='+data[key]+'&';
                }
    
                // 2.处理url中的回调函数
                // cbFuncName 回调函数的名字 :my_json_cb_ 名字的前缀 + 随机数(把小数点去掉)
                let cbFuncName='my_json_cb_'+
                        Math.random().toString().replace('.','');
                dataString+='callback='+cbFuncName;
    
                // 3.创建一个 script 标签
                let scriptEle=document.createElement('script');
                scriptEle.src=url+dataString;
    
                // 4.挂载回调函数(cbFuncName是变量,不能直接作为回调函数名)
                window[cbFuncName]=function (data) {
                    callback(data);
                    // 处理完回调函数的数据之后,删除 jsonp 的 script 标签
                    document.body.removeChild(scriptEle);
                }
    
                document.body.appendChild(scriptEle);
            }
            // 通过给全局window对象添加$jsonp属性,可在 此‘定义并立即调用匿名函数’执行后,
            // 在其他地方使用封装好的jsonp
            window.$jsonp=jsonp;
        })(window,document)
    
        //调用
        let url='https://www.xxx.com';
        let cb=function(data){console.log(data)}
        window.$jsonp(url,null,cb);
    

    5.axios
    Vue大佬尤雨溪推荐大家用axios替换JQuery封装的ajax.
    axios 是一个基于Promise,可用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征:
    1.从浏览器中创建 XMLHttpRequest
    2.支持 Promise API
    3.客户端支持防止CSRF
    4.提供了一些并发请求的接口(重要,方便了很多的操作)
    5.从 node.js 创建 http 请求
    6.拦截请求和响应
    7.转换请求和响应数据
    8.取消请求
    9.自动转换JSON数据
    axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,你值得拥有!
    axios使用示例:

    axios({
        method: 'post',
        url: '/user/12345',
        data: {
            firstName: 'Fred',
            lastName: 'Flintstone'
        }
    })
    .then(function (response) {
        console.log(response);
    })
    .catch(function (error) {
        console.log(error);
    });
    

    Reference
    1.全面分析前端的网络请求方式
    2.ajax知识体系大梳理
    3.ajax和axios、fetch的区别
    4.jsonp原理学习笔记

    相关文章

      网友评论

          本文标题:前端网络请求

          本文链接:https://www.haomeiwen.com/subject/hbazvqtx.html