Why Fetch

XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

Fetch 的出现就是为了解决 XHR 的问题,拿例子说明:

使用 XHR 发送一个 json 请求一般是这样:

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
console.log(xhr.response);
};

xhr.onerror = function() {
console.log("Oops, error");
};

xhr.send();

使用 Fetch 后,顿时看起来好一点

fetch(url).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
}).catch(function(e) {
console.log("Oops, error");
});

使用 ES6 的 箭头函数 后:

fetch(url).then(response => response.json())
.then(data => console.log(data))
.catch(e => console.log("Oops, error", e))

现在看起来好很多了,但这种 Promise 的写法还是有 Callback 的影子,而且 promise 使用 catch 方法来进行错误处理的方式有点奇怪。不用急,下面使用 async/await 来做最终优化:

注:async/await 是非常新的 API,属于 ES7,目前尚在 Stage 1(提议) 阶段,这是它的完整规范。使用 Babel 开启 runtime 模式后可以把 async/await 无痛编译成 ES5 代码。也可以直接使用 regenerator 来编译到 ES5。

try {
let response = await fetch(url);
let data = response.json();
console.log(data);
} catch(e) {
console.log("Oops, error", e);
}
// 注:这段代码如果想运行,外面需要包一个 async function

duang~~ 的一声,使用 await 后,写异步代码就像写同步代码一样爽await 后面可以跟 Promise 对象,表示等待 Promise resolve() 才会继续向下执行,如果 Promise 被 reject() 或抛出异常则会被外面的 try...catch 捕获。

Promise,generator/yield,await/async 都是现在和未来 JS 解决异步的标准做法,可以完美搭配使用。这也是使用标准 Promise 一大好处。最近也把项目中使用第三方 Promise 库的代码全部转成标准 Promise,为以后全面使用 async/await 做准备。

另外,Fetch 也很适合做现在流行的同构应用,有人基于 Fetch 的语法,在 Node 端基于 http 库实现了 node-fetch,又有人封装了用于同构应用的 isomorphic-fetch。

注:同构(isomorphic/universal)就是使前后端运行同一套代码的意思,后端一般是指 NodeJS 环境。

总结一下,Fetch 优点主要有:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便,使用 isomorphic-fetch

fetch和XMLHttpRequest

如果看网上的fetch教程,会首先对比XMLHttpRequest和fetch的优劣,然后引出一堆看了很快会忘记的内容(本人记性不好)。因此,我写一篇关于fetch的文章,为了自己看着方便,毕竟工作中用到的也就是一些很基础的点而已。

fetch,说白了,就是XMLHttpRequest的一种替代方案。如果有人问你,除了Ajax获取后台数据之外,还有没有其他的替代方案?

这是你就可以回答,除了XMLHttpRequest对象来获取后台的数据之外,还可以使用一种更优的解决方案fetch

如何获取fetch

到现在为止,fetch的支持性还不是很好,但是在谷歌浏览器中已经支持了fetch。fetch挂在在BOM中,可以直接在谷歌浏览器中使用。

查看fetch的支持情况:fetch的支持情况

当然,如果不支持fetch也没有问题,可以使用第三方的ployfill来实现只会fetch:whatwg-fetch

fetch的helloworld

下面我们来写第一个fetch获取后端数据的例子:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html') // 返回一个Promise对象
.then((res)=>{
return res.text() // res.text()是一个Promise对象
})
.then((res)=>{
console.log(res) // res是最终的结果
})

是不是很简单?可能难的地方就是Promise的写法,这个可以看阮一峰老师的ES6教程来学习。

说明一点,下面演示的GET请求或POST请求,都是采用百度中查询到的一些接口,可能传递的有些参数这个接口并不会解析,但不会影响这个接口的使用。

GET请求

GET请求初步

完成了helloworld,这个时候就要来认识一下GET请求如何处理了。

