← 返回目录

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

封装请求就是把重复的 AJAX 代码抽成一个函数,统一处理 BASE_URL、错误、loading。

核心三步:
1. 统一 BASE_URL——接口地址前缀
2. 统一错误处理——401 跳登录、500 提示
3. 统一 loading——请求时显示加载中

坑:封装要灵活传 method 和 data、错误别吞了、返回 Promise 让调用方 then

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

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

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

9.2 知识点讲解

1. 为什么要封装?不封装会怎样?

不封装的话,每个页面都写一遍 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,改一处全部生效

2. 封装的 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 导入

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

// ===== 配置 =====
// 后端接口地址
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 全部集中在一处,页面只管拿数据和渲染,再也不用每个页面写一遍判断。

4. 分层调用(api 层 + 页面层)

// ===== 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 也是这个套路。

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

可能在什么地方用:

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

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

9.3 课后作业(融入本节)

练习五:通用请求封装
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 跳登录自动执行页面只管拿数据 // 响应回来后统一处理错误码