阅读视图

发现新文章,点击刷新页面。

Copilot新模型GPT-5.1太强了!自动生成完美Axios封装,同事都看傻了

November 13, 2025 
OpenAI’s GPT-5.1, GPT-5.1-Codex and GPT-5.1-Codex-Mini are now in public preview for GitHub Copilot

打造极致优雅的Axios封装,让接口请求回归简单...

GPT-5.1赋能:让Axios封装进入AI驱动的全新时代...

走起,直接上代码....

import axios from "axios";
import qs from "qs";
import { Message } from "element-gui";

// ==================== 配置常量 ====================
const REQUEST_CONFIG = {
  BASE_URL: process.env.VUE_APP_BASE_API,
  TIMEOUT: 60000,
  LOGIN_PATH: "/login",
  DEFAULT_ERROR_MESSAGE: "请求失败",
  ENABLE_LOG: process.env.NODE_ENV === "development",
};

// HTTP 状态码错误消息映射
const HTTP_ERROR_MESSAGES = {
  401: "未授权,请重新登录",
  403: "没有权限访问",
  404: "请求资源不存在",
  500: "服务器错误",
  502: "网关错误",
  503: "服务不可用",
  504: "网关超时",
};

// ==================== 请求管理 ====================
class RequestManager {
  constructor() {
    this.pendingRequests = new Map();
  }

  // 生成请求唯一标识
  generateKey(config) {
    const { method, url, params, data } = config;
    const serializedParams = params
      ? qs.stringify(params, { sort: (a, b) => a.localeCompare(b) })
      : "";
    const serializedData =
      data && !(data instanceof FormData)
        ? qs.stringify(data, { sort: (a, b) => a.localeCompare(b) })
        : "";

    return `${method}:${url}:${serializedParams}:${serializedData}`;
  }

  // 添加请求
  addRequest(config) {
    const requestKey = this.generateKey(config);

    // 取消相同的进行中请求
    if (this.pendingRequests.has(requestKey)) {
      const cancel = this.pendingRequests.get(requestKey);
      cancel("取消重复请求");
    }

    // 为新请求创建取消令牌
    config.cancelToken = new axios.CancelToken((cancel) => {
      this.pendingRequests.set(requestKey, cancel);
    });

    config.requestKey = requestKey;
    return config;
  }

  // 移除请求
  removeRequest(config) {
    if (config?.requestKey) {
      this.pendingRequests.delete(config.requestKey);
    }
  }

  // 清空所有请求
  clearAll() {
    this.pendingRequests.forEach((cancel) => cancel("清空所有请求"));
    this.pendingRequests.clear();
  }
}

// ==================== 工具函数 ====================
class RequestUtils {
  // 日志打印
  static log(message, data) {
    if (REQUEST_CONFIG.ENABLE_LOG) {
      console.log(`[Request] ${message}`, data);
    }
  }

  static error(message, data) {
    if (REQUEST_CONFIG.ENABLE_LOG) {
      console.error(`[Request] ${message}`, data);
    }
  }

  // Token 管理
  static getToken() {
    return localStorage.getItem("token");
  }

  static removeToken() {
    localStorage.removeItem("token");
  }

  static redirectToLogin() {
    window.location.href = REQUEST_CONFIG.LOGIN_PATH;
  }

  // Content-Type 处理
  static isFormData(data) {
    return data instanceof FormData;
  }

  static isFormUrlEncoded(headers) {
    return headers?.["Content-Type"] === "application/x-www-form-urlencoded";
  }

  static isBlobResponse(config) {
    return config.responseType === "blob";
  }
}

// ==================== Axios 实例 ====================
const requestManager = new RequestManager();

const service = axios.create({
  baseURL: REQUEST_CONFIG.BASE_URL,
  timeout: REQUEST_CONFIG.TIMEOUT,
  withCredentials: true,
  headers: {
    "Content-Type": "application/json",
  },
});