上面的helloworld中这是使用了第一个参数,其实fetch还可以提供第二个参数,就是用来传递一些初始化的信息。

这里如果要特别指明是GET请求,就要写成下面的形式:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'GET'
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

GET请求的参数传递

GET请求中如果需要传递参数怎么办?这个时候,只能把参数写在URL上来进行传递了。

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中写上传递的参数
method: 'GET'
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

POST请求

POST请求初步

与GET请求类似,POST请求的指定也是在fetch的第二个参数中:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'POST' // 指定是POST请求
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

POST请求参数的传递

众所周知,POST请求的参数,一定不能放在URL中,这样做的目的是防止信息泄露。

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'POST',
body: new URLSearchParams([["foo", 1],["bar", 2]]).toString() // 这里是请求对象
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

其实除了对象URLSearchParams外,还有几个其他的对象,可以参照:常用的几个对象来学习使用。

设置请求的头信息

在POST提交的过程中,一般是表单提交,可是,经过查询,发现默认的提交方式是:Content-Type:text/plain;charset=UTF-8,这个显然是不合理的。下面咱们学习一下,指定头信息:

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded' // 指定提交方式为表单提交
}),
body: new URLSearchParams([["foo", 1],["bar", 2]]).toString()
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

这个时候,在谷歌浏览器的Network中查询,会发现,请求方式已经变成了content-type:application/x-www-form-urlencoded

通过接口得到JSON数据

上面所有的例子中都是返回一个文本,那么除了文本,有没有其他的数据类型呢?肯定是有的,具体查询地址:Body的类型

由于最常用的是JSON数据,那么下面就简单演示一下获取JSON数据的方式:


// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/rec?platform=wise&ms=1&rset=rcmd&word=123&qid=11327900426705455986&rq=123&from=844b&baiduid=A1D0B88941B30028C375C79CE5AC2E5E%3AFG%3D1&tn=&clientWidth=375&t=1506826017369&r=8255', { // 在URL中写上传递的参数
method: 'GET',
headers: new Headers({
'Accept': 'application/json' // 通过头指定,获取的数据类型是JSON
})
})
.then((res)=>{
return res.json() // 返回一个Promise,可以解析成JSON
})
.then((res)=>{
console.log(res) // 获取JSON数据
})

强制带Cookie

默认情况下, fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于维护一个用户会话,则导致未经认证的请求(要发送 cookies,必须发送凭据头).

// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'GET',
credentials: 'include' // 强制加入凭据头
})
.then((res)=>{
return res.text()
})
.then((res)=>{
console.log(res)
})

简单封装一下fetch

最后了,介绍了一大堆内容,有没有发现,在GET和POST传递参数的方式不同呢?下面咱们就来封装一个简单的fetch,来实现GET请求和POST请求参数的统一。

/**
* 将对象转成 a=1&b=2的形式
* @param obj 对象
*/
function obj2String(obj, arr = [], idx = 0) {
for (let item in obj) {
arr[idx++] = [item, obj[item]]
}
return new URLSearchParams(arr).toString()
}

/**
* 真正的请求
* @param url 请求地址
* @param options 请求参数
* @param method 请求方式
*/
function commonFetcdh(url, options, method = 'GET') {
const searchStr = obj2String(options)
let initObj = {}
if (method === 'GET') { // 如果是GET请求,拼接url
url += '?' + searchStr
initObj = {
method: method,
credentials: 'include'
}
} else {
initObj = {
method: method,
credentials: 'include',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}),
body: searchStr
}
}
fetch(url, initObj).then((res) => {
return res.json()
}).then((res) => {
return res
})
}

/**
* GET请求
* @param url 请求地址
* @param options 请求参数
*/
function GET(url, options) {
return commonFetcdh(url, options, 'GET')
}

