← 返回目录

9. 通用请求封装(BASE_URL + 拦截器)

真实项目里,你不可能在每个页面都 new 一个 XMLHttpRequest——那样代码重复、改个 token 要改 100 个文件。正确做法是把所有 AJAX 逻辑抽到一个工具文件里,页面只负责"调一下、拿数据、渲染"。

核心思路:BASE_URL 统一配置(开发/线上只改一处)、请求拦截器(每个请求自动带 token)、响应拦截器(统一处理错误码、401 跳登录)。这就是大名鼎鼎的 axios 拦截器的思想,现在学会了以后学 axios 直接秒懂。

9.1 互动演示(模拟封装好的 request 调用)

演示封装后页面怎么调用:拦截器自动拼 BASE_URL、带 token、统一处理返回。

9.2 知识点讲解

① 为什么要封装?不封装会怎样?

不封装的话,每个页面都写一遍 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", true);
xhr1.setRequestHeader("token", localStorage.getItem("token"));
// ... 监听、判断状态码、解析 JSON、处理错误...

// 页面 B:
let xhr2 = new XMLHttpRequest();
xhr2.open("post", "http://127.0.0.1:3000/api/user/login", true);
xhr2.setRequestHeader("Content-Type", "...");
xhr2.setRequestHeader("token", localStorage.getItem("token"));
// ... 又写一遍监听、判断、解析、错误...

// 问题:
// 1. 代码重复,每个页面几十行
// 2. BASE_URL 改了要改 100 个地方
// 3. token 加请求头要加 100 个地方
// 4. 错误处理逻辑各页面各写各的,乱七八糟

如果你在项目里每个页面都直接 new XHR,以后后端改了接口地址、加了新要求,你就得逐个文件去改——因为逻辑散落在各个页面,没有集中管理。封装到 utils/request.js,改一处全部生效

② 封装的 7 个要点

// 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 导入

③ 核心代码拆解(utils/request.js)

// ===== 配置 =====
const BASE_URL = "http://127.0.0.1:3000/api"; // 后端接口地址

// ===== 请求拦截器:发请求前做的事 =====
function requestInterceptor(config) { // 每个请求发出前自动执行这个函数
  // 自动带上公共参数
  config.data = { appVersion: "1.0.0", ...config.data }; // 自动加版本号等公共参数
  // 自动从 localStorage 拿 token,塞到请求头
  const token = localStorage.getItem("token") || ""; // 从本地读 token,没有就用空串
  config.headers = config.headers || {}; // 如果没请求头对象就建一个空的
  config.headers.token = token; // 把 token 塞进请求头,每个请求自动带
  return config; // 把改好的配置返回,继续往下走
} // 请求拦截器结束

// ===== 响应拦截器:收到响应后做的事 =====
function responseInterceptor(res, status) { // 每个响应回来后自动执行这个函数
  if (status === 200) { // 网络通了,再看业务码
    // 网络通了,看业务码
    if (res.code === 0) return Promise.resolve(res.data); // 业务成功,直接把数据交出去
    if (res.code === 401) { // 登录过期了
      localStorage.removeItem("token"); // 清掉本地 token
      location.href = "/login.html"; // 跳回登录页
      return Promise.reject(res.msg); // 把错误传出去
    }
    return Promise.reject(res.msg); // 其他业务错误,统一传出去
  }
  // 网络级错误:404/500/403
  let errMsg = status === 404 ? "接口地址不存在" : // 404 = 地址写错了
    status === 500 ? "服务器内部错误" : // 500 = 后端代码崩了
    status === 403 ? "无访问权限" : // 403 = 没权限
    "网络异常(HTTP " + status + ")"; // 其他错误
  return Promise.reject(errMsg); // 把统一好的错误信息传出去
} // 响应拦截器结束

// ===== 主函数:外部调这个就行 =====
function request(method, url, data) { // 页面只调这一行,三个参数:方式、路径、数据
  return new Promise(function (resolve, reject) { // 返回 Promise,页面就能用 await
    let fullUrl = BASE_URL + url; // 拼完整地址:BASE_URL + 接口路径
    const xhr = new XMLHttpRequest(); // 叫一个送货小哥

    // GET 参数拼 URL
    if (method.toUpperCase() === "GET" && data) { // 如果是 GET 且有参数——toUpperCase 是方法要加括号
      fullUrl += "?" + new URLSearchParams(data).toString(); // 把参数拼成 key=value 拼到 ? 后面
      data = null; // GET 的 data 置空,不放 send 里
    }

    xhr.open(method, fullUrl, true); // 告诉小哥用什么方式、去哪
    if (method.toUpperCase() === "POST") { // 如果是 POST——toUpperCase 是方法要加括号
      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) // 把回信和状态码交给响应拦截器
        .then(function (data) { resolve(data); }) // 拦截器说成功 → resolve 出去
        .catch(function (err) { reject(err); }); // 拦截器说失败 → reject 出去
    }; // 监听结束

    xhr.send(data ? new URLSearchParams(data).toString() : null); // 让小哥出发,POST 把参数塞兜里——toString 是方法要加括号
  }); // Promise 结束
} // 主函数结束

export default request; // 导出给别的文件用

核心思路:拦截器把 token、错误处理、loading 全部集中在一处,页面只管拿数据和渲染,再也不用每个页面写一遍判断。

④ 分层调用(api 层 + 页面层)

// ===== api/user.js —— 接口层:只填路径和方式 =====
import request from "../utils/request.js"; // 引入封装好的 request 工具

export function getUserList(params) { // 导出"获取用户列表"接口
  return request("GET", "/user/list", params); // 调底层 request,填路径和方式
}
export function login(formData) { // 导出"登录"接口
  return request("POST", "/user/login", formData); // 调底层 request,POST 登录
}

// ===== 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" }); // 等登录完成
    localStorage.setItem("token", loginData.token); // 存 token
  } catch (err) {
    console.error("页面业务处理:", err); // 拦截器已经弹过提示了
  }
}
loadPageData(); // 页面加载时自动执行

三层职责:utils 管底层请求、api 管接口聚合(只填路径和方式)、pages 管页面渲染——各干各的,低耦合,以后学 Vue/React 也是这个套路。

⑤ 实战:用在哪 / 常见坑 / 怎么解决

可能在什么地方用:

① 任何正式项目都必须封装,不能页面里直接 new XHR;② 统一带 token、统一错误提示;③ 接口地址统一写在 BASE_URL。

常见的问题:

① 在请求拦截器里就调了 setRequestHeader——但此时 xhr 还没 open,会报错;② GET 参数忘了拼 URL,直接放 send 里;③ 页面里还重复写错误处理逻辑,和拦截器重复;④ 忘了 return Promise,页面拿不到数据。

解决思路:

① setRequestHeader 必须在 xhr.open 之后再调;② GET 拼 URL、POST 放 send,封装函数里自动处理;③ 页面里只管成功逻辑,失败交给拦截器;④ 主函数一定要 return 一个 new Promise

一句话:BASE_URL 一处配置、请求拦截器 统一带 token、响应拦截器 统一处理错误码、api 层 聚合接口、页面只 async/await 拿数据。

9.3 课后作业(融入本节)

练习五:通用请求封装——① 按课件思路创建 utils/request.js,包含 BASE_URL、请求拦截器(带 token)、响应拦截器(处理状态码);② 创建 api/user.js,导出 getMemberList 和 login 两个函数;③ 新建页面调用这两个接口,用 async/await 处理。