// ==================== 请求拦截器 ====================
service.interceptors.request.use(
  (config) => {
    // 处理重复请求取消
    if (config.cancelDuplicate !== false) {
      requestManager.addRequest(config);
    }

    // 处理 Authorization
    if (!config.headers?.Authorization) {
      const needToken = config.needToken !== false;
      if (needToken) {
        const token = RequestUtils.getToken();
        if (token) {
          config.headers = config.headers || {};
          config.headers["Authorization"] = `Bearer ${token}`;
          RequestUtils.log("添加 Token", { url: config.url });
        }
      }
    } else {
      RequestUtils.log("使用自定义 Authorization", { url: config.url });
    }

    // 处理 FormData
    if (RequestUtils.isFormData(config.data)) {
      delete config.headers["Content-Type"];
      RequestUtils.log("检测到 FormData", { url: config.url });
    }
    // 处理 application/x-www-form-urlencoded
    else if (RequestUtils.isFormUrlEncoded(config.headers)) {
      if (config.data && typeof config.data === "object") {
        config.data = qs.stringify(config.data);
        RequestUtils.log("序列化 Form 数据", { url: config.url });
      }
    }

    RequestUtils.log("发送请求", {
      method: config.method,
      url: config.url,
      params: config.params,
      data: config.data,
    });

    return config;
  },
  (error) => {
    RequestUtils.error("请求配置错误", error);
    if (error.config?.showError !== false) {
      Message.error("请求发送失败: " + (error.message || "未知错误"));
    }
    return Promise.reject(error);
  }
);

// ==================== 响应拦截器 ====================
service.interceptors.response.use(
  (response) => {
    // 清理请求记录
    requestManager.removeRequest(response.config);

    RequestUtils.log("收到响应", {
      url: response.config.url,
      status: response.status,
      data: response.data,
    });

    // 处理文件下载响应
    if (RequestUtils.isBlobResponse(response.config)) {
      return response;
    }

    const res = response.data;

    // 业务成功
    if (res.code === 0 && response.status === 200) {
      return res;
    }

    // 业务失败
    if (res.code !== 0) {
      // 未授权处理
      if (res.code === 401) {
        RequestUtils.removeToken();
        RequestUtils.redirectToLogin();
      }

      const errorMessage = res.message || REQUEST_CONFIG.DEFAULT_ERROR_MESSAGE;
      if (response.config?.showError !== false) {
        Message.error(errorMessage);
      }

      return Promise.reject(new Error(errorMessage));
    }

    return res;
  },
  (error) => {
    // 清理请求记录
    requestManager.removeRequest(error.config);

    // 处理取消请求
    if (axios.isCancel(error)) {
      RequestUtils.log("请求被取消", error.message);
      return Promise.reject(error);
    }

    RequestUtils.error("响应错误", error);

    let errorMessage = REQUEST_CONFIG.DEFAULT_ERROR_MESSAGE;

    // HTTP 错误
    if (error.response) {
      const status = error.response.status;
      errorMessage =
        HTTP_ERROR_MESSAGES[status] || `未知错误: ${status}`;

      // 401 处理
      if (status === 401) {
        RequestUtils.removeToken();
        RequestUtils.redirectToLogin();
      }
    }
    // 网络错误
    else if (error.request) {
      errorMessage =
        error.code === "ECONNABORTED" ? "请求超时" : "网络连接异常";
    }
    // 其他错误
    else {
      errorMessage = error.message || "请求配置错误";
    }

    if (error.config?.showError !== false) {
      Message.error(errorMessage);
    }

    return Promise.reject(error);
  }
);

