Nuxt3 接口封装

封装的目标

接口调用对前端来说是很高频的操作,良好的接口封装可以简化接口调用的代码,还可以使业务代码更简单、更容易维护。本文主要讨论对useFetch$fetch的封装。以下是几个封装的小目标:

1、接口代理

* 本地开发时通过接口前缀来代理

* 生产环境的接口域名,在服务端使用k8s服务域名,在客户端使用普通域名

2、错误处理

* 客户端提示message

* 服务端显示error页面

3、暴露的方法既可以在服务端调用,又可以在客户端调用

4、能够将接口参数的拼装与返回结果的处理作为一个整体逻辑,换句话说就是将接口参数拼装、调用接口、结果处理的逻辑放在一个函数中

5、能够将多个接口的请求作为一个整体逻辑

我们期望的Demo:

接口定义

css 复制代码
/* 接口定义 */

export default {
  // 其中 fetchInstance 就是我们要实现的接口封装
  // 接口成功的返回结果为:{ code: '0', data: { nickname: 'xxx' }, message: '成功' }
  getUserInfo: (data, headers) => fetchInstance.get('/api/getUserInfo', data, headers),
}

使用接口

xml 复制代码
<template>
  <div>UserName:{{data?.data?.nickname}}</div>
  <el-button @click="clickRefresh" :loading="pending">刷新</el-button>
</template>

<script setup>
import apis from '@/apis/common'

const data = ref(null)
const pending = ref(false)

function getUserInfo() {
  /* 这里可以处理接口参数 */
  pending.value = true
  const { data: res } = await apis.getUserInfo()
  pending.value = false
  /* 这里可以处理接口结果 */
  data.value = res.value
}

// 在服务端和客户端都会调用接口,并且只调用一次
getUserInfo()

// 仅在客户端调用接口
function clickRefresh() {
  getUserInfo()
}
</script>

实现目标1

目标1还是比较常规的,看文档配置nuxt.config.js,然后在接口调用的时候根据是否在服务端,拼接响应的域名即可。下面是案例:

php 复制代码
export default defineNuxtConfig({
  nitro: {
    // 使用 useFetch 或 $fetch 都需要配置此参数
    devProxy: {
      '/local': { target: 'https://api.com', changeOrigin: true },
    },
    // 使用 useFetch 时,需要配置此参数
    routeRules: {
      '/local/**': { proxy: 'https://api.com/**' },
    }
  },
})

实现目标2

目标2也比较简单,在useFetch$fetchonResponseonResponseError中需要弹出错误框时调用nuxt3自带的showError即可,这里给一个案例:

vbscript 复制代码
useFetch(url, {
  onResponse({ request, options, response }) {
    const { method, baseURL, body } = options;
    const { _data, ok } = response;

    if (ok) { // 接口调用成功,response.ok为false会进入onResponseError
      const { code, message } = _data || {};
      if (code !== "0") { // 服务端操作未成功
        if (code === "xxxxxxx") {
          // 对特殊code特殊处理,比如未登录时要弹登录框
        } else {
          // 非特殊code,在客户端弹出message
          process.client && ElMessage.error(message);
        }
        process.server && console.error("response code error", method, baseURL, request, body, _data);
      } else {
        console.warn("response code success", method, baseURL, request, body);
      }
    }
  },
  onRequestError({ request, options, error }) {
    const { status, statusText } = response;
    if (process.server){
      const { method } = options;
      console.warn("response error", method, request, status);
      showError({ statusCode: status, message: statusText, fatal: true });
    } else {
      if (status >= 400 && status < 500) {
        ElMessage.error('接口不存在');
      } else {
        ElMessage.error('接口异常');
      }
    }
  },
})

useFetch 的局限性

