【JavaScript】async/await 与 Fetch 传参,PUT,PATCH,文件上传,批量删除等前端案例
看懂调用流程里用发起请求,try/catch处理成功/失败,这是前端调用后端接口的标准模式。参数对应关系GET 请求参数在 URL 上 → 后端读 QueryPOST 请求参数在 Body 里,JSON 格式 → 后端解析 JSON Body表单格式 → 后端读 PostForm错误排查思路:如果前端说“接口没反应”,先看 Fetch 的method和url是否正确;如果“参数没收到”,检查与参数
async/await 与 Fetch 传参:实战详解(后端视角)
理解 async/await
和 Fetch 的工作方式,能快速定位前后端接口交互问题(比如参数没收到、请求方式错误等)。
一、async/await 具体用法:用同步的方式写异步代码
async/await
是 Promise 的“语法糖”,目的是让异步代码(比如调用后端接口)写起来像同步代码一样直观。
核心规则:
await
只能用在async
修饰的函数里await
后面必须跟一个 Promise 对象(比如 Fetch 的返回值)- 遇到
await
时,函数会“暂停”,等待 promise 完成后再继续执行
实战示例:调用后端登录接口
假设后端有一个登录接口:
- 地址:
/api/login
- 方法:POST
- 参数:
{ username: string, password: string }
- 返回:
{ code: 200, data: { token: string }, msg: "success" }
前端调用代码:
// 1. 用 async 修饰函数,使其成为异步函数
async function login(username, password) {
try {
// 2. 用 await 等待 Fetch 请求完成(Fetch 返回 Promise)
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 告诉后端参数是 JSON 格式
},
body: JSON.stringify({ username, password }) // 转换为 JSON 字符串
});
// 3. 等待响应体解析为 JSON(response.json() 也返回 Promise)
const result = await response.json();
// 4. 处理后端返回结果(同步写法,逻辑清晰)
if (result.code === 200) {
console.log('登录成功,token:', result.data.token);
return result.data.token; // 返回 token 给调用者
} else {
console.log('登录失败:', result.msg);
throw new Error(result.msg); // 抛出自定义错误
}
} catch (error) {
// 5. 捕获所有错误(网络错误、后端报错、自己抛的错)
console.log('登录过程出错:', error.message);
return null;
}
}
// 调用异步函数(两种方式)
// 方式1:用 await(需要在另一个 async 函数里)
async function main() {
const token = await login('admin', '123456');
if (token) {
// 登录成功后的逻辑(如跳转页面)
}
}
main();
// 方式2:用 .then()(兼容非 async 环境)
login('admin', '123456').then(token => {
if (token) {
// 登录成功后的逻辑
}
});
后端视角的关键注解:
try/catch
能捕获所有异常:包括网络错误(如接口不通)、后端返回的错误状态(如 code=500)、甚至前端自己的代码错误(如变量未定义)。这对应后端的错误处理逻辑,但前端的catch
更“万能”。await
会“暂停”但不阻塞:函数内部会等,但整个 JS 线程不会卡(类似 Go 中 goroutine 等待 I/O 时会让出 CPU)。- 为什么要
await response.json()
?因为 Fetch 分两步:先拿到响应头(response
对象),再异步解析响应体(response.json()
),这和后端读取 HTTP 响应体的逻辑一致。
二、Fetch 如何传参:对应后端的 HTTP 请求解析
Fetch 是前端发起 HTTP 请求的主流 API,传参方式直接对应后端的参数接收逻辑(如 Go 的 r.FormValue
、r.Body
等)。不同请求方法(GET/POST/PUT/DELETE)的传参方式不同,这是前后端对接的高频问题点。
1. GET 请求:参数在 URL 上(对应后端的 Query 参数)
场景:获取用户列表(分页查询)
async function getUserList(page = 1, size = 10) {
// 拼接 URL 参数(用 ? 分隔,& 连接多个参数)
const url = `/api/users?page=${page}&size=${size}`;
const response = await fetch(url, {
method: 'GET' // 可以省略,Fetch 默认是 GET 方法
// GET 方法没有 body,参数全在 URL 上
});
return await response.json();
}
// 调用:获取第 2 页,每页 20 条
getUserList(2, 20);
后端接收:Go 中用
r.URL.Query().Get("page")
即可获取,和处理普通 HTTP GET 请求完全一致。
2. POST 请求:参数在请求体(Body)中
情况 A:JSON 格式(推荐,前后端数据结构对齐)
场景:创建新用户(参数较多时用 JSON)
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 必须加,告诉后端是 JSON
},
body: JSON.stringify(userData) // 转换为 JSON 字符串
});
return await response.json();
}
// 调用:传递用户信息对象
createUser({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
后端接收:Go 中需要先解析 Body,如
json.NewDecoder(r.Body).Decode(&user)
,和处理 JSON 格式的 POST 请求一致。
情况 B:表单格式(application/x-www-form-urlencoded
)
场景:简单表单提交(如登录、搜索)
async function searchProducts(keyword) {
// 用 URLSearchParams 处理表单参数
const formData = new URLSearchParams();
formData.append('keyword', keyword);
formData.append('sort', 'price'); // 可以添加多个参数
const response = await fetch('/api/products/search', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded' // 表单格式
},
body: formData // 直接传 URLSearchParams 对象
});
return await response.json();
}
// 调用:搜索关键词“手机”
searchProducts('手机');
后端接收:Go 中用
r.PostForm.Get("keyword")
即可,和处理表单提交的逻辑一致。
3. 带请求头(Headers)的请求:如认证、版本控制
场景:调用需要 Token 认证的接口
async function getOrderDetail(orderId) {
// 从本地存储获取 Token(登录时后端返回的)
const token = localStorage.getItem('token');
const response = await fetch(`/api/orders/${orderId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`, // 认证信息(JWT 常用格式)
'X-API-Version': 'v2' // 自定义头(如接口版本)
}
});
return await response.json();
}
后端接收:Go 中用
r.Header.Get("Authorization")
获取,用于身份验证逻辑。
4. Fetch 的“坑”:后端需要注意的细节
问题场景 | 前端表现 | 后端排查方向 |
---|---|---|
Fetch 默认不携带 Cookie | 后端认为“未登录”,但前端确实登录过 | 前端是否加了 credentials: 'include' (允许跨域携带 Cookie) |
4xx/5xx 不触发 catch | 接口返回 400/500,但前端没进 catch | 前端需手动判断 response.ok (response.ok 只有 200-299 才为 true) |
跨域请求失败 | 控制台报 CORS 错误 | 后端是否配置了跨域响应头(如 Access-Control-Allow-Origin ) |
总结:后端需要掌握的核心点
- 看懂调用流程:
async function
里用await fetch(...)
发起请求,try/catch
处理成功/失败,这是前端调用后端接口的标准模式。 - 参数对应关系:
- GET 请求参数在 URL 上 → 后端读 Query
- POST 请求参数在 Body 里,JSON 格式 → 后端解析 JSON Body
- 表单格式 → 后端读 PostForm
- 错误排查思路:如果前端说“接口没反应”,先看 Fetch 的
method
和url
是否正确;如果“参数没收到”,检查Content-Type
与参数格式是否匹配(JSON 对应application/json
,表单对应x-www-form-urlencoded
)。
更多案例
企业级前端场景:文件上传、PUT请求及更多实战示例
在企业级应用中,前端与后端的交互会更加复杂,涉及文件处理、部分更新、批量操作等场景。以下是几个典型场景的具体实现,包含完整代码和前后端交互要点。
一、文件上传(Multipart/Form-Data)
企业级应用中常见的头像上传、报表导入、附件上传等场景,都需要使用multipart/form-data
格式。
前端实现(文件上传)
// 企业级文件上传实现(带进度条和基本验证)
async function uploadFile(file, folderId) {
// 1. 文件验证(企业级应用必备)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
throw new Error(`文件大小不能超过${maxSize/1024/1024}MB`);
}
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.type)) {
throw new Error('只允许上传JPG、PNG和PDF文件');
}
// 2. 构建FormData(专门用于文件上传的格式)
const formData = new FormData();
formData.append('file', file); // 文件本身
formData.append('folderId', folderId); // 额外参数:文件存放的文件夹ID
formData.append('fileName', file.name); // 可选:自定义文件名
// 3. 发起上传请求
const response = await fetch('/api/files/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` // 身份验证
// 注意:上传文件时不要设置Content-Type,浏览器会自动处理
},
body: formData,
// 4. 监控上传进度(企业级体验必备)
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
console.log(`上传进度:${percent}%`);
// 实际项目中会更新进度条UI
}
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.msg || '文件上传失败');
}
return result.data; // 返回后端生成的文件ID、访问URL等信息
}
// 页面中使用示例
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
// 上传到ID为123的文件夹
const fileInfo = await uploadFile(file, '123');
console.log('上传成功', fileInfo);
// 显示上传成功的文件链接或预览
} catch (error) {
console.error('上传失败', error.message);
// 显示错误提示给用户
}
});
后端视角注解
- 前端使用
FormData
对象处理文件,对应Go后端需要用multipart
包解析- 上传进度通过
onUploadProgress
监听,后端无需特殊处理- 企业级应用必须包含文件类型、大小验证,减轻后端压力
- 实际项目中可能还会实现:
- 断点续传(通过
Range
请求头)- 大文件分片上传(切割文件为多个部分依次上传)
- 上传前MD5校验(避免重复上传)
二、PUT请求(资源全量更新)
PUT请求用于全量更新资源,在企业级应用中常用于完整更新一条记录(如更新用户完整信息、修改商品所有属性)。
前端实现(PUT请求)
// 全量更新用户信息(PUT请求典型场景)
async function updateUser(userId, userData) {
// 1. 参数验证(企业级应用必备)
if (!userId) {
throw new Error('用户ID不能为空');
}
// 2. 发起PUT请求
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT', // 使用PUT方法表示全量更新
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(userData) // 完整的用户信息对象
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.msg || `更新用户失败(${response.status})`);
}
return result.data;
}
// 使用示例:更新ID为1001的用户信息
const updatedData = {
name: '张三',
email: 'new-zhangsan@example.com',
phone: '13800138000',
department: '技术部',
status: 1 // 1表示启用,0表示禁用
};
try {
const result = await updateUser('1001', updatedData);
console.log('用户更新成功', result);
} catch (error) {
console.error('更新失败', error.message);
}
后端视角注解
- PUT请求语义上表示"全量更新",后端通常会要求提供完整的资源数据
- 与POST的区别:PUT是幂等的(多次调用结果相同),适合更新操作
- 企业级应用中,PUT请求通常需要:
- 资源ID在URL中(如
/api/users/{userId}
)- 请求体包含完整的资源数据
- 后端会根据ID找到对应资源并完全替换其内容
三、PATCH请求(资源部分更新)
PATCH请求用于部分更新资源,在企业级应用中常用于只更新需要修改的字段(如只修改用户手机号、只更新订单状态)。
前端实现(PATCH请求)
// 部分更新订单状态(PATCH请求典型场景)
async function updateOrderStatus(orderId, newStatus, remark) {
// 1. 构建只包含需要更新的字段的对象
const updateData = {
status: newStatus,
remark: remark // 只更新这两个字段
};
// 2. 发起PATCH请求
const response = await fetch(`/api/orders/${orderId}`, {
method: 'PATCH', // 使用PATCH方法表示部分更新
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(updateData) // 只包含需要更新的字段
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.msg || `更新订单状态失败(${response.status})`);
}
return result.data;
}
// 使用示例:将订单ID为"ORD20230510001"的状态改为"已发货"
try {
const result = await updateOrderStatus(
'ORD20230510001',
'shipped',
'顺丰快递:SF1234567890'
);
console.log('订单状态更新成功', result);
} catch (error) {
console.error('更新失败', error.message);
}
后端视角注解
- PATCH请求语义上表示"部分更新",后端只更新提供的字段
- 与PUT的区别:PATCH不需要提供完整资源数据,只需要提供要修改的字段
- 企业级应用中,PATCH常用于:
- 状态更新(订单状态、审批状态等)
- 部分字段修改(不影响其他字段)
- 减少数据传输量(尤其资源字段较多时)
四、批量操作(批量删除、批量更新)
企业级应用中经常需要批量处理数据,如批量删除选中项、批量更新状态等。
前端实现(批量操作)
// 1. 批量删除选中的用户
async function batchDeleteUsers(userIds) {
if (!userIds || userIds.length === 0) {
throw new Error('请选择要删除的用户');
}
const response = await fetch('/api/users/batch-delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ ids: userIds }) // 传递ID数组
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.msg || '批量删除失败');
}
return result.data;
}
// 2. 批量更新商品状态
async function batchUpdateProductsStatus(productIds, newStatus) {
if (!productIds || productIds.length === 0) {
throw new Error('请选择要更新的商品');
}
const response = await fetch('/api/products/batch-update', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
ids: productIds,
status: newStatus,
updatedBy: localStorage.getItem('userId') // 记录操作人
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.msg || '批量更新失败');
}
return result.data;
}
// 使用示例
// 批量删除ID为1001、1002、1003的用户
try {
const deleteResult = await batchDeleteUsers(['1001', '1002', '1003']);
console.log(`成功删除${deleteResult.deletedCount}个用户`);
} catch (error) {
console.error('删除失败', error.message);
}
// 批量将商品设置为"下架"状态
try {
const updateResult = await batchUpdateProductsStatus(
['P2023001', 'P2023002'],
'inactive'
);
console.log(`成功更新${updateResult.updatedCount}个商品`);
} catch (error) {
console.error('更新失败', error.message);
}
后端视角注解
- 批量操作通常使用专门的接口(如
/batch-delete
),而不是多次调用单个接口- 传递批量ID时,常用
{ ids: [] }
格式,后端可以一次性处理- 企业级应用中,批量操作需要注意:
- 权限控制(是否允许批量操作)
- 操作日志记录(记录谁批量操作了哪些资源)
- 事务处理(确保批量操作要么全部成功,要么全部失败)
- 性能考虑(大批量操作可能需要异步处理)
五、企业级请求封装(Axios为例)
在实际企业项目中,不会直接使用原生Fetch,而是封装一层请求工具(如Axios),统一处理认证、错误、拦截等。
前端实现(请求封装)
// 企业级请求工具封装(基于Axios)
import axios from 'axios';
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础URL
timeout: 30000, // 超时时间30秒
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器:添加认证信息、处理请求前逻辑
request.interceptors.request.use(
(config) => {
// 1. 添加Token认证
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 2. 处理特殊格式(如文件上传)
if (config.isFormData) {
config.headers['Content-Type'] = 'multipart/form-data';
}
// 3. 记录请求日志(生产环境可关闭)
console.log(`[请求] ${config.method} ${config.url}`, config.data);
return config;
},
(error) => {
// 请求发送失败(如网络错误)
return Promise.reject(error);
}
);
// 响应拦截器:统一处理响应、错误
request.interceptors.response.use(
(response) => {
const { data, config } = response;
// 1. 记录响应日志
console.log(`[响应] ${config.method} ${config.url}`, data);
// 2. 统一处理业务错误(如Token过期、无权限)
if (data.code !== 200) {
// Token过期,跳转登录页
if (data.code === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
return Promise.reject(new Error('登录已过期,请重新登录'));
}
// 其他业务错误
return Promise.reject(new Error(data.msg || '操作失败'));
}
// 3. 只返回数据部分,简化使用
return data.data;
},
(error) => {
// 处理HTTP错误(如404、500)
let message = '网络异常,请稍后重试';
if (error.response) {
const { status, statusText } = error.response;
message = `请求失败(${status}):${statusText}`;
// 记录HTTP错误日志
console.error(`[HTTP错误] ${status}`, error.response.config.url);
}
return Promise.reject(new Error(message));
}
);
// 封装常用请求方法,简化使用
export default {
get(url, params) {
return request.get(url, { params });
},
post(url, data, config = {}) {
return request.post(url, data, config);
},
put(url, data) {
return request.put(url, data);
},
patch(url, data) {
return request.patch(url, data);
},
delete(url, data) {
return request.delete(url, { data });
},
// 专门的文件上传方法
upload(url, formData, onProgress) {
return request.post(url, formData, {
timeout: 60000, // 上传超时时间延长到60秒
onUploadProgress: onProgress,
isFormData: true
});
}
};
后端视角注解
- 企业级封装会统一处理:
- 基础URL(方便切换开发/测试/生产环境)
- 认证信息(Token自动添加)
- 错误处理(包括HTTP错误和业务错误)
- 超时设置(不同操作设置不同超时)
- 后端接口设计需要配合这种封装:
- 返回统一格式(如
{ code, data, msg }
)- 使用标准HTTP状态码(200成功,401未授权等)
- 提供清晰的错误信息(方便前端展示给用户)
更多推荐
所有评论(0)