Fetch API 教程(打造属于自己的axios)

2,363 阅读3分钟

Fetch是什么?

fetch是一个JavaScript接口,XMLHttpRequest的一个理想的代替方案,用于跨网络异步获取资源(HTTP 请求)。

一个基本的 fetch 请求设置起来很简单。看看下面的代码:

fetch('http://example.com/movies.json')
  .then((response) => {
      return response.json();
  })
  .then((myJson) => {
    console.log(myJson);
  });

戴雨彤《来迟》: 我这一次,终究还是来的太迟

复习下XMLHttpRequest获取数据(ajax)

步骤:
1.创建XmlHttpRequest对象
2.调用open方法设置基本请求信息
3.设置发送的数据,发送请求
4.注册监听的回调函数
5.拿到返回值,对页面进行更新

//1.创建Ajax对象
    if(window.XMLHttpRequest){
       var oAjax=new XMLHttpRequest();
    }else{
       // 兼容ie
       var oAjax=new ActiveXObject("Microsoft.XMLHTTP");
    }

    //2.连接服务器(打开和服务器的连接)
    oAjax.open('GET', url, true);

    //3.发送
    oAjax.send();

    //4.接收
    oAjax.onreadystatechange=function (){
       if(oAjax.readyState==4){
           if(oAjax.status==200){
              //alert('成功了:'+oAjax.responseText);
              fnSucc(oAjax.responseText);
           }else{
              //alert('失败了');
              if(fnFaild){
                  fnFaild();
              }
           }
        }
    };

fetch封装

fetch的参数(feach(url, optionObj))

POST 请求

const response = await fetch(url, {
  method: 'POST',
  headers: {
    "Content-type": "application/json; charset=UTF-8",
  },
  body: 'foo=bar&lorem=ipsum',
});

const json = await response.json();

配置对象用到了三个属性

  • method:HTTP 请求的方法,POSTDELETEPUT都在这个属性设置。
  • headers:一个对象,用来定制 HTTP 请求的标头。
  • body:POST 请求的数据体。

有些标头不能通过headers属性设置,比如Content-LengthCookieHost等等。它们是由浏览器自动生成,无法修改。

添加 JSON 数据

const user =  { name:  'John', surname:  'Smith'  };
const response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
   'Content-Type': 'application/json;charset=utf-8'
  }, 
  body: JSON.stringify(user) 
});

封装一个自己的fetch(不支持表单)

interface IInit extends RequestInit {
  body?: BodyInit | null | any;
  prefix?: 'api' | 'api1';
}
export default function fetch(url: string, init?: IInit) {
  if (!init) init = {};
  const prefix = init.prefix ? uPrefix[init.prefix] : uPrefix.api;

  if (!init.credentials) init.credentials = 'include'; // 默认,不管同源请求,还是跨域请求,一律发送 Cookie。
  if (init.body && typeof init.body === 'object') init.body = JSON.stringify(init.body); // 转化JSON 字符串
  if (init.body && !init.method) init.method = 'POST'; // 存在body,默认请求方法POST
  if (init.method) init.method = init.method.toUpperCase(); // 兼容下,字符串转换为大写

  if (['POST', 'PUT', 'DELETE'].includes(init.method || '')) { // 提交JSON数据, 默认值是'text/plain;charset=UTF-8'
    init.headers = Object.assign({}, init.headers || {
      'Content-Type': 'application/json',
    });
  }

  init = addCustomHeader(init)
  const realUrl = `${prefix}${url}`;
  return window
    .fetch(realUrl, init)
    .then(res => checkStatus(res)) //检验响应码
    .then((res) => res.json())
    .then((res) => filter(res)) // 根据业务需求,进行对应过滤
    .catch(err => console.log(`请求失败! + url: ${realUrl}`, err));
}

fetch()配置对象的完整 API

fetch()第二个参数的完整 API 如下。

const response = fetch(url, {
  method: "GET",
  headers: {
    "Content-Type": "text/plain;charset=UTF-8"
  },
  body: undefined,
  referrer: "about:client",
  referrerPolicy: "no-referrer-when-downgrade",
  mode: "cors", 
  credentials: "same-origin",
  cache: "default",
  redirect: "follow",
  integrity: "",
  keepalive: false,
  signal: undefined
});