Nuxt3提供的useFetch的使用方式与VueUseuseFetch一样,它们把接口和数据统一为一种响应式数据,也可以看作一种状态。如果业务上只有简单的数据查询,那么useFetch还挺好用的。但是如果接口参数拼装比较复杂,或者接口结果也要做复杂的处理,或者这是一个提交接口,那么使用useFetch会导致接口相关的逻辑比较散乱。我认为在写业务代码时,应该把接口参数处理、调用接口、接口结果处理视为一个不可分拆的,有较高的内聚性、复用性的完整逻辑。所以useFetch无法直接满足我的需求,接下来尝试进行封装。

useFetch的局限性为个人拙见,有不同意见欢迎讨论。

封装 useFetch

Nuxt3封装了useAsyncDatauseFetch来处理接口调用,其中useFetch就是useAsyncData + $fetch。其中$fetch使用ofetch库实现,代替了Axios

首先尝试对useFetch进行封装:

javascript 复制代码
const localEnv = "/local";
const browserApiHost = 'http://api.com';
const serverApiHost = 'http://k8s-server:8080';

function fetchWrapper(url, opts) {
  // 设置baseUrl,本地走代理,生产指定域名
  const baseUrl = process.dev
    ? useRequestURL().origin + localEnv
    : process.client
    ? browserApiHost
    : serverApiHost;

  // 服务端请求时需要手动加cookie
  if (process.server) {
    const headers = useRequestHeaders()
    opts.headers.cookie = headers.cookie
  }

  return useFetch(baseUrl + url, {
    ...opts,
    onRequest({ request, options }) {},
    onRequestError({ request, options, error }) {},
    onResponse({ request, options, response }) {},
    onResponseError({ request, options, response }) {},
  })
}

export default {
  get(url, query, headers = {}) {
    return fetchWrapper(url, { method: "GET", query, headers });
  },
  post(url, body, headers = {}) {
    return fetchWrapper(url, { method: "POST", body, headers });
  },
};

这里有一个特殊逻辑,就是通过useRequestHeader手动加cookie。直接在setup中使用useFetch不需要这样的处理。

Demo1
xml 复制代码
<template>
  <div>UserName:{{data?.data?.nickname}}</div>
  <el-button @click="clickRefresh" :loading="pending">刷新</el-button>
</template>

<script setup>
  const { data, refresh, pending } = apis.getUserInfo()

  // 客户端点击按钮后触发
  function clickRefresh() {
    refresh()
  }
</script>

实现了目标3,在服务端和客户端都能生效,包括单页应用导航时。其实抛开目标4和5,这种封装与直接使用useFetch的效果相同,但更简洁好用。

Demo2
xml 复制代码
<template>
  <div>UserName:{{data?.data?.nickname}}</div>
  <el-button @click="clickRefresh" :loading="pending">刷新</el-button>
</template>

<script setup>
  const data = useState(() => null)
  const pending = useState(() => false)
  
  async function getUserInfo2() {
    /* 这里可以处理接口参数 */
    pending.value = true
    const { data: res } = await apis.getUserInfo()
    pending.value = false
    /* 这里可以处理接口结果 */
    data.value = res.value
  }
  getUserInfo2()

  // 客户端点击按钮后触发
  function clickRefresh() {
    getUserInfo2()
  }
</script>

这里本应实现目标4和5,但发现此时会报一个警告(如下图)

调试后发现__NUXT_DATA__中的pengingfalse,服务端应该是正确的,但客户端水合时loading属性的值是true,存在不一致,可以用<ClientOnly>包裹有水合警告的组件,这样就不会警告了。目前没搞清楚水合错误的原因,有了解的同学帮忙解答一下,感谢!

利弊分析

利:达成封装目的,使用案例也与期望一致。

弊:

1、可能出现水合警告,需要<ClientOnly>做额外处理;

2、调用接口返回了响应式数据,使用时需要通过.value取值,有点多余;

3、调用接口还会返回useFetch的其他属性,这里没用到,有点浪费;

总的来说,这样封装还不够优雅。

探索 useAsyncData + $fetch

