跳到主要内容

4.3 HTTP 请求与页面状态

本章目标: 能正确处理加载、成功、空数据和失败,不让网络问题变成静默的页面异常。

先认识 fetch 的行为​

fetch('/api/tasks') 返回 Promise。网络失败会拒绝,但 HTTP 400/404/500 通常仍然成功返回 Response。必须检查 response.ok。

response.json() 也可能失败,例如收到 HTML 错误页或空响应。尤其 DELETE 返回 204 No Content 时,不能继续解析 JSON。

Fetch 与 Axios

本手册用浏览器内置 fetch,先学清 HTTP 行为。有统一拦截器、上传进度等需求时可以选择 Axios;无论用哪个库,错误契约和页面状态仍需设计。

一个有边界检查的 API 模块​

先创建以下两个文件:

src/types/task.ts
export interface TaskItem {
id: number;
title: string;
isDone: boolean;
}

export function isTask(value: unknown): value is TaskItem {
if (typeof value !== 'object' || value === null) return false;
return 'id' in value && typeof value.id === 'number'
&& Number.isSafeInteger(value.id) && value.id > 0
&& 'title' in value && typeof value.title === 'string'
&& 'isDone' in value && typeof value.isDone === 'boolean';
}
src/api/tasks.ts
import { isTask, type TaskItem } from '../types/task';

export async function listTasks(signal?: AbortSignal): Promise<TaskItem[]> {
const response = await fetch('/api/tasks', { signal });
if (!response.ok) throw new Error(`加载失败:HTTP ${response.status}`);
const data: unknown = await response.json();
if (!Array.isArray(data) || !data.every(isTask)) {
throw new Error('服务端返回的任务格式不正确');
}
return data;
}

export async function deleteTask(id: number): Promise<void> {
const response = await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error(`删除失败:HTTP ${response.status}`);
// 成功响应为 204,没有 JSON 正文。
}

这两个函数假设 API 已经运行,开发时配置 Vite 代理,或直接运行附带实战项目。

页面显式显示各个状态​

src/App.vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { listTasks } from './api/tasks';
import type { TaskItem } from './types/task';

const tasks = ref<TaskItem[]>([]);
const loading = ref(false);
const error = ref('');
let activeController: AbortController | undefined;

async function load() {
if (loading.value) return;
const controller = new AbortController();
activeController = controller;
loading.value = true;
error.value = '';
try {
const result = await listTasks(controller.signal);
if (!controller.signal.aborted) tasks.value = result;
} catch (cause) {
if (!controller.signal.aborted) {
error.value = cause instanceof Error ? cause.message : '加载失败';
}
} finally {
if (!controller.signal.aborted) loading.value = false;
}
}

onMounted(load);
onUnmounted(() => activeController?.abort());
</script>

<template>
<main>
<h1>任务列表</h1>
<p v-if="loading" role="status">正在加载任务…</p>
<section v-else-if="error">
<p role="alert">{{ error }}</p>
<button type="button" @click="load">重试</button>
</section>
<p v-else-if="tasks.length === 0">还没有任务</p>
<ul v-else><li v-for="task in tasks" :key="task.id">{{ task.title }}</li></ul>
</main>
</template>

网络失败和空列表是不同状态。不要用一个“暂无数据”掩盖所有错误。刷新失败时可以保留旧列表,并单独提示数据未刷新;这属于进一步的交互改进。

创建请求​

发送 JSON 的片段
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '学习 HTTP' }),
});

必须先检查 HTTP 状态,再验证成功 JSON。服务端验证失败时读取 ProblemDetails 的 title、detail 或字段 errors;完整实现见实战。

配置属于构建环境​

Vite 通过 import.meta.env.VITE_API_BASE_URL 暴露配置。VITE_ 开头的值会进入浏览器产物,不能包含密码、数据库连接串或签名密钥。配置改变后通常需要重新构建。

练习​

关闭 API 再刷新页面,确认错误和重试按钮出现。重新启动 API 后点重试。用 Network 检查成功 DELETE 的 204 响应,确认前端没有 JSON 解析错误。

检查答案

错误信息应可见;重试能恢复页面;加载按钮不会持续禁用。204 没有正文,成功删除后按 ID 从本地列表移除即可。

参考:MDN Fetch。