封装请求就是把重复的 AJAX 代码抽成一个函数,统一处理 BASE_URL、错误、loading。
核心三步:
1. 统一 BASE_URL——接口地址前缀
2. 统一错误处理——401 跳登录、500 提示
3. 统一 loading——请求时显示加载中
坑:封装要灵活传 method 和 data、错误别吞了、返回 Promise 让调用方 then。
核心思路:BASE_URL 统一配置(开发/线上只改一处)、请求拦截器(每个请求自动带 token)、响应拦截器(统一处理错误码、401 跳登录)。这就是大名鼎鼎的 axios 拦截器的思想,现在学会了以后学 axios 直接秒懂。
不封装的话,每个页面都写一遍 new XHR → open → 监听 → send,代码重复一大片。更要命的是:如果后端要求所有请求都要带一个 token,你得去每个页面加一遍——100 个页面就要改 100 次,改漏一个就出 bug。封装就是把这些"所有请求都要做的事"集中到一个地方,改一处全局生效。
// ===== 不封装:每个页面都写一遍重复代码 =====
// 页面 A:
let xhr1 = new XMLHttpRequest();
xhr1.open("get", "http: // 127.0.0.1:3000/api/user/list",
xhr1.setRequestHeader("token", localStorage.getItem("token")); // 下面这些:监听、判断状态码、解析 JSON、处理错误
// 页面 B:
let xhr2 = new XMLHttpRequest(); xhr2.open("post", "http: // 127.0.0.1:3000/api/user/login"
xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr2.setRequestHeader("token", localStorage.getItem("token")); // 下面监听、判断、解析、错误处理,又得原封不动再写一遍
// 问题:
// 1. 代码重复,每个页面几十行
// 2. BASE_URL 改了要改 100 个地方
// 3. token 加请求头要加 100 个地方
// 4. 错误处理逻辑各页面各写各的,乱七八糟
如果你在项目里每个页面都直接 new XHR,以后后端改了接口地址、加了新要求,你就得逐个文件去改——因为逻辑散落在各个页面,没有集中管理。封装到 utils/request.js,改一处全部生效。
// 1. 单独建一个工具文件 utils/request.js
// 所有 AJAX 底层逻辑都放这里,和页面业务分开
// 2. 全局统一配置 BASE_URL
// 直接写后端接口地址就行
// 3. 请求拦截器(发请求前统一做的事)
// - 自动带 token
// - 自动加公共参数(版本号、设备信息)
// - 打开 loading 提示
// 4. 响应拦截器(收到响应后统一做的事)
// - 200 了看业务码 code
// - 401 登录过期 → 清 token → 跳登录页
// - 404/500/403 统一弹窗提示
// 5. 自动区分 GET/POST 传参位置
// GET 参数拼 URL,POST 参数放 send
// 6. 返回 Promise,页面用 async/await 调
// 页面写法:let data = await request("GET", "/user/list")
// 7. 用 export 导出,页面用 import 导入
// ===== 配置 =====
// 后端接口地址
const BASE_URL = "http: // 127.0.0.1:3000/api";
// ===== 请求拦截器:发请求前做的事 =====
// 每个请求发出前自动执行这个函数
function requestInterceptor(config) {
// 自动带上公共参数
config.data = config.data || {};
// 自动加版本号等公共参数
config.data.appVersion = "1.0.0";
// 自动从 localStorage 拿 token,塞到请求头
// 从本地读 token,没有就用空串
const token = localStorage.getItem("token") || "";
// 如果没请求头对象就建一个空的
config.headers = config.headers || {};
// 把 token 塞进请求头,每个请求自动带
config.headers.token = token;
// 把改好的配置返回,继续往下走
return config;
}
// 请求拦截器结束
// ===== 响应拦截器:收到响应后做的事 =====
// 每个响应回来后自动执行这个函数
function responseInterceptor(res, status) {
// 网络通了,再看业务码
if (status === 200) {
// 网络通了,看业务码
// 业务成功,直接把数据交出去
if (res.code === 0) return Promise.resolve(res.data);
// 登录过期了
if (res.code === 401) {
// 清掉本地 token
localStorage.removeItem("token");
// 跳回登录页
location.href = "/login.html";
// 把错误传出去
return Promise.reject(res.msg);
}
// 其他业务错误,统一传出去
return Promise.reject(res.msg);
// 网络级错误:404/500/403
}
// 404 = 地址写错了
let errMsg = status === 404 ? "接口地址不存在" :
// 500 = 后端代码崩了
status === 500 ? "服务器内部错误" :
// 403 = 没权限
status === 403 ? "无访问权限" :
// 其他错误
"网络异常(HTTP " + status + ")";
// 把统一好的错误信息传出去
return Promise.reject(errMsg);
}
// 响应拦截器结束
// ===== 主函数:外部调这个就行 =====
// 页面只调这一行,三个参数:方式、路径、数据
function request(method, url, data) {
// 返回 Promise,页面就能用 await
return new Promise(function (resolve, reject) {
// 拼完整地址:BASE_URL + 接口路径
let fullUrl = BASE_URL + url;
// 叫一个送货小哥
const xhr = new XMLHttpRequest();
// GET 参数拼 URL
// 如果是 GET 且有参数——toUpperCase 是方法要
if (method.toUpperCase() === "GET" && data) {
// 把参数拼成 key=value 拼到 ? 后面
fullUrl += "?" + new URLSearchParams(data).toString();
// GET 的 data 置空,不放 send 里
data = null;
}
// 告诉小哥用什么方式、去哪
xhr.open(method, fullUrl, true);
// 如果是 POST——toUpperCase 是方法要加括号
if (method.toUpperCase() === "POST") {
// 必写表单请求头
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
// 货到了打电话给你
xhr.onreadystatechange = function () {
// 没传完就跳过,只处理传完的
if (xhr.readyState !== 4) return;
// 存后端回信
let res;
// 翻译回信,翻译失败就给个错误对象
try { res = JSON.parse(xhr.responseText); } catch (e) { res = { code: -1, msg: "服务响应格式异常" }; }
// 交给响应拦截器处理
// 把回信和状态码交给响应拦截器
responseInterceptor(res, xhr.status)
// 拦截器说成功 → resolve 出去
.then(function (data) { resolve(data); })
// 拦截器说失败 → reject 出去
.catch(function (err) { reject(err); });
// 监听结束
};
// 让小哥出发,POST 把参数塞兜里——toString 是方
xhr.send(data ? new URLSearchParams(data).toString() : null);
// Promise 结束
});
}
// 主函数结束
// 导出给别的文件用
export default request;
核心思路:拦截器把 token、错误处理、loading 全部集中在一处,页面只管拿数据和渲染,再也不用每个页面写一遍判断。
// ===== api/user.js —— 接口层:只填路径和方式 =====
// 引入封装好的 request 工具
import request from "../utils/request.js";
// 导出"获取用户列表"接口
export function getUserList(params) {
// 调底层 request,填路径和方式
return request("GET", "/user/list", params);
}
// 导出"登录"接口
export function login(formData) {
// 调底层 request,POST 登录
return request("POST", "/user/login", formData);
}
// ===== page/index.js —— 页面层:async/await 调用 =====
// 从接口层引入两个函数
import { getUserList, login } from "../api/user.js";
// 页面加载数据函数
async function loadPageData() {
try {
// 等拿到用户列表
const list = await getUserList({ page: 1, size: 10 });
// 渲染列表
console.log("用户列表:", list);
// 等登录完成
const loginData = await login({ username: "admin", password: "123456" });
// 存 token
localStorage.setItem("token", loginData.token);
} catch (err) {
// 拦截器已经弹过提示了
console.error("页面业务处理:", err);
}
}
// 页面加载时自动执行
loadPageData();
三层职责:utils 管底层请求、api 管接口聚合(只填路径和方式)、pages 管页面渲染——各干各的,低耦合,以后学 Vue/React 也是这个套路。
可能在什么地方用:
1. 任何正式项目都必须封装,不能页面里直接 new XHR
2. 统一带 token、统一错误提示
3. 接口地址统一写在 BASE_URL。
常见的问题:
1. 在请求拦截器里就调了 setRequestHeader——但此时 xhr.open 还没调,会报错
2. GET 参数忘了拼 URL,直接放 send 里
3. 页面里还重复写错误处理逻辑,和拦截器重复
4. 忘了 return Promise,页面拿不到数据。
解决思路:
1. setRequestHeader 必须在 xhr.open 之后再调
2. GET 拼 URL、POST 放 send,封装函数里自动处理
3. 页面里只管成功逻辑,失败交给拦截器
4. 主函数一定要 return 一个 new Promise。
练习五:通用请求封装
1. 按课件思路创建 utils/request.js,包含 BASE_URL、请求拦截器(带 token)、响应拦截器(处理状态码)
2. 创建 api/user.js,导出 getMemberList 和 login 两个函数
3. 新建页面调用这两个接口,用 async/await 处理。
| API | 作用 | 参数 | 返回值 | 代码示例 |
|---|---|---|---|---|
| request(method,url,data) | 封装好的统一请求函数 | 入参:方法、地址、参数 | 返回:Promise / 结果 | let d = await request("get", "/user/list"); |
| BASE_URL | 接口地址统一前缀,只改一处 | 配置常量 | 自动拼到 url 前面 | const BASE_URL = "http://api.com"; |
| 请求拦截器 | 每个请求自动带上 token | 自动执行 | 不用每页手写 | // 请求发出前自动带上 token |
| 响应拦截器 | 统一处理错误码、401 跳登录 | 自动执行 | 页面只管拿数据 | // 响应回来后统一处理错误码 |