useFetch主要是由useAsyncData+$fetch实现,那我们再试试能不能封装$fetch,然后结合useAsyncData的注水能力来达成目标。

$fetch进行封装:

javascript 复制代码
const localEnv = "/local";
const browserApiHost = 'http://api.com';
const serverApiHost = 'http://k8s-server:8080';

function fetchWrapper(url, opts) {
  // 设置baseUrl,本地走代理,生产指定域名
  const newBaseUrl = process.dev
    ? useRequestURL().origin + localEnv
    : process.client
    ? browserApiHost
    : serverApiHost;

  // 服务端请求时需要手动加cookie
  if (process.server) {
    const headers = useRequestHeaders()
    opts.headers.cookie = headers.cookie
  }

  return $fetch(url, {
    baseURL: newBaseUrl,
    ...opts,
    credentials: 'include',
    onRequest({ request, options }) {},
    onRequestError({ request, options, error }) {},
    onResponse({ request, options, response }) {},
    onResponseError({ request, options, response }) {},
  }).catch(() => {});
}

export default {
  get(url, query, headers = {}) {
    return fetchWrapper(url, { method: "GET", query, headers });
  },
  post(url, body, headers = {}) {
    return fetchWrapper(url, { method: "POST", body, headers });
  },
};

这里增加了credentials: 'include',使其支持跨域携带cookie;增加了catch,防止错误暴露到全局。

Demo1
xml 复制代码
<template>
  <div>UserName:{{data?.data?.nickname}}</div>
  <el-button @click="clickRefresh" :loading="pending">刷新</el-button>
</template>

<script setup>
  const { data, refresh, pending } = useAsyncData(async () => {
    /* 这里可以处理接口参数 */
    const res = await apis.getUserInfo()
    /* 这里可以处理接口结果 */
    return res
  })

  // 客户端点击按钮后触发
  function clickRefresh() {
    refresh()
  }
</script>

这个案例就已经实现了目标1、2、3、4,第5个目标也能实现,只不过useAsyncData中调用多个接口时,需要把多个接口包在Promise.all()中才行,我没理解原理,有了解的同学帮忙解答一下,感谢!

总结

useAsyncData + $fetch的方案,其实与期望案例还有一些不同。使用useAsyncData还是导出了状态,不过useAsyncData中的逻辑是同构逻辑,所以导出状态是合理的,就像Nuxt2asyncData一样,这里封装后的一个好处是实现了目标4和5。

useFetchuseSWR都是把接口和数据统一为状态,这在hooks盛行的当下,是个挺有意思的玩法,但个人觉得这是多余的封装。在业务代码中,应该把接口参数拼装、接口调用、接口结果处理这一过程作为一个可复用逻辑,这样更简单易懂。

以上就是本篇文章的内容了,纯属个人拙见,欢迎讨论~

相关推荐
豆苗学前端3 分钟前
手把手实现支持百万级数据量、高可用和可扩展性的穿梭框组件
前端·javascript·面试
不见_3 分钟前
不想再写周报了?来看看这个吧!
前端·命令行
yinke小琪5 分钟前
JavaScript 事件冒泡与事件捕获
前端·javascript
pany7 分钟前
写代码的节奏,正在被 AI 改写
前端·人工智能·aigc
liliangrong77710 分钟前
ES2025新特性详解
前端
gzzeason18 分钟前
Ajax:现代JS发起http通信的代名词
前端·javascript·ajax
iphone10825 分钟前
一次编码,多端运行:HTML5多终端调用
前端·javascript·html·html5
老坛00143 分钟前
2025决策延迟的椭圆算子分析:锐减协同工具的谱间隙优化
前端
老坛00144 分钟前
从记录到预测:2025新一代预算工具如何通过AI实现前瞻性资金管理
前端
今禾1 小时前
" 当Base64遇上Blob,图像转换不再神秘,让你的网页瞬间变身魔法画布! "
前端·数据可视化