cache

cache属性指定如何处理缓存。可能的取值如下:

  • default:默认值,先在缓存里面寻找匹配的请求。
  • no-store:直接请求远程服务器,并且不更新缓存。
  • reload:直接请求远程服务器,并且更新缓存。
  • no-cache:将服务器资源跟本地缓存进行比较,有新的版本才使用服务器资源,否则使用缓存。
  • force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器。
  • only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误。

mode

mode属性指定请求的模式。可能的取值如下:

  • cors:默认值,允许跨域请求。
  • same-origin:只允许同源请求。
  • no-cors:请求方法只限于 GET、POST 和 HEAD,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。

credentials

credentials属性指定是否发送 Cookie。可能的取值如下:

  • same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送。
  • include:不管同源请求,还是跨域请求,一律发送 Cookie。
  • omit:一律不发送。

跨域请求发送 Cookie,需要将credentials属性设为include


fetch('http://another.com', {
  credentials: "include"
});

signal

signal属性指定一个 AbortSignal 实例,用于取消fetch()请求,详见下一节。

keepalive

keepalive属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。

一个典型的场景就是,用户离开网页时,脚本向服务器提交一些用户行为的统计信息。这时,如果不用keepalive属性,数据可能无法发送,因为浏览器已经把页面卸载了。


window.onunload = function() {
  fetch('/analytics', {
    method: 'POST',
    body: "statistics",
    keepalive: true
  });
};

redirect

redirect属性指定 HTTP 跳转的处理方法。可能的取值如下:

  • follow:默认值,fetch()跟随 HTTP 跳转。
  • error:如果发生跳转,fetch()就报错。
  • manualfetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。

integrity

integrity属性指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值。

比如,下载文件时,检查文件的 SHA-256 哈希值是否相符,确保没有被篡改。


fetch('http://site.com/file', {
  integrity: 'sha256-abcdef'
});

referrer

referrer属性用于设定fetch()请求的referer标头。

这个属性可以为任意字符串,也可以设为空字符串(即不发送referer标头)。


fetch('/page', {
  referrer: ''
});

referrerPolicy

referrerPolicy属性用于设定Referer标头的规则。可能的取值如下:

  • no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。
  • no-referrer:不发送Referer标头。
  • originReferer标头只包含域名,不包含完整的路径。
  • origin-when-cross-origin:同源请求Referer标头包含完整的路径,跨域请求只包含域名。
  • same-origin:跨域请求不发送Referer,同源请求发送。
  • strict-originReferer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。
  • strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨域请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。
  • unsafe-url:不管什么情况,总是发送Referer标头。

取消fetch()请求

fetch()请求发送以后,如果中途想要取消,需要使用AbortController对象。


let controller = new AbortController();
let signal = controller.signal;

fetch(url, {
  signal: controller.signal
});

signal.addEventListener('abort',
  () => console.log('abort!')
);

controller.abort(); // 取消

console.log(signal.aborted); // true

上面示例中,首先新建 AbortController 实例,然后发送fetch()请求,配置对象的signal属性必须指定接收 AbortController 实例发送的信号controller.signal

controller.abort()方法用于发出取消信号。这时会触发abort事件,这个事件可以监听,也可以通过controller.signal.aborted属性判断取消信号是否已经发出。

下面是一个1秒后自动取消请求的例子。


let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('/long-operation', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') {
    console.log('Aborted!');
  } else {
    throw err;
  }
}

开始打造axios

1、clone克隆demo项目

    git clone https://gitee.com/dengruifeng/wabpack-demo

建立 fetch.ts

import { notification } from 'antd';

const window = self.window;
export interface IRes { //与后端约定的格式
  code: number;
  message: string;
  data: any;
}

interface IInit extends RequestInit {
  body?: BodyInit | null | any;
}

const checkStatus = (res: Response) => {
  if (res.status === 401) {
    window.location.href = '/login';
  }

  if (res.status === 405 || res.status === 403) {
    location.href = '/403';
  }

  if (res.status === 404) {
    notification.error({ message: '接口不存在' });
  }

  return res;
};

const filter = (res: IRes) => {
  // if (res.code !== 0 && res.code !== 200) {
  //   notification.error({
  //     message: '错误',
  //     description: res.message || '服务错误,请重试!',
  //   });
  //   throw res;
  // }
  res.data = res;
  return res.data; // 返回数据
};

