AJAX与Fetch API -----从es6入门javascript

AJAX与Fetch API

AJAX与Fetch API

AJAX与XMLHttpRequest

AJAX这个技术名词的出现是在十年前(2005),其中内容包含XML、JavaScript中的XMLHttpRequest物件、HTML与CSS等等技术的整合应用方式,这个名词并非专指某项特定技术或是软体, Google在时所推出的Gmail服务与地图服务,获得很大的成功,当时这个技术名词以此作为主要的案例说明。实际上这个技术的实现是在更早之前(2000年之前),一开始是微软公司实作了一个Outlook与邮件伺服器沟通的介面,后来把它整合到IE5浏览器上。在2006年XMLHttpRequest正式被列入W3C标准中,现在已被所有的浏览器品牌与新版本所支援。

所谓的AJAX技术在JavaScript中,即是以XMLHttpRequest物件(简称为XHR)为主要核心的实作。正如它的名称,它是用于客户端对伺服器端送出httpRequest(要求)的物件,使用的资料格式是XML格式(但后来JSON格式才是最为流行的资料格式)。流程即是建立一个XMLHttpRequest(XHR)物件,打开网址然后送出要求,成功时最后由回调函式处理伺服器传回的Response(回应)。整体的流程是很简单的,但经过这么长久的使用时间(11年),它在使用上产生不少令人头痛的问题,例如:

  • API设计得过于高阶(简单),所有的输出与输入、状态,都只能与这个XHR物件沟通取得,进程状态是用事件来追踪。
  • XHR是使用以事件为基础(event-based)的模组来进行异步程式设计。
  • 跨网站的HTTP要求(cross-site HTTP request)与CORS(Cross-Origin Resource Sharing)不易实作。
  • 对非文字类型的资料处理上不易实作。
  • 除错不易。

XHR在使用上都是像下面的范例程式码这样,其实你可以把它视作一种事件处理的结构,大小事都是依靠XHR物件来作,语法中并没有把每件事情分隔得很清楚,而比较像是挤在一团:

function reqListener() {
  const data = JSON.parse(this.responseText);
  console.log(data)
}

function reqError(err) {
  console.log('Fetch Error :-S', err)
}

const oReq = new XMLHttpRequest();
oReq.onload = reqListener
oReq.onerror = reqError
oReq.open('get', './sample.json', true)
oReq.send()

在今天浏览器功能相当强大,以及网站应用功能复杂的时代,XHR早就已经不敷使用,它在架构上明显的有太多的问题,尤其在很多功能的应用情况,程式码会显得复杂且不易维护。除非你是有一定要使用原生JavaScript的强迫症,要不然现在要作AJAX功能时,程式设计师并不会使用原生XHR物件来撰写,大部份时候会使用外部函式库。因为一个AJAX的程式,并不是单纯到只有对XHR的要求与回应这么简单,例如你可能会对伺服器要求一份资料,当成功得到资料后,后面还有需要进一步的资料处理流程,这样就会涉及到异步程式的执行结构,原生XHR并没有提供可用的方式,它只是单纯的作与伺服器互动那件事而已。

XHR Level 2(第2级)

XHR并不是没有在努力进步,在约5年前已经有制定XHR的第2级新标准,但它仍然与原有XHR向下相容,所以整体的模型架构并没有重大的改变,只是针对问题加以补强或是扩充。目前XHR第2级在9成以上的浏览器品牌新版本都已经支援全部的功能,除了IE系列要版本10之后才支援,以及Opera Mini浏览器完全不支援,还有一小部份功能在不同浏览器上实作细节会有所不同。XHR第2级(5年前)相较于原有的XHR(11年前)多加了以下的功能,这也是现在我们已经可以使用到的XHR的新特性:

  • 指定回应格式
  • 上传文件与blob格式档案
  • 使用FormData传送表单
  • 跨来源资源共享(CORS)
  • 监视传输的进程

不过,XHR第2级的新标准并没有太引人注目的新功能,它比较像是解决长期以来的一些严重问题的补强版本。而且,XHR在原本上的设计就是这样,常被批评的是它的语法结构不论在使用与设定都相当的零乱。补强或扩充都还是跳脱不了基本的结构,现今是HTM5、CSS3与ES6的时代,有许多新的技术正在蓬勃发展,说句实在话,就是XHR技术已经旧掉了,当时的设计不符合现在时代需求了,这也无关对或错。

