每天一小步 < ajax、fetch和axios的区别和封装 >

316 阅读6分钟

前言

本文主要是讲述ajaxfetchaxios的优缺点以及一些基本的封装方式,更加深入的理解还请自行查阅资料

AJAX

Asynchronous JavaScript + XML(异步 JavaScript 和 XML), 其本身不是一种新技术,而是一个在 2005 年被 Jesse James Garrett 提出的新术语,用来描述一种使用现有技术集合的‘新’方法,包括:HTML 或 XHTML,  CSS, JavaScript, DOM, XML, XSLT, 以及最重要的 XMLHttpRequest。当使用结合了这些技术的 AJAX 模型以后, 网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。这使得程序能够更快地回应用户的操作。(来自MDN AJAX文档

简单来说,AJAX并不是一种新技术,而是一种能使网页实现异步刷新的方式,那么下面就来看看AJAX的优劣势吧👇

AJAX的优势

  • 让用户体验到无需刷新也能获取网络资源的快乐
  • 使用异步方式通信
  • abort方法可以中断请求
  • 基于标准化的并被广泛支持的技术,无需安装依赖

AJAX的劣势

  • 对浏览器BAckHISTORY功能的破坏,浏览器无法前进和后退
  • 同步或异步需要通过传参区分,而不是基于事件的异步或同步

AJAX工作流程

  1. 使用XMLHttpRequestActiveXObject创建一个XHR对象
  2. 通过open向这个XHR对象传递请求初始参数,urlmethod
  3. 如果不是GET方法,则需要通过setRequestHeader设置请求头
  4. 调用send方法发送请求
  5. 使用onreadystatechange接收响应结果

简单封装(选自JavaScript学习笔记(二十七)-- ajax及ajax封装

function ajax(options) {
  // 先准备一个默认值
  let defaultConfig = {
    url: '', // 地址不需要默认值
    type: 'GET', // 请求方式的默认值是 GET
    async: true, // 默认值是异步
    data: '', // 参数没有默认值
    dataType: 'string', // 默认不需要执行 json.parse
    success () {}, // 默认是一个函数
  }

  // 先来判断一下有没有传递 url,如果没有,直接抛出异常
  if (!options.url) {
    throw new Error('url 必须传递')
  }

  // 有了 url 以后就,我们就把用户传递的参数和我们的默认数据合并
  for (let key in options) {
    defaultConfig[key] = options[key]
  }

  // 接下来的一切我们都是使用我们的 defaultConfig 就可以了
  // 第一步就是判断参数 data
  // data 可以不传递,可以为空
  // data 也可以是一个 key=value&key=value 格式的字符串
  // data 也可以是一个对象
  // 否则就抛出异常
  if (!(typeof defaultConfig.data === 'string' && /^(\w+=\w+&?)*$/.test(defaultConfig.data) || Object.prototype.toString.call(defaultConfig.data) === '[object Object]')) {
    throw new Error('请按照要求传递参数')
  }

  // 参数处理完毕以后,在判断 async 的数据类型
  // 只能传递 布尔数据类型
  if (typeof defaultConfig.async !== 'boolean') {
    throw new Error('async 参数只接受布尔数据类型')
  }

  // 在接下来就判断 type
  // 请求方式我们只接受 GET 或着 POST
  if (!(defaultConfig.type.toUpperCase() === 'GET' || defaultConfig.type.toUpperCase() === 'POST')) {
    throw new Error('目前本插件只接受 GET 和 POST 方式,请期待更新')
  }

  // 接下来就是判断 success 的判断,必须是一个函数
  if (Object.prototype.toString.call(defaultConfig.success) !== '[object Function]') {
    throw new Error('success 只接受函数数据类型')
  }

  // 参数都没有问题了
  // 我们就要把 data 处理一下了
  // 因为 data 有可能是对象,当 data 是一个对象的时候,我们要把它转换成一个字符串
  var str = ''
  if (Object.prototype.toString.call(defaultConfig.data) === '[object Object]') {
    for (let attr in defaultConfig.data) {
      str += `${attr}=${defaultConfig.data[attr]}&`
    }
    str = str.slice(0, -1)
    defaultConfig.data = str
  }
  
  // 参数全部验证过了以后,我们就可以开始进行正常的 ajax 请求了
  // 1. 准备一个 ajax 对象
  //    因为要处理兼容问题,所以我们准备一个函数
  function createXHR() {
    if (XMLHttpRequest) {
      return new XMLHttpRequest()
    } else {
      return new ActiveXObject('Microsoft.XMLHTTP')
    }
  }

  // 2. 创建一个 ajax 对象
  var xhr = createXHR()

  // 3. 进行 open
  xhr.open(defaultConfig.type, defaultConfig.url + (defaultConfig.type.toUpperCase() === 'GET' ? `?${defaultConfig.data}&_=${new Date().getTime()}` : ''), defaultConfig.async)

  if (defaultConfig.type.toUpperCase() === 'POST') {
    xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded')
  }

  // 4. 进行 send
  xhr.send((defaultConfig.type.toUpperCase() === 'POST' ? `${defaultConfig.data}` : ''))

  // 5. 接受响应
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && /2\d{2}/.test(xhr.status)) {
      // 表示成功,我们就要执行 success
      // 但是要进行 dataType 的判断
      if (defaultConfig.dataType === 'json') {
        defaultConfig.success(JSON.parse(xhr.responseText))
      } else {
        defaultConfig.success()
      }
    }
  }
}

FETCH

Fetch 提供了对 RequestResponse (以及其他与网络请求有关的)对象的通用定义。使之今后可以被使用到更多地应用场景中:无论是 service worker、Cache API、又或者是其他处理请求和响应的方式,甚至是任何一种需要你自己在程序中生成响应的方式。