// ==================== 文件下载处理 ====================
function handleDownloadResponse(response, fileName, showError = true) {
  const contentType = response.headers["content-type"] || "";

  // 检查是否为 JSON 错误响应
  if (contentType.includes("application/json")) {
    if (showError) {
      Message.error("暂无数据,无法导出!");
    }
    return;
  }

  // 从响应头获取文件名
  const disposition = response.headers["content-disposition"] || "";
  const headerFileName = disposition
    ? decodeURIComponent(
        disposition.split("filename*=UTF-8''")[1] ||
          disposition.split("filename=")[1] ||
          ""
      ).replace(/['"]/g, "")
    : "";

  // 创建下载链接
  const blob = new Blob([response.data]);
  const link = document.createElement("a");
  link.href = window.URL.createObjectURL(blob);
  link.download =
    fileName ||
    headerFileName ||
    `download_${Date.now()}.xlsx`;
  link.click();

  // 清理
  setTimeout(() => {
    window.URL.revokeObjectURL(link.href);
  }, 100);

  return response;
}

// ==================== 封装请求方法 ====================
const request = {
  /**
   * GET 请求
   * @param {string} url - 请求地址
   * @param {object} params - URL 参数
   * @param {object} config - Axios 配置
   */
  get(url, params = {}, config = {}) {
    return service({
      method: "get",
      url,
      params,
      ...config,
    });
  },

  /**
   * POST 请求
   * @param {string} url - 请求地址
   * @param {object} data - 请求体数据
   * @param {object} config - Axios 配置
   */
  post(url, data = {}, config = {}) {
    return service({
      method: "post",
      url,
      data,
      ...config,
    });
  },

  /**
   * PUT 请求
   */
  put(url, data = {}, config = {}) {
    return service({
      method: "put",
      url,
      data,
      ...config,
    });
  },

  /**
   * DELETE 请求
   */
  delete(url, params = {}, config = {}) {
    return service({
      method: "delete",
      url,
      params,
      ...config,
    });
  },

  /**
   * PATCH 请求
   */
  patch(url, data = {}, config = {}) {
    return service({
      method: "patch",
      url,
      data,
      ...config,
    });
  },

  /**
   * 文件上传
   * @param {string} url - 上传地址
   * @param {FormData} formData - 表单数据
   * @param {object} config - 配置
   */
  upload(url, formData, config = {}) {
    return service({
      method: "post",
      url,
      data: formData,
      headers: {
        "Content-Type": "multipart/form-data",
        ...(config.headers || {}),
      },
      cancelDuplicate: false, // 上传不取消重复请求
      ...config,
    });
  },

  /**
   * 文件下载
   * @param {string} url - 下载地址
   * @param {object} data - 请求数据
   * @param {string} fileName - 文件名
   * @param {object} config - 配置
   */
  download(url, data = {}, fileName = "", config = {}) {
    return service({
      method: "post",
      url,
      data,
      responseType: "blob",
      cancelDuplicate: false, // 下载不取消重复请求
      ...config,
    })
      .then((response) =>
        handleDownloadResponse(response, fileName, config.showError !== false)
      )
      .catch((error) => {
        if (config.showError !== false && !axios.isCancel(error)) {
          Message.error("下载失败!");
        }
        return Promise.reject(error);
      });
  },

  /**
   * 表单提交 (application/x-www-form-urlencoded)
   */
  form(url, data = {}, config = {}) {
    return service({
      method: "post",
      url,
      data,
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        ...(config.headers || {}),
      },
      ...config,
    });
  },

  /**
   * 自定义请求
   * @param {string} method - 请求方法
   * @param {string} url - 请求地址
   * @param {object} data - 请求体数据
   * @param {object} params - URL 参数
   * @param {object} config - 配置
   */
  custom(method, url, data = {}, params = {}, config = {}) {
    return service({
      method,
      url,
      data,
      params,
      ...config,
    });
  },

  /**
   * 取消所有进行中的请求
   */
  cancelAll() {
    requestManager.clearAll();
  },
};

export default request;
export { service, requestManager };

主要改进点

1. 代码结构更清晰

  • 使用类封装相关功能,职责划分明确
  • 配置集中管理,便于维护
  • 代码分层更合理

2. 安全性提升

  • 生产环境不输出敏感日志
  • Token 管理更规范
  • 错误信息标准化

3. 可维护性增强

  • JSDoc 注释完善
  • 错误消息统一管理
  • 配置可扩展性强

4. 功能更完善

  • 支持手动取消所有请求
  • 文件下载处理更健壮
  • 默认 Content-Type 设置

5. 调试更友好

  • 开发环境详细日志
  • 请求/响应完整追踪
  • 错误定位更精准

使用示例

基础使用

import request from '@/utils/request'

// ==================== GET 请求 ====================
export function getUserList(params) {
  return request.get('/api/users', params)
}

// 自定义配置
export function getUserListNoAuth(params) {
  return request.get('/api/users', params, {
    needToken: false,  // 不需要 token
    showError: false,  // 不显示错误提示
  })
}

// ==================== POST 请求 ====================
export function createUser(data) {
  return request.post('/api/users', data)
}

// 禁用重复请求取消
export function sendOtp(data) {
  return request.post('/api/send-otp', data, {
    cancelDuplicate: false,  // 允许重复发送
  })
}

// ==================== PUT/DELETE 请求 ====================
export function updateUser(id, data) {
  return request.put(`/api/users/${id}`, data)
}

export function deleteUser(id) {
  return request.delete(`/api/users/${id}`)
}

// ==================== 表单提交 ====================
export function login(data) {
  return request.form('/api/login', data)
}

// ==================== 文件上传 ====================
export function uploadAvatar(file) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('type', 'avatar')
  
  return request.upload('/api/upload', formData, {
    onUploadProgress: (progressEvent) => {
      const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
      console.log('上传进度:', percent + '%')
    }
  })
}