jQuery的

外部函式库例如jQuery很早就看到XHR物件中在使用的问题,使用像jQuery的函式库来撰写AJAX相关功能,不光是在解决不同浏览器中的不相容问题,或是提供简化语法这么简单而已。jQuery它扩充了原有的XHR物件为jqXHR物件,并加入类似于Promise的介面与Deferred Object(延迟物件)的设计。

为何要加入类似Promise的介面?可以看看它的说明中,是为了什么而加入的?

这些方法可以使用一个以上的函式传入参数,当$.ajax()的要求结束时呼叫它们。这可以让你在单一个(request)要求中指定多个callbacks(回调),甚至可以在要求完成后指定多个callbacks(回调)。~译自jQuery官网The jqXHR Object

原生的XHR根本就没有这种结构,Promise的结构基本上除了是一种异步程式设计的架构,它也可以包含错误处理的流程。简单地来说,jQuery的目标并不是只是简化语法或浏览器相容性而已,它的目标是要" 取代以原生XHR物件的AJAX语法结构 ",虽然本质上它仍然是以XHR物件为基础。

jQuery作得相当成功,十分受到程式设计师们的欢迎,它的语法结构相当清楚,可阅读性与设定弹性相当高,常让人忘了原来的XHR是有多不好使用。在Promise还没那么流行的前些年,里面就已经有类似概念的设计。加上现在的新版本(3.0)已经支援正式的Promise标准,说实在没什么理由不去使用它。以下是jQuery中ajax方法的范例:

// 使用 $.ajax() 方法
$.ajax({

    // 進行要求的網址(URL)
    url: './sample.json',

    // 要送出的資料 (會被自動轉成查詢字串)
    data: {
        id: 'a001'
    },

    // 要使用的要求method(方法),POST 或 GET
    type: 'GET',

    // 資料的類型
    dataType : 'json',
})
  // 要求成功時要執行的程式碼
  // 回應會被傳遞到回調函式的參數
  .done(function( json ) {
     $( '<h1>' ).text( json.title ).appendTo( 'body' );
     $( '<div class=\'content\'>').html( json.html ).appendTo( 'body' );
  })
  // 要求失敗時要執行的程式碼
  // 狀態碼會被傳遞到回調函式的參數
  .fail(function( xhr, status, errorThrown ) {
    console.log( '出現錯誤,無法完成!' )
    console.log( 'Error: ' + errorThrown )
    console.log( 'Status: ' + status )
    console.dir( xhr )
  })
  // 不論成功或失敗都會執行的回調函式
  .always(function( xhr, status ) {
    console.log( '要求已完成!' )
  })

把原生的XHR用Promise包裹住,的确是一个好作法,有很多其他的函式库也是使用类似的作法,例如axiosSuperAgent,相较于jQuery的多功能,这些是专门只使用于AJAX的函式库,另外这些函式库也可以用在伺服器端,它们也是有一定的使用族群。

Fetch是近年来号称要取代XHR的新技术标准,它是一个HTML5的API,并非来自ECMAScript标准。在浏览器支援性的部份,首先由Mozilla与Google公司在2015年3月发布Fetch实作消息,目前也只有Firefox与Chrome、Opera浏览器在新版本中原生支援,微软的新浏览器Edge也在最近宣布支援( 新闻连结 )(应该是Edge 14),其他浏览器目前可以使用polyfill来作填充,提供暂时解决相容性的方案。另外,Fetch同样要使用ES6 Promise的新特性,这代表如果浏览器没有Promise特性,一样也需要使用es6-promise来作填充。

Fetch并不是一个单纯的XHR扩充加强版或改进版本,它是一个用不同角度思考的设计,虽然是可以作类似的事情。此外,Fetch还是基于Promise语法结构的,而且它的设计足够低阶,这表示它可以依照实际需求进行更多弹性设定。相对于XHR的功能来说,Fetch已经有足够的相对功能来取代它,但Fetch并不仅于此,它还提供更多有效率与更多扩充性的作法。

注: 英文中fetch/费曲/ 有"获取"、"取回"的意思。它与get、bring单词是近义词。

Fetch基本语法

fetch()方法是一个位于全域window物件的方法,它会被用来执行送出Request(要求)的工作,如果成功得到回应的话,它会回传一个带有Response(回应)物件的已实现Promise物件。fetch()的语法结构完全是Promise的语法,十分清楚容易阅读,也很类似于jQuery的语法:

