js Fetch返回数据res.json()报错问题

前言

一直以来在简单的场景中经常使用fetch代替第三方请求库, fetch是JavaScript的原生函数, 简单、高效、快速、稳定、可定制等等诸多优点。一直也是用着很是舒服,直到有一天它竟然报错了。

什么是Fetch?

官方: Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。.

还原现场

因为业务需求简单,这里只封装了get和post方法, 并且后端数据都是已默认的json格式返回

const http = {
  apiBaseUrl: config.apiBaseUrl,
  get: function (url) {
    return new Promise((resolve, reject) => {
      fetch(this.apiBaseUrl + url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        }
      }).then(res => res.json()).then(res => {
        resolve(res);
      }).catch(e => {
        console.error("请求失败了,详细信息:" + JSON.stringify(e));
        reject(e);
      });
    })
  },
  post: function (url, body) {
    return new Promise((resolve, reject) => {
      fetch(this.apiBaseUrl + url, {
        method: "POST",
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        }, body: JSON.stringify(body)
      }).then(res => res.json()).then(res => {
        resolve(res);
      }).catch(e => {
        console.error("请求失败了,详细信息:" + JSON.stringify(e));
        reject(e);
      })
    })
  }
}

这样的封装貌似也没什么问题 :)。

后端没有做统一的返回数据格式约定, 直接使用.net中的ActionResult结构返回, .net封装的应该都是很好用的 :)

/// <summary>
/// 模拟处理数据
/// </summary>
/// <returns></returns>
[HttpPost("handle-data")]
public ActionResult HandleData(string mockData)
{
    // 业务逻辑
    // ......

    // 处理完成,直接告诉浏览器我Ok了, 状态200
    return Ok();
}

此时前端在处理完成后, 控制台面板中 Network 中也显示post成功返回200了, 但是在 Console 中却有一条红色报错信息

请求失败了,详细信息:{}
js Fetch返回数据res.json()报错问题
fetch error


看到报错位置竟然是post中的catch, 可明明在Network中看到返回200了啊, 稍作镇静之后就意识到应该就是返回时数据处理报错了, 在resolve(res)上面打印也没走这个逻辑, 那就是 上一层 .then(res=> res.json()有问题。

// 将.then(res=> res.json()) 替换成下面
.then(res => {
   console.log(res.json());
   return res.json();
})

这样写同样报错

js Fetch返回数据res.json()报错问题

可以看到时解析json报错了, 嗯, 因为我们就是没有返回任何数据, 解析自然会报错!
既然没有值Json解析报错, 那解决办法自然就得加一层判断了(也可以让后端必须返回一个Json, 简单粗暴, 哈哈! ) , 思路是先读取值然后判断是否为空.
但是打印res时

// 将.then(res=> res.json()) 替换成下面
.then(res => {
   console.log(res);
   let arrayBuffer = res.json();
   let json = res.json();
   return res.json();
})
body: ReadableStream
         locked: true
         [[Prototype]]: ReadableStream
bodyUsed: true
headers: Headers
[[Prototype]]: Headers
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "'http://localhost:5069/api/handle-data"
[[Prototype]]: Response

而且还有一条报错信息

Uncaught (in promise) TypeError: Failed to execute 'json' on 'Response': body stream already read

body stream already read说明流只能读取一次,
body是一个ReadableStream数据流,必须先读取流才能看到数据, 那就看一下是否还能转换成其他格式的数据.
查找MDN https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch#body

Body 类定义了以下方法(这些方法都被 Request 和 Response所实现)以获取 body 内容。这些方法都会返回一个被解析后的 Promise 对象和数据。

  • `Request.arrayBuffer()` / `Response.arrayBuffer()`

  • `Request.blob()` / `Response.blob()`

  • `Request.formData()` / `Response.formData()`

  • `Request.json()` / `Response.json()`

  • `Request.text()` / `Response.text()`

可知有5种数据格式,因为json和text可使用js原生方法JSON.parse/JSON.stringify相互转换, 那就直接选用.text()转成字符串判断即可

// 将.then(res=> res.json()) 替换成下面
.then(res => {
  let data = res.text();//转成字符串判断
  return data.then(r => {
  if (r.length === 0) return null;
  else return JSON.parse(r);
  })
})

验证结果正确, 一切又回到了正常。

简单封装fetch 获取json或空数据

let checkStatus = res => {
  if (res.status >= 200 && res.status < 300) return res;
  else {
    let err = new Error(res.statusText);
    err.response = res;
    throw err;
  }
}
let parseJson = res => {
  let data = res.text();
  return data.then(r => {
    if (r.length === 0) return null;
    else return JSON.parse(r);
  })
}
const http = {
  apiBaseUrl: config.apiBaseUrl,
  get: function (url) {
    return new Promise((resolve, reject) => {
      fetch(this.apiBaseUrl + url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        }
      }).then(checkStatus).then(parseJson).then(res => {
        resolve(res);
      }).catch(e => {
        console.error("请求失败了,详细信息:" + JSON.stringify(e));
        reject(e);
      });
    })
  },
  post: function (url, body) {
    return new Promise((resolve, reject) => {
      fetch(this.apiBaseUrl + url, {
        method: "POST",
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        }, body: JSON.stringify(body)
      }).then(checkStatus).then(parseJson).then(res => {
        resolve(res);
      }).catch(e => {
        console.error("请求失败了,详细信息:" + JSON.stringify(e));
        reject(e);
      })
    })
  }
}

嗯 , 前端能处理的问题就不麻烦后端了 :)

总结

fetch返回的是数据流, 最终是什么格式数据需要我们自己判断使用, arrayBuffer/blob/formData/json/text 这些格式都有其使用场景, 前端er不要将思维只限定在了json哦。