如何在 Axios 中处理多个 baseURL 而不造成混乱

网罗开发(小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


文章目录

    • 摘要
    • 引言
    • 多个 baseURL 的处理方案
      • 一个实例,每个请求重写 baseURL
      • 多个实例 + 共享拦截器(你的想法)
      • 实例工厂 + 服务映射(扩展性好)
      • 单个实例 + 动态路由器
    • 可运行的演示(Node + Axios)
      • 我们将构建什么
      • 服务器(Express)
      • 客户端(带有工厂 + 共享拦截器的 Axios)
      • 最小 `package.json`
    • 为什么这种结构有效
      • 共享行为而不重复
      • 服务默认值和演进
      • 可调试性和类型化
      • 适用于代理和网关
    • 三个实际场景及示例
    • 微前端命中不同域
    • 每个服务的重试和退避
    • 带有 URL 前缀路由的单个实例(网关)
    • 常见问题解答
    • 总结

摘要

许多项目最终需要调用多个后端:一个遗留的单体应用、一个新的微服务,可能还有一个第三方计费 API。它们的 baseURL 不匹配,如果不小心,你的 Axios 配置会变成一堆混乱的导入和复制的拦截器。本文展示了在 Axios 中组织多个 baseURL 的实用、生产级方法——涵盖实例工厂、共享拦截器、动态 baseURL 路由和每个请求的重写——外加一个可运行的演示(Node + Axios),你可以在本地尝试。我们还将深入探讨实际问题,如认证头、重试、取消、类型检查和 CORS。

引言

Axios 很灵活:你可以使用单个全局实例、多个作用域实例,甚至是一个按需生成配置客户端的工厂。"正确"的选择取决于你的仓库规模、服务数量以及它们关注点的差异(认证、超时、头信息)。如果你的团队目前创建一个巨大的 service 并不断动态更改 baseURL,你很快就会遇到问题:重复的拦截器逻辑、混乱的错误处理和难以追踪的 bug。让我们将其转变为清晰且可维护的结构。

多个 baseURL 的处理方案

一个实例,每个请求重写 baseURL

适用于非常小的项目或快速脚本。

import axios from "axios";const http = axios.create({ baseURL: "/api", timeout: 10_000 });// 每个请求重写
http.get("/users", { baseURL: "/server" });
http.post("/answers", { baseURL: "/question" });

优点:简单;拦截器只有一个地方。
缺点:容易忘记重写;关注点混合;更难应用每个服务的默认值(认证、头信息、重试)。

多个实例 + 共享拦截器(你的想法)

是的——这是一个可靠的模式。创建一个小助手来附加拦截器,然后为每个服务创建一个实例。

import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from "axios";const addInterceptors = (instance: AxiosInstance) => {instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {config.headers.set("X-Trace-Id", crypto.randomUUID());const token = sessionStorage.getItem("token");if (token) config.headers.set("Authorization", `Bearer ${token}`);return config;});instance.interceptors.response.use((res) => res.data, // 解包 .data(err: AxiosError) => {// 标准化错误const status = err.response?.status;const msg = (err.response?.data as any)?.message || err.message;return Promise.reject({ status, message: msg, cause: err });});
};export const serverApi = axios.create({ baseURL: "/server", timeout: 10_000 });
export const questionApi = axios.create({ baseURL: "/question", timeout: 10_000 });
addInterceptors(serverApi);
addInterceptors(questionApi);

优点:清晰的分离;每个服务的默认值;通过助手共享行为。
缺点:如果服务增长,你可能需要更多结构(类型化 SDK、工厂)。

实例工厂 + 服务映射(扩展性好)

当你有许多后端或需要不同的策略(超时、重试)时,从配置生成实例。这避免了到处重复 addInterceptors(...)

type ServiceKey = "server" | "question" | "billing";const serviceDefs: Record<ServiceKey, { baseURL: string; timeout?: number }> = {server:   { baseURL: "/server",   timeout: 10_000 },question: { baseURL: "/question", timeout: 8_000  },billing:  { baseURL: "https://billing.example.com", timeout: 15_000 }
};import axios, { AxiosInstance } from "axios";const instances = new Map<ServiceKey, AxiosInstance>();const addInterceptors = (ins: AxiosInstance) => {// 同上
};export function getClient(name: ServiceKey): AxiosInstance {if (!instances.has(name)) {const ins = axios.create(serviceDefs[name]);addInterceptors(ins);instances.set(name, ins);}return instances.get(name)!;
}// 用法
const serverApi = getClient("server");
const questionApi = getClient("question");

优点:集中配置;懒创建;易于添加/删除服务;测试变得更简单。
缺点:小的间接层(为了可维护性值得)。

单个实例 + 动态路由器

如果你必须保持单个实例,在请求拦截器中根据请求元数据(例如,自定义头信息或 URL 前缀)路由 baseURL

const http = axios.create({ baseURL: "/", timeout: 10_000 });http.interceptors.request.use((config) => {const target = (config as any).meta?.service; // 例如 "server" | "question"if (target === "server")   config.baseURL = "/server";if (target === "question") config.baseURL = "/question";return config;
});// 用法
http.get("/users", { meta: { service: "server" } as any });
http.post("/ask", { question: "..." }, { meta: { service: "question" } as any });

优点:一个实例统治所有。
缺点:"魔法"元选项可能不透明;类型安全需要额外注意。

可运行的演示(Node + Axios)

我们将构建什么

  • 一个小的 Express 服务器,暴露两个具有不同 baseURL 的"服务":

    • /server/users(用户服务)
    • /question/ask(问答服务)
  • 一个 TypeScript 客户端,包含:

    • 实例工厂
    • 共享拦截器
    • 取消 + 重试演示
    • 类型化 API 包装器

服务器(Express)

保存为 server.ts

import express from "express";const app = express();
app.use(express.json());app.get("/server/users", (_req, res) => {res.json([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]);
});app.post("/server/users", (req, res) => {const user = { id: Math.floor(Math.random() * 1000), ...req.body };res.status(201).json(user);
});app.post("/question/ask", (req, res) => {const { question } = req.body;res.json({ answer: `Echo: ${question}`, ts: Date.now() });
});// 模拟慢端点
app.get("/question/slow", async (_req, res) => {await new Promise((r) => setTimeout(r, 5000));res.json({ ok: true });
});const port = 3000;
app.listen(port, () => console.log(`演示服务器在 http://localhost:${port}`));

客户端(带有工厂 + 共享拦截器的 Axios)

保存为 client.ts

import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "axios";// 1) 服务定义
type ServiceKey = "server" | "question";
const serviceDefs: Record<ServiceKey, { baseURL: string; timeout?: number }> = {server:   { baseURL: "http://localhost:3000/server",   timeout: 10_000 },question: { baseURL: "http://localhost:3000/question", timeout: 10_000 },
};// 2) 拦截器(共享)
const addInterceptors = (ins: AxiosInstance) => {ins.interceptors.request.use((config: InternalAxiosRequestConfig) => {config.headers.set("X-Trace-Id", crypto.randomUUID());return config;});ins.interceptors.response.use((res) => res.data,async (error: AxiosError) => {// 在 5xx 错误上简单重试一次const cfg = error.config as any;const status = error.response?.status ?? 0;cfg.__retryCount = cfg.__retryCount || 0;if (status >= 500 && cfg.__retryCount < 1) {cfg.__retryCount++;return ins(cfg);}return Promise.reject(error);});
};// 3) 工厂
const cache = new Map<ServiceKey, AxiosInstance>();
function getClient(name: ServiceKey): AxiosInstance {if (!cache.has(name)) {const ins = axios.create(serviceDefs[name]);addInterceptors(ins);cache.set(name, ins);}return cache.get(name)!;
}// 4) 类型化 API 包装器
type User = { id: number; name: string };
export const UserAPI = {list: () => getClient("server").get<User[]>("/users"),create: (name: string) => getClient("server").post<User>("/users", { name }),
};export const QAAPI = {ask: (q: string) => getClient("question").post<{ answer: string; ts: number }>("/ask", { question: q }),slow: (signal?: AbortSignal) => getClient("question").get<{ ok: boolean }>("/slow", { signal }),
};// 5) 演示运行器
async function main() {console.log("1) 列出用户");console.log(await UserAPI.list());console.log("2) 创建用户");console.log(await UserAPI.create("Charlie"));console.log("3) 提问");console.log(await QAAPI.ask("如何在 Axios 中组织多个 baseURL?"));console.log("4) 取消演示(将在 1 秒时中止慢请求)");const ctrl = new AbortController();const t = setTimeout(() => ctrl.abort("时间预算超出"), 1000);try {console.log(await QAAPI.slow(ctrl.signal));} catch (e: any) {console.log("已取消:", e.message || String(e));} finally {clearTimeout(t);}
}main().catch((e) => {console.error("演示错误:", e?.response?.data || e.message || e);process.exit(1);
});

最小 package.json

{"name": "axios-multi-baseurl-demo","type": "module","scripts": {"dev:server": "tsx server.ts","dev:client": "tsx client.ts"},"dependencies": {"axios": "^1.7.2","express": "^4.19.2"},"devDependencies": {"tsx": "^4.19.1","@types/express": "^4.17.21","typescript": "^5.5.4"}
}

运行方式:

  1. npm i
  2. npm run dev:server(终端 A)
  3. npm run dev:client(终端 B)
    你将看到对 /server/question 的调用,对 5xx 错误的重试(如果有)以及一个取消示例。

为什么这种结构有效

共享行为而不重复

addInterceptors 放在一个地方可以保持认证/头信息/日志记录/错误标准化的一致性。如果你的 billing API 需要不同的令牌或 withCredentials,添加第二个助手(addBillingInterceptors)并仅将其应用于该实例。

服务默认值和演进

超时、基本头信息、withCredentials,甚至不同的适配器(Node 与浏览器)可能因服务而异。工厂将这些默认值与服务键绑定,因此未来的更改只需一行配置编辑。

可调试性和类型化

将端点包装在薄类型化助手(UserAPIQAAPI)后面为你提供:

  • 路径和负载类型的自动完成
  • 集中式响应解包
  • 以后添加每个端点缓存或分页助手的绝佳位置

适用于代理和网关

在浏览器应用中,你通常使用开发代理(Vite/CRA/Next)来重写 /serverhttp://localhost:3000/server/questionhttp://localhost:3000/question。相同的实例模式仍然适用,你只需在生产中将 baseURL 切换到相对路径并配置代理。

三个实际场景及示例

微前端命中不同域

你将应用托管在 app.example.com,但调用 user.example.comfaq.example.net。使用不同的实例来附加不同的凭据或 CORS 模式。

export const userApi = axios.create({baseURL: "https://user.example.com",withCredentials: true,
});
export const faqApi = axios.create({baseURL: "https://faq.example.net",withCredentials: false,
});
addInterceptors(userApi);
addInterceptors(faqApi);

原因:不同的 cookie/跨域行为不应在服务之间泄漏。

每个服务的重试和退避

计费端点需要健壮的重试;问答服务不需要。拆分拦截器或使用小的重试助手。

function addRetry(ins: AxiosInstance, tries = 3) {ins.interceptors.response.use(undefined, async (err) => {const cfg = err.config || {};cfg.__try = (cfg.__try || 0) + 1;if (cfg.__try < tries && (err.response?.status ?? 0) >= 500) {await new Promise(r => setTimeout(r, 200 * cfg.__try)); // 线性退避return ins(cfg);}return Promise.reject(err);});
}// 仅应用于计费
const billing = axios.create({ baseURL: "https://billing.example.com" });
addInterceptors(billing);
addRetry(billing, 3);

带有 URL 前缀路由的单个实例(网关)

你有一个单一的 API 网关(/api),它转发 /api/server/*/api/question/*。使用一个实例;保持 baseURL/api;通过路径前缀路由,并仍然享受共享逻辑。

const http = axios.create({ baseURL: "/api" });
addInterceptors(http);export const UserAPI = {list: () => http.get("/server/users"),
};
export const QAAPI = {ask: (q: string) => http.post("/question/ask", { question: q }),
};

如果每个调用都通过一个网关域,这很清晰。

常见问题解答

创建多个 Axios 实例昂贵吗?
不。实例是轻量级的。你关心的成本是重复的逻辑——通过共享拦截器/助手或工厂解决。

SSR 或 Node 与浏览器的差异呢?
在 Node 中,Axios 使用 HTTP 适配器;在浏览器中,它使用 XMLHttpRequest/fetch。如果你依赖 cookie,相应地设置 withCredentials 并在服务器上配置 CORS/Set-Cookie。

如何避免令牌在服务之间泄漏?
不要全局附加令牌。在 addInterceptors 中,为正确的实例读取正确的令牌(或使用每个服务的拦截器)。如果服务共享一个令牌,没问题;否则拆分。

我可以在运行时切换 baseURL 吗(例如,基于租户)?
可以。要么在租户更改时构建实例,要么基于租户上下文通过请求拦截器为每个请求重写 config.baseURL

如何清晰地类型化 config.meta
通过模块增强扩展 Axios 类型,或定义一个包装器请求函数,该函数接受你自己的类型化选项并将它们合并到 Axios 配置中。

总结

如果你有多个 baseURL,不要与 Axios 对抗——拥抱多个实例。一次性添加共享拦截器,通过一个小工厂连接默认值,并为每个域暴露类型化 API 包装器。对于非常小的项目,每个请求的 baseURL 重写是可以的;对于中型/大型代码库,工厂 + 服务映射模式可以保持整洁和可扩展。包含的 Node 演示为你提供了一个工作的起点:运行它,然后将结构适配到你的项目(超时、头信息、重试、取消、代理)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919748.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919748.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AP服务发现PRS_SOMEIPSD_00255 的解析

[PRS_SOMEIPSD_00255 ] 「SOME/IP-SD头部的重启标志&#xff0c;对于重启后发出的所有报文&#xff0c;都应设置为 1&#xff0c;直至 SOME/IP头部中的会话 ID (Session-ID) 回绕并因此再次从 1 开始。在此回绕之后&#xff0c;重启标志应设置为 0。」(RS_SOMEIPSD_00006)核心含…

纯手撸一个RAG

纯手撸一个RAGRAG基本流程第一阶段&#xff1a;数据预处理&#xff08;索引&#xff09; - 构建知识库第二阶段&#xff1a;查询与生成&#xff08;推理&#xff09; - 回答问题总结Chunk介绍Chunk框架的介绍Chunk核心概念选择分块策略和框架如何选择分块框架Python代码实现第一…

视觉语言对比学习的发展史:从CLIP、BLIP、BLIP2、InstructBLIP(含MiniGPT4的详解)

前言 本文一开始是属于此文《图像生成(AI绘画)的发展史&#xff1a;从CLIP、BLIP、InstructBLIP到DALLE、DALLE 2、DALLE 3、Stable Diffusion(含ControlNet详解)》的&#xff0c;后独立成本文 第一部分 从CLIP、BLIP1、BLIP2到InstructBLIP 1.1 CLIP&#xff1a;基于对比文本…

HTTP代理与SOCKS代理的区别、应用场景与选择指南

在互联网日常使用与跨境业务中&#xff0c;HTTP代理 和 SOCKS代理 是两种常见的网络代理方式。无论是跨境电商、社交媒体账号运营、数据采集&#xff0c;还是科学访问海外资源&#xff0c;都需要选择合适的代理协议。什么是HTTP代理&#xff1f;定义HTTP代理 是基于 HTTP协议 的…

AI重塑职业教育:个性化学习计划提效率、VR实操模拟强技能,对接就业新路径

职业教育长期面临着一系列问题&#xff0c;包括“统一课程难以适配不同基础学员”、“实操反馈滞后”和“就业技能与企业需求脱节”等。随着人工智能技术的应用&#xff0c;这些传统教学模式正在发生变化。个性化技能培养得以实现&#xff0c;甚至可以提前识别学员的就业短板。…

主题配色下的背景透明度

用 CSS color-mix() 解决背景透明度的痛点 在设计卡片组件时&#xff0c;经常遇到这样的需求&#xff1a;卡片背景需要80%透明度&#xff0c;鼠标悬浮在内部某项时&#xff0c;修改背景色但保持同样的透明度。 问题场景 .card {background: rgba(59, 130, 246, 0.8); /* 蓝色80…

【Python代码】谷歌专利CSV处理函数

以下是一个重构后的高可用、可配置、低耦合的专利CSV处理函数&#xff0c;包含清晰的注释和结构&#xff1a; import csv import pandas as pd from datetime import datetime import os from typing import List, Dict, Any, Optional, Tuple import logging# 配置日志 loggin…

3-2〔OSCP ◈ 研记〕❘ WEB应用攻击▸WEB安全防护体系

郑重声明&#xff1a; 本文所有安全知识与技术&#xff0c;仅用于探讨、研究及学习&#xff0c;严禁用于违反国家法律法规的非法活动。对于因不当使用相关内容造成的任何损失或法律责任&#xff0c;本人不承担任何责任。 如需转载&#xff0c;请注明出处且不得用于商业盈利。 …

PCIe 5.0相比顶级PCIe 4.0有何提升?

还在为PCIe 4.0固态硬盘那7000MB/s的速度沾沾自喜&#xff1f;醒醒&#xff0c;朋友。当很多人还在讨论PCIe 4.0是否“性能过剩”时&#xff0c;真正面向未来的PCIe 5.0已经带着碾压级的实力&#xff0c;来到了我们面前。这不是一次常规的“升级”&#xff0c;更不是英特尔式的…

23种设计模式——适配器模式(Adapter)​详解

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 设计模式 ✨特色专栏&#xff1a; 知识分享 &…

Vue3源码reactivity响应式篇之Reactive

概览 vue3中reactive用于将普通对象转换为响应式对象&#xff0c;它的实现原理是通过Proxy和Reflect来实现的。具体的实现文件参见packages\reactivity\src\reactive.ts。本文会介绍reactive的相关api如下&#xff1a; reactive&#xff1a;将普通对象转换为响应式对象readonly…

初识数据结构——Map和Set:哈希表与二叉搜索树的魔法对决

数据结构专栏 ⬅(click) 大家好&#xff01;我是你们的老朋友——想不明白的过度思考者&#xff01;今天我们要一起探索Java中两个神奇的数据结构&#xff1a;Map和Set&#xff01;准备好了吗&#xff1f;让我们开始这场魔法之旅吧&#xff01;&#x1f3a9; &#x1f3af; 先…

Unreal Engine UStaticMeshComponent

UnrealUnreal Engine - UStaticMeshComponent&#x1f3db; 定义&#x1f3db; 类继承⚡ 关键特性⚙️ 常见配置&#x1f6e0;️ 使用方法&#x1f4da; 在 C 中使用&#x1f4da; 在蓝图中使用&#x1f3ae; 典型应用场景&#x1f4da; 常见子类与用途&#x1f4dd; 小结Unrea…

demo 汽车之家(渲染-筛选-排序-模块抽离数据)

效果图展示&#xff1a;代码截图注释详情实现笔记总体目标&#xff08;按需求点对照代码&#xff09;数据模块化、整体渲染框架、筛选/排序的高亮与行为&#xff0c;全部已在 Index.ets CarData.ets 落地。下面按图片需求 2~4 点逐条总结&#xff0c;并给出关键代码定位与“为…

双重机器学习DML介绍

本文参考&#xff1a; [1]文心一言回答&#xff1b; 一、核心原理与数学框架 双重机器学习&#xff08;Double Machine Learning, DML&#xff09;由Chernozhukov等学者于2018年提出&#xff0c;是一种结合机器学习与传统计量经济学的因果推断框架。其核心目标是在高维数据和非…

【图像算法 - 21】慧眼识虫:基于深度学习与OpenCV的农田害虫智能识别系统

摘要&#xff1a; 在现代农业生产中&#xff0c;病虫害是影响作物产量和品质的关键因素之一。传统的害虫识别依赖人工巡查&#xff0c;效率低、成本高且易出错。本文将介绍如何利用深度学习与OpenCV构建一套高效的农田害虫智能识别系统。该系统能够自动识别10类常见农业害虫&a…

循环神经网络实战:GRU 对比 LSTM 的中文情感分析(三)

循环神经网络实战&#xff1a;GRU 对比 LSTM 的中文情感分析&#xff08;三&#xff09; 文章目录循环神经网络实战&#xff1a;GRU 对比 LSTM 的中文情感分析&#xff08;三&#xff09;前言数据准备&#xff08;与 LSTM 相同&#xff09;模型搭建&#xff08;GRU&#xff09;…

学习游戏制作记录(制作提示框以及使用键盘切换UI)8.21

1.制作装备提示框创建提示框&#xff0c;添加文本子对象&#xff0c;用来描述名称&#xff0c;类型以及属性加成挂载垂直分配组件和文本大小适配组件&#xff0c;这样图像会根据文本大小来调整自己创建UI_ItemTip脚本并挂载在文本框上&#xff1a;[SerializeField] private Tex…

chapter07_初始化和销毁方法

一、简介 一个Bean&#xff0c;在进行实例化之后&#xff0c;需要进行两种初始化 初始化属性&#xff0c;由PropertyValues进行赋值初始化方法&#xff0c;由ApplicationContext统一调用&#xff0c;例如加载配置文件 Bean的初始化与销毁&#xff0c;共有三种方式&#xff08;注…

open webui源码分析6-Function

一、Functions简介 可以把Tools作为依赖于外部服务的插件&#xff0c;Functions就是内部插件&#xff0c;二者都是用来增强open webui的能力的。Functions是轻量的&#xff0c;高度可定制的&#xff0c;并且是用纯Python编写的&#xff0c;所以你可以自由地创建任何东西——从新…