fetch('http://abc.com/', {method: 'get'})
.then(function(response) {
    //處理 response
}).catch(function(err) {
    // Error :(
})

但要注意的是fetch在只要在伺服器有回应的情况下,都会回传已实现的Promise物件状态(只要不是网路连线问题,或是伺服器失连等等),在这其中也会包含状态码为错误码(404, 500...)的情况,所以在使用时你还需要加一下检查:

fetch(request).then(response => {
  //ok 代表狀態碼在範圍 200-299
  if (!response.ok) throw new Error(response.statusText)
  return response.json()
}).catch(function(err) {
    // Error :(
})

或是先用另一个处理状态码的函式,使用Promise.resolvePromise.reject将回应的情况包装为回传不同状态的Promise物件,然后再下个then方法再处理:

function processStatus(response) {
    // 狀態 "0" 是處理本地檔案 (例如Cordova/Phonegap等等)
    if (response.status === 200 || response.status === 0) {
        return Promise.resolve(response)
    } else {
        return Promise.reject(new Error(response.statusText))
    }
}

fetch(request)
    .then(processStatus)
    .then()
    .catch()

Fetch相关介面说明

fetch的核心由GlobalFetch、Request、Response与Headers四个介面(物件)与一个Body(Mixin混合)。概略的内容说明如下:

  • GlobalFetch: 提供全域的fetch方法
  • Request:要求,其中包含methodurlheaderscontextbody等等属性与clone方法
  • Response:回应,其中包含headersokstatusstatusTexttypebody等等属性与clone方法
  • Headers: 执行Request与Response中所包含的headers的各种动作,例如取回、增加、移除、检查等等。设计这个介面的原因有一部份是为了安全性。
  • Body: 同时在Request与Response中均有实作,里面有包含主体内容的资料,是一种ReadableStream(可读取串流)的物件

注: Mixin(混合)样式是一种将多个物件(或类别)中会共同使用(分享)的方法或属性另外用一个物件或介面整合包装起来,然后让其他的物件(或类别)来使用其中的方法或属性的设计样式。Mixins(混合)的主要目的是要让程式码功能可以达到重覆使用,但并不是透过类别继承的方式。

与XHR有很大的明显不同,每个XHR物件都是一个独立的物件,麻烦的是每次作不同的Request(要求)或要处理不同的Response(回应)时,就得再重新实体化一个新的XHR物件,然后再设定一次。而fetch中则是可以明确地设定不同的Request(要求)或Response(回应)物件,提供了更多细部设定的弹性,而且这些设定过的物件都可以重覆再使用。Request(要求)物件可以直接作为fetch方法的传入参数,例如下面的这个范例:

const req = new Request(URL, {method: 'GET', cache: 'reload'})

fetch(req).then(function(response) {
  //處理 response
}).catch(function(err) {
    // Error :(
})

另一个很棒的功能是你可以用原有的Request(要求)物件,当作其他要新增的Request(要求)物件的基本样版,像下面范例中的新的postReq即是把原有的req物件的method改为'POST'而已,这可以很容易重覆使用原先设定好的Request(要求)物件。

const postReq = new Request(req, {method: 'POST'})

以下摘要Request(要求)物件中可以包含的属性值,可以看到设定值相当多,可以依使用情况设定到很细:

  • 方法:GETPOSTPUTDELETEHEAD
  • url: 要求的网址。
  • headers: 与要求相关的Headers物件。
  • referrer - no-referrerclient或一个网址。预设为client
  • mode - corsno-corssame-originnavigate。预设为cors。Chrome(v47~)目前的预设值是same-origin
  • credentials - omitsame-origininclude。预设为omit。Chrome(v47~)目前的预设值是include
  • redirect - followerrormanual。Chrome(v47~)目前的预设值是。manual
  • integrity - Subresource Integrity(子资源完整性, SRI)的值
  • cache - defaultno-storereloadno-cache, 或 force-cache
  • body:要加到要求中的内容。注意,method为GETHEAD时不使用这个值。

注:由于不能在GET时使用body属性,如果你需要在GET时用到query字串,解决方案请参考以下的相关问答集: 问答问答

Request(要求)物件中可以包含headers属性,它是一个以Headers()建构式进行实体化的物件,实体化后可以再使用其中的方法进行设定。例如以下的范例:

