【JavaScript】async/await 与 Fetch 传参,PUT,PATCH,文件上传,批量删除等前端案例
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; // 10MBif (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); // 额外参数:文件存放的文件夹IDformData.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, // 从环境变量获取基础URLtimeout: 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未授权等)
- 提供清晰的错误信息(方便前端展示给用户)
这些企业级场景覆盖了大部分前后端交互需求,理解这些实现方式能帮助你更好地与前端团队协作,更快地定位和解决接口问题。如果需要了解某个具体场景的更多细节,可以继续提问。