加载中...

Nuxt3 接口封装

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,但发现此时会报一个警告(如下图)

New Image

调试后发现__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盛行的当下,是个挺有意思的玩法,但个人觉得这是多余的封装。在业务代码中,应该把接口参数拼装、接口调用、接口结果处理这一过程作为一个可复用逻辑,这样更简单易懂。

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