$api
介绍
$api 是通过 Nuxt 插件注入的全局 $fetch 实例,内置完整的拦截器链(认证注入、业务状态码检查、数据解包、Toast 提示)。它是所有 API composable(useApiFetch、useLazyApiFetch 等)的底层引擎。
用法
const { $api } = useNuxtApp()
// GET 请求(数据已自动解包)
const users = await $api<User[]>('/users')
// POST 请求
const user = await $api<User>('/users', {
method: 'POST',
body: { name: 'test', email: 'test@example.com' }
})
// PUT 请求
const updated = await $api<User>(`/users/${id}`, {
method: 'PUT',
body: { name: 'new-name' }
})
// DELETE 请求
await $api(`/users/${id}`, { method: 'DELETE' })
$api 返回的数据已经过拦截器自动解包(提取 dataKey 字段),泛型 T 直接对应业务数据类型。多端点切换
使用 $api.use(endpoint) 切换到指定端点:
const { $api } = useNuxtApp()
// 使用默认端点
const users = await $api('/users')
// 切换到 admin 端点
const adminUsers = await $api.use('admin')('/users')
// 保存端点引用,避免重复 use()
const adminApi = $api.use('admin')
const config = await adminApi('/config')
const stats = await adminApi('/stats')
端点实例内部有缓存机制,多次调用 $api.use('admin') 返回同一实例。
切换并对照 baseURL
$api 默认走 default 端点的 baseURL,$api.use(name) 切换到别名端点后请求改用对应 baseURL。下拉切换端点即可观察 baseURL 与返回随之变化:
$api 默认走 default 端点的 baseURL,$api.use(name) 切换到别名端点后请求改用对应 baseURL。
<script setup lang="ts">
const props = defineProps<{
endpoint: 'default' | 'v2'
}>()
const { $api } = useNuxtApp()
const publicConfig = useRuntimeConfig().public.movkApi
const data = ref<unknown>(null)
const pending = ref(false)
const error = ref<string | null>(null)
const baseURL = computed(() => publicConfig.endpoints?.[props.endpoint]?.baseURL)
async function load() {
pending.value = true
error.value = null
try {
data.value = await $api.use(props.endpoint)('/profile')
}
catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
finally {
pending.value = false
}
}
watch(() => props.endpoint, load, { immediate: true })
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2 flex-wrap">
<UBadge :color="endpoint === 'v2' ? 'secondary' : 'neutral'" variant="subtle">
baseURL: {{ baseURL }}
</UBadge>
<UButton size="sm" variant="outline" icon="i-lucide-refresh-cw" :loading="pending" @click="load">
刷新
</UButton>
</div>
<p class="text-xs text-muted">
$api 默认走 default 端点的 baseURL,$api.use(name) 切换到别名端点后请求改用对应 baseURL。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ error ? { error } : data }}</pre>
</div>
</template>
命令式调用
脱离 useFetch 上下文,直接通过 $api.use(name) 取得端点实例并发起裸请求:
<script setup lang="ts">
const { $api } = useNuxtApp()
const data = ref<unknown>(null)
const pending = ref(false)
const error = ref<string | null>(null)
async function call() {
pending.value = true
error.value = null
try {
data.value = await $api.use('v2')('/profile')
}
catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
finally {
pending.value = false
}
}
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" icon="i-lucide-zap" :loading="pending" @click="call">
$api.use('v2')('/profile')
</UButton>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ error ? { error } : data }}</pre>
</div>
</template>
当前生效配置
runtimeConfig.public.movkApi 暴露端点列表与默认端点名,便于运行时调试:
在 nuxt.config 的 movk.api.endpoints 注册新别名后即可被 endpoint 选项引用。
{
"defaultEndpoint": "default",
"endpoints": {
"default": {
"baseURL": "/api"
},
"v2": {
"baseURL": "/api/demo/v2"
}
}
}<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
</script>
<template>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted">
在 <code>nuxt.config</code> 的 <code>movk.api.endpoints</code> 注册新别名后即可被 <code>endpoint</code> 选项引用。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ { defaultEndpoint: publicConfig.defaultEndpoint, endpoints: publicConfig.endpoints } }}</pre>
</div>
</template>
认证集成
模块自动接入 nuxt-auth-utils:登录后从 session[sessionTokenPath] 读取 token,所有 $api 请求自动注入 Authorization 头;遇到 401 时按 auth.unauthorized 策略清理 session 或跳转登录页。
当前 Session
useUserSession 暴露的响应式 session;登录 / 登出后调用 fetch() 同步状态:
sessionTokenPath: token
{
"id": "a5009294-11c6-46b8-9391-c2344af8275b"
}<script setup lang="ts">
const { session, loggedIn, fetch: refreshSession } = useUserSession()
const { $api } = useNuxtApp()
const publicConfig = useRuntimeConfig().public.movkApi
const loginPending = ref(false)
const logoutPending = ref(false)
async function login() {
loginPending.value = true
try {
await $api('/demo/login', { method: 'POST' })
await refreshSession()
}
finally {
loginPending.value = false
}
}
async function logout() {
logoutPending.value = true
try {
await $api('/demo/logout', { method: 'POST' })
await refreshSession()
}
finally {
logoutPending.value = false
}
}
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2 flex-wrap">
<UBadge :color="loggedIn ? 'success' : 'neutral'" variant="subtle">
{{ loggedIn ? '已登录' : '未登录' }}
</UBadge>
<UButton
v-if="!loggedIn"
size="sm"
:loading="loginPending"
icon="i-lucide-log-in"
@click="login"
>
登录(写入 session.token)
</UButton>
<UButton
v-else
size="sm"
color="error"
variant="outline"
:loading="logoutPending"
icon="i-lucide-log-out"
@click="logout"
>
登出(清除 session)
</UButton>
</div>
<p class="text-xs text-muted">
sessionTokenPath: {{ publicConfig.auth?.sessionTokenPath }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ session }}</pre>
</div>
</template>
调用受保护接口
useApiFetch 自动携带 Authorization 头访问 /demo/protected;未登录时返回 401:
未登录时拦截器返回 401;已登录则携带 Authorization 头
<script setup lang="ts">
const protectedRes = useApiFetch('/demo/protected', { immediate: false })
const display = computed(() => {
if (protectedRes.error.value) {
return {
statusCode: protectedRes.error.value.statusCode,
message: protectedRes.error.value.message
}
}
return protectedRes.data.value
})
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" icon="i-lucide-shield-check" @click="protectedRes.execute()">
请求 /demo/protected
</UButton>
<p class="text-xs text-muted">
未登录时拦截器返回 401;已登录则携带 Authorization 头
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ display }}</pre>
</div>
</template>
401 处理策略
auth.unauthorized 控制重定向与 session 清理;docs 站点为便于演示关闭重定向:
auth.unauthorized 控制 401 时的行为;docs 站点关闭重定向便于演示,生产建议启用 redirect: true + loginPath。
{
"redirect": false,
"clearSession": true,
"loginPath": "/login"
}<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
</script>
<template>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted">
<code>auth.unauthorized</code> 控制 401 时的行为;docs 站点关闭重定向便于演示,生产建议启用 <code>redirect: true</code> + <code>loginPath</code>。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ publicConfig.auth?.unauthorized }}</pre>
</div>
</template>
令牌注入格式
headerName + tokenType 决定最终请求头形态;后端 echo authHeader 字段供核对:
{
"headerName": "Authorization",
"tokenType": "Bearer",
"echoed": null
} 登录后再次请求,echoed 字段会展示拦截器实际注入的 Authorization 头。
<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
const protectedRes = useApiFetch('/demo/protected', { immediate: false })
const echoed = computed(() => {
const data = protectedRes.data.value
return data && typeof data === 'object'
? (data as { authHeader?: string }).authHeader
: null
})
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" variant="outline" icon="i-lucide-send" @click="protectedRes.execute()">
请求 /demo/protected 查看注入
</UButton>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ {
headerName: publicConfig.auth?.headerName ?? 'Authorization',
tokenType: publicConfig.auth?.tokenType ?? 'Bearer',
echoed
} }}</pre>
<p class="text-xs text-muted">
登录后再次请求,<code>echoed</code> 字段会展示拦截器实际注入的 Authorization 头。
</p>
</div>
</template>
数据解包与业务校验
$api 默认按 response.dataKey 抽取业务数据并按 successCodes 校验业务码;两个开关 skipUnwrap / skipBusinessCheck 让调用方按场景接管。下拉切换模式观察返回结构差异:
default:按默认data字段抽取业务数据,调用方直接拿到去信封后的对象。skip-unwrap:skipUnwrap: true不重写response._data,保留完整code/message/data信封。skip-business-check:legacy 接口未返回code字段,跳过校验后仍按dataKey解包出业务数据。combined:外部接口信封非标,同时开两开关由调用方完全自处理响应。
按 dataKey(默认 data)抽取业务数据,调用方直接拿到去信封后的对象。
<script setup lang="ts">
const props = defineProps<{
mode: 'default' | 'skip-unwrap' | 'skip-business-check' | 'combined'
}>()
const fetchers = {
'default': useApiFetch('/profile', { key: 'unwrap-default', immediate: false }),
'skip-unwrap': useApiFetch('/profile', { key: 'unwrap-skip-unwrap', immediate: false, skipUnwrap: true }),
'skip-business-check': useApiFetch('/demo/legacy', { key: 'unwrap-skip-business-check', immediate: false, skipBusinessCheck: true }),
'combined': useApiFetch('/demo/external', { key: 'unwrap-combined', immediate: false, skipBusinessCheck: true, skipUnwrap: true })
}
const active = computed(() => fetchers[props.mode])
const hints: Record<typeof props.mode, string> = {
'default': '按 dataKey(默认 data)抽取业务数据,调用方直接拿到去信封后的对象。',
'skip-unwrap': 'skipUnwrap: true 不重写 response._data,保留完整 code/message/data 信封。',
'skip-business-check': 'legacy 接口未返回 code 字段,跳过校验后仍按 dataKey 解包出业务数据。',
'combined': 'external 接口信封非标,同时开两开关由调用方完全自处理响应。'
}
watch(() => props.mode, () => active.value.execute(), { immediate: true })
</script>
<template>
<div class="flex flex-col gap-3">
<UBadge variant="subtle">
{{ mode }}
</UBadge>
<p class="text-xs text-muted">
{{ hints[mode] }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ active.data.value }}</pre>
</div>
</template>
Toast 提示
$api 内置 Toast 提示,按 toast.successCodes 判定成功 / 失败;支持全局默认 + 请求级覆盖两层定制。下拉切换模式、配合「注入错误」开关观察成功 / 失败两条分支:
default:未传toast时继承全局配置,成功 / 错误均按默认样式弹出。silent:toast: false关闭所有单次提示。message:successMessage/errorMessage替换默认文案。error-only:{ success: false }关闭成功提示但保留错误提示。custom:成功 / 错误分别覆盖color、icon等 ToastProps。
未传 toast 时继承全局配置,成功 / 错误均按默认样式弹出。
<script setup lang="ts">
const props = defineProps<{
mode: 'default' | 'silent' | 'message' | 'error-only' | 'custom'
}>()
const fail = ref(false)
const target = () => `/profile${fail.value ? '?fail=1' : ''}`
const fetchers = {
'default': useApiFetch(target, { key: 'toast-default', immediate: false }),
'silent': useApiFetch(target, { key: 'toast-silent', immediate: false, toast: false }),
'message': useApiFetch(target, {
key: 'toast-message',
immediate: false,
toast: { successMessage: '加载成功 ✓', errorMessage: '加载失败 ✗' }
}),
'error-only': useApiFetch(target, { key: 'toast-error-only', immediate: false, toast: { success: false } }),
'custom': useApiFetch(target, {
key: 'toast-custom',
immediate: false,
toast: {
success: { color: 'secondary', icon: 'i-lucide-sparkles' },
error: { color: 'warning', icon: 'i-lucide-triangle-alert' }
}
})
}
const active = computed(() => fetchers[props.mode])
const hints: Record<typeof props.mode, string> = {
'default': '未传 toast 时继承全局配置,成功 / 错误均按默认样式弹出。',
'silent': 'toast: false 关闭所有单次提示。',
'message': 'successMessage / errorMessage 替换默认文案。',
'error-only': '{ success: false } 关闭成功提示但保留错误提示。',
'custom': '为成功 / 错误分别覆盖 color、icon 等 ToastProps。'
}
const display = computed(() => active.value.error.value
? { error: active.value.error.value.message }
: active.value.data.value)
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3 flex-wrap">
<USwitch v-model="fail" label="注入错误(验证错误分支)" />
<UButton size="sm" variant="outline" icon="i-lucide-send" @click="active.execute()">
触发请求
</UButton>
</div>
<p class="text-xs text-muted">
{{ hints[mode] }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ display }}</pre>
</div>
</template>
在事件处理器中使用
事件处理器中使用 $api 是最常见的场景:
<script setup lang="ts">
const { $api } = useNuxtApp()
// 表单提交
async function handleCreate(formData: CreateUserRequest) {
try {
const user = await $api<User>('/users', {
method: 'POST',
body: formData
})
// Toast 已自动显示,无需手动处理
navigateTo(`/users/${user.id}`)
}
catch (error) {
// 业务错误 Toast 已自动显示
// 此处可做额外逻辑(如表单字段高亮)
}
}
// 删除确认
async function handleDelete(id: string) {
await $api(`/users/${id}`, { method: 'DELETE' })
await refreshNuxtData() // 刷新页面缓存数据
}
</script>
在 useAsyncData 中使用
当需要聚合多个接口或做复杂数据处理时,在 useAsyncData 中使用 $api:
const { $api } = useNuxtApp()
// 聚合多个接口
const { data: dashboard } = await useAsyncData('dashboard', async () => {
const [users, stats, recent] = await Promise.all([
$api<User[]>('/users'),
$api<Stats>('/stats'),
$api<Activity[]>('/activity/recent')
])
return { users, stats, recent }
})
封装自定义 composable
export function useDashboard() {
const { $api } = useNuxtApp()
return useAsyncData('dashboard', async () => {
const [users, stats] = await Promise.all([
$api<User[]>('/users'),
$api<Stats>('/stats')
])
return { users, stats }
})
}
业务操作 composable
对于删除、审批等写操作,将 $api 调用封装到独立的 composable 中,保持页面文件精简:
export function useUserActions() {
const { $api } = useNuxtApp()
async function remove(id: string) {
await $api(`/users/${id}`, { method: 'DELETE' })
await refreshNuxtData()
}
async function create(data: CreateUserRequest) {
return await $api<User>('/users', {
method: 'POST',
body: data,
})
}
async function updateRole(id: string, role: UserRole) {
return await $api<User>(`/users/${id}/role`, {
method: 'PUT',
body: { role },
context: { toast: { successMessage: '角色已更新' } },
})
}
return { remove, create, updateRole }
}
<script setup lang="ts">
// 数据获取用 useApiFetch
const { data: users, refresh } = await useApiFetch<User[]>('/users')
// 写操作用 $api composable
const { remove, updateRole } = useUserActions()
</script>
请求级配置
通过 context 字段传递请求级配置:
const { $api } = useNuxtApp()
// 禁用 Toast
await $api('/users', {
context: { toast: false }
})
// 自定义 Toast 消息
await $api('/users', {
method: 'POST',
body: formData,
context: {
toast: { successMessage: '创建成功' }
}
})
// 跳过业务状态码检查(直接返回原始解包数据)
const raw = await $api('/external-api/data', {
context: { skipBusinessCheck: true }
})
context 字段通过 ofetch 的 FetchOptions 扩展传递,仅 Movk Nuxt 的 $api 实例识别此字段。原生 $fetch 不会处理这些选项。API
ApiInstance
$api 的类型定义:
type ApiInstance = $Fetch & {
use: (endpoint: string) => ApiInstance
}
context 字段。$api 实例。endpoint 类型为 MovkApiEndpointName(由模块按 movk.api.endpoints 声明合并出的字面量联合)。端点实例内部有缓存,多次调用返回同一实例。ApiFetchContext
通过 options.context 传递的扩展配置:
false 禁用所有 Toast。true 时不抛出 ApiError,但仍会解包 dataKey 字段。true 时拦截器不重写 response._data,调用方拿到 code/message/data 完整信封。与 skipBusinessCheck 正交。