封装的目标
接口调用对前端来说是很高频的操作,良好的接口封装可以简化接口调用的代码,还可以使业务代码更简单、更容易维护。本文主要讨论对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
、$fetch
的onResponse
、onResponseError
中需要弹出错误框时调用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
的使用方式与VueUse
的useFetch
一样,它们把接口和数据统一为一种响应式数据,也可以看作一种状态。如果业务上只有简单的数据查询,那么useFetch
还挺好用的。但是如果接口参数拼装比较复杂,或者接口结果也要做复杂的处理,或者这是一个提交接口,那么使用useFetch
会导致接口相关的逻辑比较散乱。我认为在写业务代码时,应该把接口参数处理、调用接口、接口结果处理视为一个不可分拆的,有较高的内聚性、复用性的完整逻辑。所以useFetch
无法直接满足我的需求,接下来尝试进行封装。
useFetch
的局限性为个人拙见,有不同意见欢迎讨论。
封装 useFetch
Nuxt3
封装了useAsyncData
、useFetch
来处理接口调用,其中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__
中的penging
为false
,服务端应该是正确的,但客户端水合时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
中的逻辑是同构逻辑,所以导出状态是合理的,就像Nuxt2
的asyncData
一样,这里封装后的一个好处是实现了目标4和5。
useFetch
或useSWR
都是把接口和数据统一为状态,这在hooks盛行的当下,是个挺有意思的玩法,但个人觉得这是多余的封装。在业务代码中,应该把接口参数拼装、接口调用、接口结果处理这一过程作为一个可复用逻辑,这样更简单易懂。
以上就是本篇文章的内容了,纯属个人拙见,欢迎讨论~