// ==================== 文件下载 ====================
export function exportUsers(params) {
  return request.download('/api/users/export', params, '用户列表')
}

// 自定义文件名
export function exportReport(params) {
  const fileName = `报表_${new Date().toLocaleDateString()}`
  return request.download('/api/report/export', params, fileName)
}

// ==================== 自定义 Authorization ====================
export function callThirdPartyApi(data) {
  return request.post('/api/third-party', data, {
    headers: {
      'Authorization': 'Bearer your-custom-token',
      'X-Api-Key': 'your-api-key'
    }
  })
}

在 Vue 组件中使用

<template>
  <div>
    <!-- 用户列表 -->
    <el-table :data="userList" v-loading="loading">
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="email" label="邮箱" />
      <el-table-column label="操作">
        <template slot-scope="{ row }">
          <el-button @click="handleEdit(row)">编辑</el-button>
          <el-button @click="handleDelete(row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 文件上传 -->
    <el-upload
      action="#"
      :http-request="handleUpload"
      :before-upload="beforeUpload"
    >
      <el-button>上传头像</el-button>
    </el-upload>

    <!-- 导出按钮 -->
    <el-button @click="handleExport">导出数据</el-button>
  </div>
</template>

<script>
import { getUserList, deleteUser, uploadAvatar, exportUsers } from '@/api/user'

export default {
  data() {
    return {
      userList: [],
      loading: false,
      queryParams: {
        page: 1,
        size: 10
      }
    }
  },

  created() {
    this.fetchUsers()
  },

  methods: {
    // 获取用户列表
    async fetchUsers() {
      try {
        this.loading = true
        const res = await getUserList(this.queryParams)
        this.userList = res.data
      } catch (error) {
        // 错误已在拦截器中处理
        console.error('获取用户列表失败', error)
      } finally {
        this.loading = false
      }
    },

    // 删除用户
    async handleDelete(id) {
      try {
        await this.$confirm('确认删除该用户?', '提示', {
          type: 'warning'
        })

        await deleteUser(id)
        this.$message.success('删除成功')
        this.fetchUsers()
      } catch (error) {
        if (error !== 'cancel') {
          console.error('删除失败', error)
        }
      }
    },

    // 上传前验证
    beforeUpload(file) {
      const isImage = file.type.startsWith('image/')
      const isLt2M = file.size / 1024 / 1024 < 2

      if (!isImage) {
        this.$message.error('只能上传图片文件!')
        return false
      }
      if (!isLt2M) {
        this.$message.error('图片大小不能超过 2MB!')
        return false
      }
      return true
    },

    // 自定义上传
    async handleUpload({ file }) {
      try {
        const res = await uploadAvatar(file)
        this.$message.success('上传成功')
        console.log('文件地址:', res.data.url)
      } catch (error) {
        console.error('上传失败', error)
      }
    },

    // 导出数据
    async handleExport() {
      try {
        await exportUsers(this.queryParams)
        // 下载成功后无需额外处理
      } catch (error) {
        console.error('导出失败', error)
      }
    }
  },

  // 组件销毁时取消所有请求
  beforeDestroy() {
    // 如果需要取消所有请求
    // request.cancelAll()
  }
}
</script>