const addCustomHeader = (init?: IInit) => { // 自定义添加头部信息
  init = init || {};
  init.headers = Object.assign(init?.headers || {}, {
    // 'X-SSO-USER': 'admin',
  });
  return init;
};

export default function fetch(url: string, init?: IInit) {
  if (!init) init = {};

  // if (!init.credentials) init.credentials = 'include'; // 默认same-origin,include不管同源请求,还是跨域请求,一律发送 Cookie。
  if (init.body && typeof init.body === 'object') init.body = JSON.stringify(init.body); // 转化JSON 字符串
  if (init.body && !init.method) init.method = 'POST'; // 存在body,默认请求方法POST
  if (init.method) init.method = init.method.toUpperCase(); // 兼容下,字符串转换为大写

  if (['POST', 'PUT', 'DELETE'].includes(init.method || '')) { // 提交JSON数据, 默认值是'text/plain;charset=UTF-8'
    init.headers = Object.assign({}, init.headers || {
      'Content-Type': 'application/json',
    });
  }

  init = addCustomHeader(init);
  console.log(init);
  return window
    .fetch(url, init)
    .then(res => checkStatus(res)) //检验响应码
    .then((res) => res.json())
    .then((res) => filter(res)) // 根据业务需求,进行对应过滤
    .catch(err => console.log(`请求失败! + url: ${url}`, err));
}

export function formFetch(url: string, init?: IInit) { // 表单fetch,用于上传文件等
  if (!init) init = {};

  init = addCustomHeader(init)
  return window
    .fetch(url, init)
    .then(res => checkStatus(res))
    .then((res) => res.json())
    .then((res) => filter(res))
    .catch(err => console.log(`请求失败! + url: ${url}`, err));
}

找个免费api测试百度有许多资源 我在这里找
api.uomg.com/doc-rand.im…

建立api接口文件

image.png

import fetch from '../utils/fetch';

export const getImgList = (sort: string) => {
  return fetch(`https://api.uomg.com/api/rand.img1?sort=${sort}&format=json`);
};

在页面使用

import { Input, Tag } from 'antd';
import { getImgList } from 'api/test';
import React from 'react';
import { Link } from 'react-router-dom';
import './index.less';

export const Page1 = () => {
  const [imgurl, setData] = React.useState('');
  const [sort, setsort] = React.useState('美女');

  React.useEffect(() => {
    getData();
  }, [sort]);

  const getData = () => {
    getImgList(sort).then((res: { imgurl: string }) => {
      console.log(res);
      setData(res.imgurl);
    });
  };

  return (
    <div className="test-css">
      <h1 className="test-css-home">我是Page1</h1>
      <Link to={'/'}>
        <Tag>返回首页</Tag>
      </Link>
      <h2>搜索图片</h2>
      <Input.Search
        style={{ width: 300 }}
        placeholder="输入关键字,如:美女、壁纸等"
        onSearch={(value: string) => setsort(value)}
        enterButton
      />
      <br />
      <img src={imgurl} alt="img" />
    </div>
  );
};

查看完整流程,自行查看项目代码

上传文件(formFetch)

上传的文件包含在整个表单里面,一起提交,文件上传要借助表单

  const handleChange = (e: UploadChangeParam) => {
    const file = e.fileList[0]?.originFileObj; // 文件流对象
    const fileName = e.fileList[0].name; // 文件名
    const formData = new FormData();
    formData.append('uploadFile', file as any);
    formData.append('fileName', fileName);
    upload(formData).then((res) => {
      console.log(res);
    });
  };
   export const upload = (obj: FormData) => {
  return formFetch(`/v3/normal/ecm/package`, {
    method: 'POST',
    body: obj,
  });
};

浏览器会这样展示

image.png

直接上传二进制数据

let blob = await new Promise(resolve =>   
  canvasElem.toBlob(resolve,  'image/png')
);

let response = await fetch('/article/fetch/post/image', {
  method:  'POST',
  body: blob
});

参考文献

# Fetch API 教程 - 阮一峰
fetch
官网
问题: 未测试取消fetch()请求
仓库: gitee.com/dengruifeng…