Fetch

Fetch

JavaScript通过XMLHttpRequest(XHR)来执行异步请求,这个方式已经存在了很长一段时间。虽说它很有用,但它不是最佳API。它在设计上不符合职责分离原则,将输入、输出和用事件来跟踪的状态混杂在一个对象里。而且,基于事件的模型与最近JavaScript流行的Promise以及基于生成器的异步编程模型不太搭。

新的Fetch API打算修正上面提到的那些缺陷。它向JS中引入和HTTP协议中同样的原语。具体而言,它引入一个实用的函数fetch()用来简洁捕捉从网络上检索一个资源的意图。Fetch规范 的API明确了用户代理获取资源的语义。它结合ServiceWorkers,尝试达到以下优化:

  • 改善离线体验
  • 保持可扩展性

而与jQuery相比,fetch方法与 jQuery.ajax() 的主要区别在于:

  • fetch()方法返回的Promise对象并不会在HTTP状态码为404或者500的时候自动抛出异常,而需要用户进行手动处理
  • 默认情况下,fetch并不会发送任何的本地的cookie到服务端,注意,如果服务端依靠Session进行用户控制的话要默认开启Cookie

请求构建

假设fetch已经被挂载到了全局的window目录下。

// Simple response handling
fetch("/some/url")
  .then(function (response) {})
  .catch(function (err) {
    // Error :(
  });
// Chaining for more "advanced" handling
fetch("/some/url")
  .then(function (response) {
    return; //...
  })
  .then(function (returnedValue) {
    // ...
  })
  .catch(function (err) {
    // Error :(
  });

Request

Request对象代表了一次fetch请求中的请求体部分,你可以自定义 Request 对象:

  • method -使用的HTTP动词,GET, POST, PUT, DELETE, HEAD
  • url -请求地址,URL of the request
  • headers -关联的Header对象
  • referrer - referrer
  • mode -请求的模式,主要用于跨域设置,cors, no-cors, same-origin
  • credentials -是否发送Cookieomit, same-origin
  • redirect -收到重定向请求之后的操作,follow, error, manual
  • integrity -完整性校验
  • cache -缓存模式(default,reload,no-cache)
// 构建独立的请求对象
const request = new Request("/users.json", {
  method: "POST",
  mode: "cors",
  redirect: "follow",
  headers: new Headers({
    "Content-Type": "text/plain",
  }),
});

// Now use it!
fetch(request).then(function () {
  /* handle response */
});

// 直接作为参数传入到 fetch 函数中
fetch("/users.json", {
  method: "POST",
  mode: "cors",
  redirect: "follow",
  headers: new Headers({
    "Content-Type": "text/plain",
  }),
}).then(function () {
  /* handle response */
});

POST请求中,如果我们需要传递参数,则应该将参数值进行序列化处理为字符串然后传递:

fetch("/users", {
  method: "post",
  headers: {
    Accept: "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Hubot",
    login: "hubot",
  }),
});

URI Encode

注意,fetch方法是自动会将URI中的双引号进行编码的,如果在URI中存入了部分JSON,有时候会出现意想不到的问题,譬如我们以GET方法访问如下的URI

[GET] http://api.com?requestData={"p":"q"}

那么fetch会自动将双引号编码,变成:

[GET] http://api.com?requestData={%22p%22:%22q%22}

请求在传入某些后端服务器时可能会触发异常,建议对URIQuery Parameter部分进行统一的URI编码:

//将 requestData 序列化为 JSON
const requestDataString = encodeURIComponent(
  JSON.stringify(requestData).replace(/%22/g, '"')
);