高级用法

import request from '@/utils/request'

// ==================== 并发请求 ====================
export async function fetchDashboardData() {
  try {
    const [users, orders, statistics] = await Promise.all([
      request.get('/api/users'),
      request.get('/api/orders'),
      request.get('/api/statistics')
    ])

    return {
      users: users.data,
      orders: orders.data,
      statistics: statistics.data
    }
  } catch (error) {
    console.error('获取数据失败', error)
    throw error
  }
}

// ==================== 串行请求(有依赖关系)====================
export async function createOrderWithDetails(orderData, itemsData) {
  try {
    // 先创建订单
    const orderRes = await request.post('/api/orders', orderData)
    const orderId = orderRes.data.id

    // 再添加订单明细
    const itemsRes = await request.post('/api/order-items', {
      orderId,
      items: itemsData
    })

    return {
      order: orderRes.data,
      items: itemsRes.data
    }
  } catch (error) {
    console.error('创建订单失败', error)
    throw error
  }
}

// ==================== 轮询请求 ====================
export function pollTaskStatus(taskId, interval = 2000, maxAttempts = 30) {
  let attempts = 0

  return new Promise((resolve, reject) => {
    const timer = setInterval(async () => {
      attempts++

      try {
        const res = await request.get(`/api/tasks/${taskId}`, {}, {
          showError: false  // 轮询不显示错误
        })

        if (res.data.status === 'completed') {
          clearInterval(timer)
          resolve(res.data)
        } else if (res.data.status === 'failed') {
          clearInterval(timer)
          reject(new Error('任务执行失败'))
        } else if (attempts >= maxAttempts) {
          clearInterval(timer)
          reject(new Error('轮询超时'))
        }
      } catch (error) {
        clearInterval(timer)
        reject(error)
      }
    }, interval)
  })
}

// ==================== 重试机制 ====================
export async function requestWithRetry(fn, maxRetries = 3) {
  let lastError

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      console.log(`请求失败,第 ${i + 1} 次重试...`)
      
      // 等待一段时间再重试
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }

  throw lastError
}

// 使用示例
export function fetchImportantData() {
  return requestWithRetry(() => request.get('/api/important-data'))
}

// ==================== 请求队列 ====================
class RequestQueue {
  constructor(concurrency = 3) {
    this.concurrency = concurrency
    this.running = 0
    this.queue = []
  }

  async add(fn) {
    if (this.running >= this.concurrency) {
      await new Promise(resolve => this.queue.push(resolve))
    }

    this.running++
    try {
      return await fn()
    } finally {
      this.running--
      const resolve = this.queue.shift()
      if (resolve) resolve()
    }
  }
}

// 批量上传文件(限制并发数)
export async function batchUpload(files) {
  const queue = new RequestQueue(3) // 最多同时上传 3 个
  const results = []

  for (const file of files) {
    const result = await queue.add(() => uploadAvatar(file))
    results.push(result)
  }

  return results
}

全局配置和扩展

import request, { service, requestManager } from '@/utils/request'

// ==================== 全局请求前置处理 ====================
export function setupRequestInterceptor() {
  // 添加请求追踪
  service.interceptors.request.use(config => {
    config.metadata = { startTime: Date.now() }
    return config
  })

  // 添加响应时间统计
  service.interceptors.response.use(
    response => {
      const duration = Date.now() - response.config.metadata.startTime
      console.log(`请求耗时: ${duration}ms`)
      return response
    },
    error => {
      if (error.config?.metadata) {
        const duration = Date.now() - error.config.metadata.startTime
        console.log(`请求失败耗时: ${duration}ms`)
      }
      return Promise.reject(error)
    }
  )
}