const httpHeaders = { 'Content-Type' : 'image/jpeg', 'Accept-Charset' : 'utf-8', 'X-My-Custom-Header' : 'fetch are cool' }
const myHeaders = new Headers(httpHeaders)

const req = new Request(URL, {headers: myHeaders})
const httpHeaders = new Headers()
httpHeaders.append('Accept', 'application/json')

const req = new Request(URL, {headers: httpHeaders})

注: Headers()建构式的传入参数可以是其他的Headers物件,或是内含符合HTTP headers的位元组字串的物件。

注: Headers物件中还有一个很特殊的属性guard,它与安全性有关,请参考Basic_concepts#Guard

当然fetch方法也可以不需要一定得要传入Request(要求)实体物件,它可以直接使用相同结构的物件字面当作传入参数,例如以下的范例:

fetch('./sample.json', {
    method: 'GET',
    mode: 'cors',
    redirect: 'follow',
    headers: new Headers({
        'Content-Type': 'text/json'
    })
}).then(function(response) {
  //處理 response
})

fetch的语法连锁下一个.then方法,如果成功的话,会得到一个带有Response(回应)物件值的已实现状态的Promise物件。虽然在fetch API中也允许你自己建立一个Response(回应)物件实体,不过,Response(回应)物件通常都是从外部资源要求所得到,自订Response(回应)物件算是会在特殊的情况下才会作的事情。

Response(回应)物件中包含的属性摘要如下:

  • 类型:basiccors
  • url: 回应网址
  • useFinalURL: 布林值,代表这个网址是否为最后的网址(也可能是重新导向的网址)
  • status: 状态码(例如: 200, 404, 500...)
  • ok: 代表成功的状态码(状态码介于200-299)
  • statusText: 状态码的文字(例如: OK)
  • headers: 与回应相关的Headers物件

由于Response(回应)实作了Body介面(物件),可以由Body的方法来取得回应回来的内容,但因为Body属性值本身是个ReadableStream的物件,需要再依照不同的内容资料类型使用对应的方法,才能真正取到资料物件,其中最常使用的是json与text方法:

  • arrayBuffer()
  • BLOB()
  • formdat A()
  • JSON()
  • 文本()

这几个方法在使用过后,会产生带有相关已解析资料值的已实现Promise物件,通常的作法还需要再下一个then方法中才取得到其中的已解析资料值(物件)。另一种作法是使用巢状的Promise语法来取得资料,不过巢状的Promise语法容易造成语法复杂,你可以独立出来解析JSON资料物件的程式码到另一个函式中会比较清楚。

注: arrayBuffer请参考ArrayBuffer

注: blob请参考Blob

不过要特别注意的是,Body实体的在Request(要求)与Response(回应)中的设计是" 只要读取过就不能再使用",Request(要求)或Response(回应)物件其中都有一个bodyUsed只能读不能写的属性,它在被读取过会变成true,代表不能再被重覆使用。所以如果要重覆使用Body物件,必须在被读取前(即bodyUsed被设定为true之前),先呼叫Request(要求)或Response(回应)物件中的clone方法,另外拷贝出一个新的实体。

以下分别由几种不同的资料类型来撰写的样式。

纯文字/HTML格式文字
fetch('/next/page')
  .then(function(response) {
    return response.text()
  }).then(function(text) {
      console.log(text)
  }).catch(function(err) {
      // Error :(
  })
json格式

json方法会回传一个带有包含JSON资料的物件值的Promise已实现物件。

fetch('https://davidwalsh.name/demo/arsenal.json').then(function(response) {
    // 直接轉成JSON格式
    return response.json()
}).then(function(j) {
    // `j`會是一個JavaScript物件
    console.log(j)
}).catch(function(err) {
  // Error :(
})
blob(原始资料raw data)
fetch('https://davidwalsh.name/flowers.jpg')
    .then(function(response) {
      return response.blob();
    })
    .then(function(imageBlob) {
      document.querySelector('img').src = URL.createObjectURL(imageBlob);
    })

注: URL也是一个的Web API,在新式的浏览器上都有支援。请参考URL.createObjectURL

formdat到

FormData是在要求时传送表单资料时使用。以下为范例:

fetch('https://davidwalsh.name/submit', {
    method: 'post',
    body: new FormData(document.getElementById('comment-form'))
})

也可以使用JSON格式的物件资料来作要求:

