async/await 与 Fetch 传参:实战详解(后端视角)

理解 async/await 和 Fetch 的工作方式,能快速定位前后端接口交互问题比如参数没收到、请求方式错误等)。

一、async/await 具体用法:用同步的方式写异步代码

async/await 是 Promise 的“语法糖”,目的是让异步代码(比如调用后端接口)写起来像同步代码一样直观。

核心规则:

  1. await 只能用在 async 修饰的函数里
  2. await 后面必须跟一个 Promise 对象(比如 Fetch 的返回值)
  3. 遇到 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.FormValuer.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.okresponse.ok 只有 200-299 才为 true)
跨域请求失败 控制台报 CORS 错误 后端是否配置了跨域响应头(如 Access-Control-Allow-Origin

总结:后端需要掌握的核心点

  1. 看懂调用流程async function 里用 await fetch(...) 发起请求,try/catch 处理成功/失败,这是前端调用后端接口的标准模式。
  2. 参数对应关系
    • GET 请求参数在 URL 上 → 后端读 Query
    • POST 请求参数在 Body 里,JSON 格式 → 后端解析 JSON Body
    • 表单格式 → 后端读 PostForm
  3. 错误排查思路:如果前端说“接口没反应”,先看 Fetch 的 methodurl 是否正确;如果“参数没收到”,检查 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未授权等)
    • 提供清晰的错误信息(方便前端展示给用户)

https://github.com/0voice

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