/**
* POST请求
* @param url 请求地址
* @param options 请求参数
*/
function POST(url, options) {
return commonFetcdh(url, options, 'POST')
}
  1. GET('https://www.baidu.com/search/error.html', {a:1,b:2})
  2. POST('https://www.baidu.com/search/error.html', {a:1,b:2})

Ajax

它的全称是:Asynchronous JavaScript And XML,翻译过来就是“异步的 Javascript 和 XML”。

很多小伙伴可能会误以为 Ajax 是发请求的一种方式,或者把 XMLHttpRequest 与 Ajax 划等号,其实这是错误和片面的。

正解:

Ajax 是一个技术统称,是一个概念模型,它囊括了很多技术,并不特指某一技术,它很重要的特性之一就是让页面实现局部刷新。

特点:

  • 局部刷新页面,无需重载整个页面。

简单来说,Ajax 是一种思想,XMLHttpRequest 只是实现 Ajax 的一种方式。其中 XMLHttpRequest 模块就是实现 Ajax 的一种很好的方式,这也是很多面试官喜欢让面试者手撕的代码之一。

利用 XMLHttpRequest 模块实现 Ajax。

示例代码:

<body>
  <script>
    function ajax(url) {
      const xhr = new XMLHttpRequest();
      xhr.open("get", url, false);
      xhr.onreadystatechange = function () {
        // 异步回调函数
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            console.info("响应结果", xhr.response)
          }
        }
      }
      xhr.send(null);
    }
    ajax('https://smallpig.site/api/category/getCategory')
  </script>
</body>

输出结果:

这里利用 XMLHttpRequest 模块实现了一个最简单的 get 网络请求。

注意:我们使用这种方式实现网络请求时,如果请求内部又包含请求,以此循环,就会出现回调地狱,这也是一个诟病,后来才催生了更加优雅的请求方式。

Fetch、Ajax、Axios区别

Fetch

Fetch 是在 ES6 出现的,它使用了 ES6 提出的 promise 对象。它是 XMLHttpRequest 的替代品。

很多小伙伴会把它与 Ajax 作比较,其实这是不对的,我们通常所说的 Ajax 是指使用 XMLHttpRequest 实现的 Ajax,所以真正应该和 XMLHttpRequest 作比较。

正解:

Fetch 是一个 API,它是真实存在的,它是基于 promise 的。

特点:

  • 使用 promise,不使用回调函数。
  • 采用模块化设计,比如 rep、res 等对象分散开来,比较友好。
  • 通过数据流对象处理数据,可以提高网站性能。

所以这里就和 Ajax 又很大不同了,一个是思想,一个是真实存在的 API,不过它们都是用来给网络请求服务的,我们一起来看看利用 Fetch 实现网络请求。

示例代码:

<body>
  <script>
    function ajaxFetch(url) {
      fetch(url).then(res => res.json()).then(data => {
        console.info(data)
      })
    }
    ajaxFetch('https://smallpig.site/api/category/getCategory')
  </script>
</body>

输出结果:

上段代码利用 Fetch 发送了一个最简单的 get 请求,其中最重要的特点之一就是采用了.then 链式调用的方式处理结果,这样不仅利于代码的可读,而且也解决了回调地狱的问题。

Axios

Axios 是随着 Vue 的兴起而被广泛使用的,目前来说,绝大多数的 Vue 项目中的网络请求都是利用 Axios 发起的。当然它并不是一个思想,或者一个原生 API,它是一个封装库。

正解:

Axios 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。

特点:

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

所以说,Axios 可以说是 XHR 的一个子集,而 XHR 又是 Ajax 的一个子集。既然说它是一个库,那么我们在使用的时候就需要引入它。

示例代码:

// 发送 POST 请求
axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})

总结

Ajax、Fetch、axios三者之间的关系可以用一张图来清晰的表示,如图:

三者做个对比:

Ajax一种技术统称,主要利用XHR实现网络请求
Fetch具体API,基于promise,实现网络请求
Axios一个封装库,基于XHR封装,较为推荐使用
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