// ==================== 路由守卫中取消请求 ====================
export function setupRouterCancelRequests(router) {
  router.beforeEach((to, from, next) => {
    // 路由切换时取消所有进行中的请求
    requestManager.clearAll()
    next()
  })
}

// ==================== 在 main.js 中使用 ====================
// import { setupRequestInterceptor, setupRouterCancelRequests } from '@/utils/request-extension'
// setupRequestInterceptor()
// setupRouterCancelRequests(router)

image.png

从零到一:编写一个简单的 Umi 插件并发布到 npm

 一、前言

        最近在学习 Umi 框架时,发现它的插件机制特别灵活。于是我尝试写一个最简单的 Umi 插件,比如在启动项目时输出一句提示语。

        做完这个我们可以了解插件的基本规范,以及如何打包发布到npm,如何让别人npm install 直接安装使用。

二、创建插件项目

        创建项目文件夹    编辑

编辑

        这样项目就创建好了,而且携带了package.json文件,现在我们需要添加一个依赖umi,然后安装umi。

编辑

执行pnpm install 就自动安装好依赖了。

三、注册登录npm然后发布本地插件到npm

        注册到npm官网就可以,这里我直接登录好了。

        编辑

        登录之后就可以直接npm publish 直接上传到npm了。

        也可以用命令注册,npm adduser 然后输入账户密码就可以了,邮箱也需要填写。

4、发布之后通过npm install 安装插件并且使用

        首先可以本地写好插件上传发布之前在自己项目用pnpm link 插件名称本地测试一下。

        首先在插件项目目录pnpm link 将插件包link到npm 或者pnpm环境

编辑

        然后在项目目录npm link 插件包名运行即可

编辑

        在自己的项目输入npm install 自己的插件名称。

编辑

编辑

        这样插件就安装完成了。

        安装完之后我们在自己的umi项目里面,在.umirc.ts配置或者config/config.ts中配置defineConfig中的plugins插件配置项就可以了。

import { defineConfig } from "umi";

export default defineConfig({
  routes: [
    { path: "/", component: "index" },
    { path: "/docs", component: "docs" },
    { path: "/login", component: "login", layout: false },
    { path: "/weather", component: "weather" },
    { path: "/promise", component: "Promise" },
    { path: "/aiChat", component: "aiChat", layout: false },
    { path: "/websocket", component: "websocket", layout: false },
    { path: "/test", component: "test", layout: false },
    { path: "/streamingChat", component: "streamingChat", layout: false },
    { path: "/fileUploadDemo", component: "FileUploadDemo", layout: false },
  ],
  npmClient: "pnpm",
  mock: {
    include: ["src/mock/**/*.ts"], // 启用mock功能并指定mock文件位置
  },
  plugins: ["umi-plugin-hello-lcy"],
});

        运行项目控制台会输出

        编辑

5、常见问题       

问题 原因 解决办法
npm error EPRIVATE package.json"private": true 删除该字段
npm error 402 Payment Required 使用了作用域包(@xxx/)且没设置公开 加上 --access public
插件未生效 没在 Umi 配置文件中启用 确认在 plugins 数组中声明

6、总结

这一套流程下来,你已经理解了 Umi 插件的基本结构,能在本地测试插件(npm link 插件名称),成功将插件发布到 npm,
并能被其他项目直接安装使用。

值得一提的工具

  • nrm包镜像源管理工具类似于nvm,管理镜像源
  • 如果要在npm公布上传插件包,需要切换到对应npm镜像源然后登录
  • 如果公司内网也需要切换到内网镜像源
  • 相关命令为 nrm ls 查看当前镜像源以及使用的镜像源
  • nrm use 切换镜像源
❌