它同时还为有关联性的概念,例如 CORS 和 HTTP 原生头信息,提供一种新的定义,取代它们原来那种分离的定义。(来自MDN FETCH文档

FETCH相较于AJAX来说更加轻便,基于原生打造也使得它能使用更多的API,下面看看FETCH的优劣势👇

FETCH的优势

  • 最大的优点就是基于Promise打造,无需担心异步问题
  • 语法更加简洁,更加语义化
  • 包含了更丰富的API

FETCH的劣势

  • 只对网络错误报错,400500类状态码都当作成功请求
  • 默认不携带Cookie
  • 不支持abort

FETCH的工作流程

  • 配置请求头
  • 使用fetch发送请求
  • 接收响应报文,并按类型将报文解码

简单封装

import baseUrl from './config'

const HttpMethod = {
    get: 'GET',
    post: 'POST',
    put: 'PUT',
    patch: 'PATCH',
    delete: 'DELETE'
}

const myFetch = (url, config) => {
    return new Promise(async (resolve, reject) => {
        let promise, contentType
        if (config?.['Content-Type'] !== undefined) {
            contentType = config['Content-Type']
        } else if (config?.method === HttpMethod.post) {
            contentType = ContentType.form
        } else {
            contentType = ContentType.json
        }

        const parseResult = async res => {
            const contentType = res.headers.get('Content-Type')
            if (contentType) {
                if (contentType.indexOf('json') !== -1) return await res.json()
                if (contentType.indexOf('text') !== -1) return await res.text()
                if (contentType.indexOf('form') !== -1)
                    return await res.formData()
                if (contentType.indexOf('video') !== -1) return await res.blob()
            }
            return res.text()
        }

        const handleResult = async res => {
            const result = await parseResult(res)
            if (res.ok) return result
        }

        const requestUrl = (baseUrl + url).replace('//', '/')
        const headers = new Headers({
            token: config?.token ?(localStorage.getItem('token')??config.token):false,
            'Content-Type': contentType,
        })

        try {
            if (!config?.method || config.method === HttpMethod.get) {
                promise = await fetch(requestUrl, {
                    headers,
                    credentials: config?.token?'include':'omit',
                })
            } else if (config.method === HttpMethod.post) {
                promise = await fetch(requestUrl, {
                    body: JSON.stringify(config.body),
                    headers,
                    method: HttpMethod.post,
                    credentials: config?.token ? 'include' : 'omit',
                })
            } else {
                promise = await fetch(requestUrl, {
                    body: JSON.stringify(config.body),
                    headers,
                    method: config.method,
                    credentials: config?.token ? 'include' : 'omit',
                })
            }
            resolve(handleResult(promise))
        } catch (err) {
            console.error('fetch error!', err)
            reject(err)
        }
    })
}

AXIOS

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

毫无疑问,AXIOS仍然是请求方式的首要之选,因为确实太方便了,它的优劣势如下👇

AXIOS的优势

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

AXIOS的劣势

  • 由于是第三方库,终归还是有那么一点点的担心跑路

AXIOS的工作流程

  • 配置请求头
  • 使用create方法创建AXIOS实例
  • 调用AXIOS实例的getpost等方法发送请求
  • 使用Promise链式调用获取响应结果

简单封装(节选自vue中Axios的封装和API接口的管理

/**
* axios封装
* 请求拦截、响应拦截、错误统一处理
*/
import axios from 'axios';
import router from '../router';
import store from '../store/index';
import { Toast } from 'vant';

/** 
* AXIOS实例配置项
*/
const instanceConfig = {
   baseURL: '' ,
   timeout: 1000,
   withCredentials: true
}

/** 
* 提示函数 
* 禁止点击蒙层、显示一秒后关闭
*/
const tip = msg => {    
   Toast({        
       message: msg,        
       duration: 1000,        
       forbidClick: true    
   });
}

/** 
* 跳转登录页
* 携带当前页面路由,以期在登录页面完成登录后返回当前页面
*/
const toLogin = () => {
   router.replace({
       path: '/login',        
       query: {
           redirect: router.currentRoute.fullPath
       }
   });
}

/** 
* 请求失败后的错误统一处理 
* @param {Number} status 请求失败的状态码
*/
const errorHandle = (status, other) => {
   // 状态码判断
   switch (status) {
       ...
       // 做错误状态管理
       default:
           console.log(other);   
       }}

// 创建axios实例
const instance = axios.create(instanceConfig);
// 设置post请求头
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
/** 
* 请求拦截器 
* 每次请求前,如果存在token则在请求头中携带token 
*/ 
instance.interceptors.request.use(    
   config => {        
       // 登录流程控制中,根据本地是否存在token判断用户的登录情况        
       // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token        
       // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码        
       // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。        
       const token = store.state.token;     
       token && (config.headers.Authorization = token);        
       return config;    
   },    
   error => Promise.error(error))

// 响应拦截器
instance.interceptors.response.use(    
   // 请求成功
   res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),    
   // 请求失败
   error => {
       const { response } = error;
       if (response) {
           // 请求已发出,但是不在2xx的范围 
           errorHandle(response.status, response.data.message);
           return Promise.reject(response);
       } else {
           if (!window.navigator.onLine) {
           ...
           // 处理断网的情况
           // eg:请求超时或断网时,更新state的network状态
           // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
           // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
           } else {
               return Promise.reject(error);
           }
       }
   });

结语

本文只是比较简单的介绍一下AJAXFETCHAXIOS的一些区别和简单的使用,内容基本上在社区都能找得到,只是为了方便笔者记忆所以动手写一下

要是有任何的错误或者需要修改的地方,恳请指出,感激不敬