VueUse useFetch解析

1,198 阅读11分钟

介绍

通过 Fetch API 方式发送请求并提供在触发请求之前拦截请求、在 url 更改时自动重新获取请求以及使用预定义选项创建自己的请求封装。

使用

import { useFetch } from '@vueuse/core'

const { isFetching, error, data } = useFetch(url)

源码

首先我们先来看看最简单的 Fetch API 的封装

export function useFetch(url, options) {
    return fetch(url, options)
}

在此基础上我们加上默认是 GET 的请求。

const config = {
  method: 'GET'
}

export function useFetch(url, options) {
  return fetch(url, { method: config.method, ...options })
}

Fetch 要获取 response body,需要使用一个其他的方法调用。

Response 提供了多种基于 promise 的方法,来以不同的格式访问 body:

  • response.text() —— 读取 response,并以文本形式返回 response。
  • response.json() —— 将 response 解析为 JSON 格式。
  • response.formData() —— 以 FormData 对象的形式返回 response。
  • response.blob() —— 以 Blob(具有类型的二进制数据)形式返回 response。
  • response.arrayBuffer() —— 以 ArrayBuffer(低级别的二进制数据)形式返回 response。

这里默认以文本形式获取 response body。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = ref(null)
  return {
    data,
    fetch: fetch(url, { method: config.method, ...options }).then(async (response) => {
      const responseData = await response[config.type]()
      data.value = responseData
      return responseData
    })
  }
}

代码中将请求结果赋值给 data,在组件中就可以直接使用 data,但是 Fetch API 返回的 Promise 对象是在fetch 中,一般我们更习惯函数直接返回 Promise,因此返回值可以模拟一个 then 方法。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = ref(null)
  const promise = fetch(url, { method: config.method, ...options }).then(async (response) => {
    const responseData = await response[config.type]()
    data.value = responseData
    return responseData
  })
  return {
    data,
    then(onFullfilled, onRejected) {
      return promise.then(onFullfilled, onRejected)
    }
  }
}

这里会有一个疑问,返回带有 then 方法的对象可以用 await 么?

我们先来看看 await 后面可以接什么,是不是仅仅只能接 Promise?

1、await 接 Promise 实例。

这是最常用的用法,等待 Promise resolve 或 reject。resolve 后就同步执行,reject 就被 try catch 捕获,或者不处理,由上层调用方法处理。

var b = new Promise((r,reject)=>reject('Promise reject result'));
var basync = (async function(){
    try{
        await b;  
    }catch(e){
        console.log('error',e);
    }
})();

2、await 接普通对象。

这种方式 await 是多余的,但浏览器不会报错。

var c = {name: 'kenko'};
var casync = (async function(){
    try{
        await c;    // await 不起作用,等于直接同步执行
        console.log('ccc');
    }catch(e){
        console.log('error',e);
    }
})();

3、await 接 Thenable 对象。

Thenable 对象是什么呢?我们先来看看 MDN 上的定义:

在 Promise 成为 JavaScript 语言的一部分之前,JavaScript 生态系统已经有了多种 Promise 实现。尽管它们在内部的表示方式不同,但至少所有类 Promise 的对象都实现了 Thenable 接口。thenable 对象实现了 .then() 方法,该方法被调用时需要传入两个回调函数,一个用于 Promise 被兑现时调用,一个用于 Promise 被拒绝时调用。Promise 也是 thenable 对象。

也就说 Thenable 对象其实就是带有 then 方法的对象。

那么 await 遇到 Thenable 对象会做哪些事情呢?

  • 调用接的对象的 then 方法,分别传入 resolve 和 reject 作为回调
  • 如果 thenable 对象 resolve 了,那么 await 把 resolve 结果赋值给前边变量(如果有),然后同步执行下一行代码;
  • 如果 thenable 对象 reject 了,那么 await 把 reject 内容,throw 出去,所以紧接着如果有 try catch,这个 throw 内容就会被捕获。如果没有 try catch,这个 throw 会被整个 async 方法捕获,作为对上层的 reject。

因此上面示例中返回带有 then 方法的对象,await 是可以和后面接 Promise 一样处理。