//将字符串链接
const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`;

Headers |自定义请求头

// Create an empty Headers instance
const headers = new Headers();

// Add a few headers
headers.append("Content-Type", "text/plain");
headers.append("X-My-Custom-Header", "CustomValue");

// Check, get, and set header values
headers.has("Content-Type"); // true
headers.get("Content-Type"); // "text/plain"
headers.set("Content-Type", "application/json");

// Delete a header
headers.delete("X-My-Custom-Header");

// Add initial values
const headers = new Headers({
  "Content-Type": "text/plain",
  "X-My-Custom-Header": "CustomValue",
});

常见的请求方法有:append,has,get,set以及 delete

const request = new Request("/some-url", {
  headers: new Headers({
    "Content-Type": "text/plain",
  }),
});

fetch(request).then(function () {
  /* handle response */
});

Cookies

如果需要设置fetch自动地发送本地的Cookie,需要将credentials设置为same-origin:

fetch("/users", {
  credentials: "same-origin",
});

该选项会以类似于XMLHttpRequest的方式来处理Cookie,否则,可能因为没有发送Cookie而导致基于Session的认证出错。对于跨域情况下的Cookie发送,可以将 credentials 的值设置为include 来在CORS情况下发送请求。

fetch("https://example.com:1234/users", {
  credentials: "include",
});

另外需要注意的是,当你为了配置在CORS请求中附带Cookie等信息时,来自于服务器的响应中的Access-Control-Allow-Origin不可以再被设置为 *,必须设置为某个具体的域名,则响应会失败。

Response |响应处理

fetchthen函数中提供了一个Response对象,即代表着对于服务端返回值的封装,你也可以在Mock的时候自定义Response对象,譬如在你需要使用Service Workers的情况下,在Response中,你可以作如下配置:

  • type - basic, cors
  • url
  • useFinalURL -是否为最终地址
  • status -状态码(ex:200,404, etc.)
  • ok -是否成功响应(status in the range 200-299)
  • statusText - status code (ex: OK)
  • headers -响应头
// Create your own response for service worker testing
// new Response(BODY, OPTIONS)
const response = new Response(".....", {
  ok: false,
  status: 404,
  url: "/",
});

// The fetch's `then` gets a Response instance back
fetch("/").then(function (responseObj) {
  console.log("status: ", responseObj.status);
});

Response 还提供以下方法:

  • clone() -创建一个Response对象的克隆。
  • error() -返回与网络错误关联的新Response对象。
  • redirect() -使用不同的URL创建一个新的响应。
  • arrayBuffer() -返回一个用ArrayBuffer解析的promise
  • blob() -返回一个用Blob解析的promise
  • formData() -返回一个用FormData对象解析的promise
  • json() -返回一个用JSON对象解析的promise
  • text() -返回一个用USVString(text)解析的promise

Handling HTTP error statuses |处理HTTP错误状态

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  } else {
    const error = new Error(response.statusText);
    error.response = response;
    throw error;
  }
}

function parseJSON(response) {
  return response.json();
}

fetch("/users")
  .then(checkStatus)
  .then(parseJSON)
  .then(function (data) {
    console.log("request succeeded with JSON response", data);
  })
  .catch(function (error) {
    console.log("request failed", error);
  });

Handling JSON |处理JSON响应

fetch("https://davidwalsh.name/demo/arsenal.json")
  .then(function (response) {
    // Convert to JSON
    return response.json();
  })
  .then(function (j) {
    // Yay, `j` is a JavaScript object
    console.log(j);
  });

Handling Basic Text/HTML Response |处理文本响应

fetch("/next/page")
  .then(function (response) {
    return response.text();
  })
  .then(function (text) {
    // <!DOCTYPE ....
    console.log(text);
  });

Blob Responses |二进制数据处理

如果你希望通过fetch方法来载入一些类似于图片等资源:

fetch("flowers.jpg")
  .then(function (response) {
    return response.blob();
  })
  .then(function (imageBlob) {
    document.querySelector("img").src = URL.createObjectURL(imageBlob);
  });

blob() 方法会接入一个响应流并且一直读入到结束。

自定义封装

可控的Promise

代理

在上面的介绍中会发现,fetch并没有在客户端实现Cancelable Request的功能,或者超时自动放弃功能,因此这一步骤往往是需要在代理层完成。笔者在自己的工作中还遇到另一个请求,就是需要在客户端抓取其他没有设置CORS响应或者JSONP响应的站点,而必须要进行中间代理层抓取。笔者为了尽可能小地影响逻辑层代码,因此在自己的封装中封装了如下方法:

/**
 * @function 通过透明路由,利用get方法与封装好的QueryParams形式发起请求
 * @param BASE_URL 请求根URL地址,注意,需要添加http://以及末尾的/,譬如`http://api.com/`
 * @param path 请求路径,譬如"path1/path2"
 * @param queryParams 请求的查询参数
 * @param contentType 请求返回的数据格式
 * @param proxyUrl 请求的路由地址
 */
getWithQueryParamsByProxy({BASE_URL=Model.BASE_URL, path="/", queryParams={}, contentType="json", proxyUrl="http://api.proxy.com"}) {

    //初始化查询字符串,将BASE_URL以及path进行编码
    let queryString = `BASE_URL=${encodeURIComponent(BASE_URL)}&path=${encodeURIComponent(path)}&`;

    //根据queryParams构造查询字符串
    for (let key in queryParams) {
        //拼接查询字符串
        queryString += `${key}=${encodeURIComponent(queryParams[key])}&`;
    }

    //将查询字符串进行编码
    let encodedQueryString = (queryString);

    //封装最终待请求的字符串
    const packagedRequestURL = `${proxyUrl}?${encodedQueryString}action=GET`;

    //以CORS方式发起请求
    return this._fetchWithCORS(packagedRequestURL, contentType);
}
下一页