fetch('https://davidwalsh.name/submit-json', {
    method: 'post',
    body: JSON.stringify({
        email: document.getElementById('email').value,
        answer: document.getElementById('answer').value
    })
})

注: FormData介面包含在XMLHttpRequest的新标准之中,目前只有Chrome与Firefox支援,请参考FormData

相较于jQuery.ajax

jQuery的ajax及相关方法的设计,已经很与fetch的语法结构很类似,不过它的回传值仍然只是XHR物件的扩充jqXHR物件,需要经过转换才能成为ES6的Promise物件。除此之外,有两个重要的不同之处需要注意,来自window.fetch polyfill :

  • fetch方法回传的Promise物件不会在有收到Response(回应),但是是在HTTP错误状态码(例如404、500)的时候变成已拒绝(rejected)状态。也就是说,它只会在网路出现问题或是被阻止进行Request(要求)时,才会变成已拒绝(rejected)状态,其他都是已实现(fulfilled)。

  • fetch方法预设是不会传送任何的认证证书(credentials)例如cookie到伺服器上的,这有可能会造成有管理使用者连线阶段(session)的伺服器视为未经认证的Request(要求) 。要加上传送cookie可以用fetch(url, {credentials: 'include'})的语法来设置。

问题点

要求中断或是设定timeout

Fetch目前没有办法像XHR可以中断要求、或是设定timeout属性,请参考Add timeout option #20的讨论。这篇部落格JavaScript Fetch API in action有提供一个用Promise物件包装的暂时解决方式,部份程式码如下:

var MAX_WAITING_TIME = 5000;// in ms

var timeoutId = setTimeout(function () {
    wrappedFetch.reject(new Error('Load timeout for resource: ' + params.url));// reject on timeout
}, MAX_WAITING_TIME);

return wrappedFetch.promise// getting clear promise from wrapped
    .then(function (response) {
        clearTimeout(timeoutId);
        return response;
    });

进程事件(Progress events)

Fetch目前没办法观察进程事件(或传输状态)。在Fetch标准中有提供一个简单的范例,但并不是太好的作法,程式码如下:

function consume(reader) {
  var total = 0
  return pump()
  function pump() {
    return reader.read().then(({done, value}) => {
      if (done) {
        return
      }
      total += value.byteLength
      log(`received ${value.byteLength} bytes (${total} bytes in total)`)
      return pump()
    })
  }
}

fetch("/music/pk/altes-kamuffel.flac")
  .then(res => consume(res.body.getReader()))
  .then(() => log("consumed the entire body without keeping the whole thing in memory!"))
  .catch(e => log("something went wrong: " + e))

结论

Fetch在浏览器的实作与XHR不同,里面的功能内容与API也相差很多,它有很多设计是为了新式的HTML5相关应用所设计的。现在已经有许多大公司的网站开始大量的使用Fetch API来取代XHR的作法,相信这个技术在这二、三年会愈来愈普及,毕竟AJAX技术对网站应用实在太重要,而这是一种扮演关键角色的技术。而且值得一提的是,现在在Chrome浏览器中在Service Worker技术实作中也提供了Fetch方法,但只限制在Service Worker中使用,这是一个非常新的应用技术,称为Progressive Web App(渐进式的网路应用, PWA)。

补充: AJAX与Fetch函式库比较表

本表主要参考自AJAX/HTTP Library Comparison

名称Github星浏览器支援Node支援诺言原始最后发布日
XMLHttpRequest的----
HTTP节点----
-部份-是*-
window.fetch填充工具9269全部--2016/5
节点取894--2016/5
同构取2587-2015/11
爱可信2035-2016/7
的SuperAgent8175-2016/7
jQuery的40718-是*-2016/7

忠毅:

  1. 以上统计数据为2016/7月
  2. 浏览器是以目前各浏览器品牌的最新发布稳定版本而言。
  3. isomorphic-fetch是混合window.fetch polyfill(whatwg-fetch模组)与node-fetch的专案。
  4. Promise为ES6新特性,浏览器支援一览表。需要另外填充时使用es6-promise
  5. jQuery并非专门用于AJAX的函式库,3.0版本后支援Promise目前标准。

参考资料

Fetch标准

教学

XHR&相关协定

原文来自Eddy Chang的gitbook,https://www.gitbook.com/@eyesofkids

转载于:https://my.oschina.net/yihong/blog/873683

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值