我们在使用的时候经常会遇到不马上发送请求,在特定的函数中发送请求的情况,我们可以添加配置项immediate 来控制是否马上发送请求。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = ref(null)
  let promise
  const execute = async () => {
    return fetch(url, { method: config.method, ...options }).then(async (response) => {
      const responseData = await response[config.type]()
      data.value = responseData
      return responseData
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  return {
    data,
    execute,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

这里 const data = ref(null) 换成 shallowRef 更好些,因为其值都是直接赋值,没有对其属性单独修改,因此 shallowRef 性能会好些,并且再优化下把请求的 response 对象也返回,方便对返回对象做一些特殊处理的情况。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  let promise
  const execute = async () => {
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  return {
    data,
    execute,
    response,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

既然对返回值进行了响应式处理,那么对异常信息也进行响应式处理。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)

  let promise
  const execute = async () => {
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  return {
    data,
    execute,
    response,
    error,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

有时我们需要判断请求是否正在发送或者已经发送完成,因此我们在返回值中添加请求的状态响应。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)

  let promise
  const execute = async () => {
    isFetching.value = true
    isFinished.value = false
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
    }).finally(() => {
      isFetching.value = false
      isFinished.value = true
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

这里 isFetching 为 true 代表正在请求,isFinished 为 true 代表请求完成。

通过 isFetching 和 isFinished 可以更方便做 loading 的处理,并且后面设置 get、post 等方法的时候也会用到。

我们再优化下代码,将 isFetching 和 isFinished 的赋值操作封装成一个函数。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)

  let promise

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    loading(true)
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
    }).finally(() => {
      loading(false)
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

然后再添加配置项 refetch, 为 true 时, url 值改变自动重新发送请求

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)

  let promise

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    loading(true)
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
    }).finally(() => {
      loading(false)
    })
  }

  if (options.immediate) {
    promise = execute()
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

使用 toRef 是防止 refetch 和 url 不是响应式变量。

源码中在执行 execute 时候用了 Promise

Promise.resolve().then(() => execute())

这在实现设置 get、post 方法的时候会用到。

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)

  let promise

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    loading(true)
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
    }).finally(() => {
      loading(false)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,
    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

我们再添加各个阶段的钩子:请求成功,请求失败,请求完成

const config = {
  method: 'GET',
  type: 'text'
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise

  let fetchResponseFn = null
  const onFetchResponse = (callback) => {
    fetchResponseFn = callback
  }

  let fetchErrorFn = null
  const onFetchError = (callback) => {
    fetchErrorFn = callback
  }

  let fetchFinallyFn = null
  const onFetchFinally = (callback) => {
    fetchFinallyFn = callback
  }

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    loading(true)
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      fetchResponseFn(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      fetchErrorFn(fetchError)
    }).finally(() => {
      loading(false)
      fetchFinallyFn(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    onFetchResponse,
    onFetchError,
    onFetchFinally,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

使用发布订阅模式优化下

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    loading(true)
    return fetch(url, { method: config.method, ...options }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

还差一个请求之前的钩子

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    const context = { ...options }

    loading(true)

    if (options.beforeFetch)
      Object.assign(context, await options.beforeFetch(context))

    return fetch(url, { method: config.method, ...context }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

实现到这里还缺个取消请求的功能,我们想要取消执行中 fetch,可以使用 AbortController,它不仅可以中止 fetch,还可以中止其他异步任务。

首先我们先要创建一个控制器。

 let controller = new AbortController();

在控制器上 signal 属性设置事件监听器,来执行可取消操作。

然后在需要的时候调用 controller.abort() 执行取消操作。

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

// 执行可取消操作部分
// 获取 "signal" 对象,
// 并将监听器设置为在 controller.abort() 被调用时触发
signal.addEventListener('abort', () => alert("abort!"));

// 另一部分,取消(在之后的任何时候):
controller.abort(); // 中止!

// 事件触发,signal.aborted 变为 true
alert(signal.aborted); // true

和 fetch 一起使用。

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});

controller.abort();

这样就能取消执行中的 fetch。

放入 useFetch 中的代码如下:

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const supportsAbort = typeof AbortController === 'function'
  const canAbort = computed(() => supportsAbort && isFetching.value)
  const aborted = ref(false)
  let controller
  let context
  const abort = () => {
    context = { ...options }
    if (supportsAbort) {
      controller&&controller.abort()
      controller = new AbortController()
      controller.signal.onabort = () => aborted.value = true
      context.signal = controller.signal
    }
  }

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  const execute = async () => {
    abort()
    loading(true)

    if (options.beforeFetch)
      Object.assign(context, await options.beforeFetch(context))

    return fetch(url, { method: config.method, ...context }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    canAbort,
    abort,
    aborted,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

基于取消请求我们还可以实现设置请求超时时间。

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise
  let timer

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const supportsAbort = typeof AbortController === 'function'
  const canAbort = computed(() => supportsAbort && isFetching.value)
  const aborted = ref(false)
  let controller
  let context
  const abort = () => {
    context = { ...options }
    if (supportsAbort) {
      controller?.abort()
      controller = new AbortController()
      controller.signal.onabort = () => aborted.value = true
      context.signal = controller.signal
    }
  }

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  if (options.timeout)
    timer = setTimeout(abort, timeout)

  const execute = async () => {
    abort()
    loading(true)

    if (options.beforeFetch)
      Object.assign(context, await options.beforeFetch(context))

    return fetch(url, { method: config.method, ...context }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  return {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    canAbort,
    abort,
    aborted,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    }
  }
}

添加设置GET、POST、PUT方法的函数。

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise
  let timer

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const supportsAbort = typeof AbortController === 'function'
  const canAbort = computed(() => supportsAbort && isFetching.value)
  const aborted = ref(false)
  let controller
  let context
  const abort = () => {
    context = { ...options }
    if (supportsAbort) {
      controller?.abort()
      controller = new AbortController()
      controller.signal.onabort = () => aborted.value = true
      context.signal = controller.signal
    }
  }

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  if (options.timeout)
    timer = setTimeout(abort, timeout)

  const execute = async () => {
    abort()
    loading(true)

    if (options.beforeFetch)
      Object.assign(context, await options.beforeFetch(context))

    return fetch(url, { method: config.method, ...context }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  const shell = {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    canAbort,
    abort,
    aborted,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    },

    get: setMethod('GET'),
    post: setMethod('POST'),
    put: setMethod('put'),
    delete: setMethod('delete'),
    patch: setMethod('PATCH'),
    head: setMethod('HEAD'),
    options: setMethod('OPTIONS')
  }

  function setMethod(method) {
    return () => {
      if (!isFetching.value) {
        config.method = method
        return { ...shell }
      }
      return undefined
    }

  }

  return {
    ...shell
  }
}

setMethod 方法中只是设置 method 并没有执行 execute,因此需要

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

异步执行 execute。

最后是设置请求返回内容的格式。

const config = {
  method: 'GET',
  type: 'text'
}

export function createEventHook() {
  const fns = new Set()

  const off = (fn) => {
    fns.delete(fn)
  }

  const on = (fn) => {
    fns.add(fn)
    const offFn = () => off(fn)
    return { off: offFn }
  }

  const trigger = (param) => {
    return Promise.all(Array.from(fns).map(fn => fn(param)))
  }
  return {
    on,
    off,
    trigger
  }
}

export function useFetch(url, options) {
  const data = shallowRef(null)
  const response = shallowRef(null)
  const error = shallowRef(null)
  const isFinished = ref(false)
  const isFetching = ref(false)
  let promise
  let timer

  const onFetchResponse = createEventHook()
  const onFetchError = createEventHook()
  const onFetchFinally = createEventHook()

  const supportsAbort = typeof AbortController === 'function'
  const canAbort = computed(() => supportsAbort && isFetching.value)
  const aborted = ref(false)
  let controller
  let context
  const abort = () => {
    context = { ...options }
    if (supportsAbort) {
      controller?.abort()
      controller = new AbortController()
      controller.signal.onabort = () => aborted.value = true
      context.signal = controller.signal
    }
  }

  const loading = (isLoading) => {
    isFetching.value = isLoading
    isFinished.value = !isLoading
  }

  if (options.timeout)
    timer = setTimeout(abort, timeout)

  const execute = async () => {
    abort()
    loading(true)

    if (options.beforeFetch)
      Object.assign(context, await options.beforeFetch(context))

    return fetch(url, { method: config.method, ...context }).then(async (fetchResponse) => {
      response.value = fetchResponse
      const responseData = await fetchResponse[config.type]()
      data.value = responseData
      onFetchResponse.trigger(fetchResponse)
      return responseData
    }).catch(fetchError => {
      error.value = fetchError.message || fetchError.name
      onFetchError.trigger(fetchError)
    }).finally(() => {
      loading(false)
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      onFetchFinally.on(null)
    })
  }

  if (options.immediate) {
    promise = Promise.resolve().then(() => execute())
  }

  const refetch = toRef(options.refetch)
  watch([refetch, toRef(url)], ([refetch]) => {
    refetch && execute()
  }, { deep: true })


  const shell = {
    isFetching,
    isFinished,
    data,
    execute,
    response,
    error,

    canAbort,
    abort,
    aborted,

    onFetchResponse: onFetchResponse.on,
    onFetchError: onFetchError.on,
    onFetchFinally: onFetchFinally.on,

    then(onFullfilled, onRejected) {
      return promise ? promise.then(onFullfilled, onRejected) : Promise.resolve().then(onFullfilled, onRejected)
    },

    get: setMethod('GET'),
    post: setMethod('POST'),
    put: setMethod('put'),
    delete: setMethod('delete'),
    patch: setMethod('PATCH'),
    head: setMethod('HEAD'),
    options: setMethod('OPTIONS'),

    json: setType('json'),
    text: setType('text'),
    blob: setType('blob'),
    arrayBuffer: setType('arrayBuffer'),
    formData: setType('formData'),
  }

  // 这里执行get不会马上发送请求啊,还是之前的请求。
  function setMethod(method) {
    return () => {
      if (!isFetching.value) {
        config.method = method
        return { ...shell }
      }
      return undefined
    }
  }

  function setType(type) {
    return () => {
      if (!isFetching.value) {
        config.type = type
        return {
          ...shell
        }
      }
      return undefined
    }
  }

  return {
    ...shell
  }
}

这样 useFetch 函数就完成了。