MkSaaS 前端页面调用后端 API 的代码实现

文章导读
MkSaaS 前端页面调用后端 API,核心是先把请求地址、鉴权头和租户信息对齐后端定义,再封装一个统一的请求函数,避免每个页面重复写。流式输出要单独处理,不能按普通 JSON 解析。如果遇到 401,要设计刷新重放逻辑,并在浏览器 Network 面板里核对实际发出的请求。
📋 目录
  1. 先确认 MkSaaS 生成的 API 路由前缀与鉴权方式
  2. 封装统一的请求实例,注入 token 和租户 ID
  3. 针对流式响应使用特定的 fetch 读取方式
  4. 处理 401 时刷新令牌并重放请求
  5. 在浏览器 network 面板验证请求头与响应状态
A A

MkSaaS 前端页面调用后端 API,核心是先把请求地址、鉴权头和租户信息对齐后端定义,再封装一个统一的请求函数,避免每个页面重复写。流式输出要单独处理,不能按普通 JSON 解析。如果遇到 401,要设计刷新重放逻辑,并在浏览器 Network 面板里核对实际发出的请求。

适用场景:使用 MkSaaS 生成的后端接口,前端需要对接并处理鉴权、租户和流式响应。操作动作:从后端路由定义确认前缀和鉴权字段,封装 fetch 请求自动注入 token 和租户 ID,用 ReadableStream 解析 SSE,并在 401 时刷新重放。验证方式:在 Network 面板检查 URL、请求头、状态码和响应体。风险边界:token 刷新和租户字段需与后端实现保持一致,流式解析要考虑分片和中断。

先确认 MkSaaS 生成的 API 路由前缀与鉴权方式

不要靠猜。打开后端项目,找路由注册的地方。FastAPI 通常在 include_router 里写 prefix="/api/v1";Spring Boot 在 application.yml 里配置 servlet.context-path。如果后端提供 Swagger 文档,直接看接口的完整路径,前缀通常会在页面顶部标出。

鉴权也一样,去后端看拦截器或装饰器里从哪个头取 token。常见的写法是 Authorization: Bearer <token>,但也有些内部系统用 X-TokenX-Api-Key。租户信息同理,MkSaaS 多租户场景下一般放在自定义头里,比如 X-Tenant-ID。确认后再写前端,否则请求发到错误路径或错误字段,后端根本不认。

MkSaaS 前端页面调用后端 API 的代码实现

封装统一的请求实例,注入 token 和租户 ID

每次请求都手动带 token 很容易漏,而且后面改字段要满项目找。写一个 apiFetch 函数,自动从 localStorage 读取 token 和租户 ID,并附加到请求头上。

const API_BASE = '/api/v1';

function apiFetch(path, options = {}) {
  const token = localStorage.getItem('mk_token');
  const tenantId = localStorage.getItem('mk_tenant_id');
  const headers = {
    'Content-Type': 'application/json',
    ...(options.headers || {}),
  };

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  if (tenantId) {
    headers['X-Tenant-ID'] = tenantId;
  }

  return fetch(`${API_BASE}${path}`, {
    ...options,
    headers,
  });
}

如果后端要求的头不是这两个名字,直接改函数里的赋值部分。这样所有请求都走同一个入口,后续加 trace 头、改超时都方便。

针对流式响应使用特定的 fetch 读取方式

对话式 AI 接口通常返回 SSE 流,不能直接 response.json()。需要从 response.body 拿到可读流,用 getReader() 读 chunk,再按行切分。注意中文等字符可能被拆到两个 chunk 里,必须用 buffer 拼接。

MkSaaS 前端页面调用后端 API 的代码实现
async function streamSSE(url, onMessage, onError) {
  const token = localStorage.getItem('mk_token');
  const response = await fetch(url, {
    headers: { 'Authorization': `Bearer ${token}` },
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const lines = buffer.split('\n');
    buffer = lines.pop(); // 最后一段可能不完整,留到下一轮

    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('data:')) {
        const data = trimmed.slice(5).trim();
        if (data === '[DONE]') return;
        onMessage(data);
      }
    }
  }
}

这段代码把每次读取到的数据追加到 buffer,按换行分割后只处理完整行,剩余残片留在 buffer 等下次拼上。这样能避免中文被截断产生乱码。调用时传回调函数,比如 onMessage 里做 setState 或直接更新界面。

处理 401 时刷新令牌并重放请求

token 过期后,前端如果直接抛错,用户得重新登录。更顺滑的做法是拦截 401,刷新 token 后重放原请求。但注意防止递归死循环——刷新接口本身也可能 401。

MkSaaS 前端页面调用后端 API 的代码实现
let refreshPromise = null;

async function apiFetchWithRetry(path, options = {}) {
  const response = await apiFetch(path, options);

  if (response.status === 401) {
    // 刷新接口自身失败,直接跳登录
    if (path === '/auth/refresh') {
      window.location.href = '/login';
      throw new Error('Unauthorized');
    }

    // 并发请求只触发一次刷新
    if (!refreshPromise) {
      refreshPromise = refreshToken().finally(() => {
        refreshPromise = null;
      });
    }

    await refreshPromise;
    return apiFetch(path, options); // 重放一次
  }

  return response;
}

这里用 refreshPromise 保证多个请求同时遇到 401 时只刷新一次,其他请求等待同一个 Promise。重放只做一次,如果再次 401 就交给上层处理,避免死循环。

在浏览器 network 面板验证请求头与响应状态

写完代码,用浏览器自带开发者工具验证。按 F12 打开 Network,刷新页面或触发操作,找到对应的请求,检查这几项:

  • Request URL:是否带着正确的 API 前缀,比如 /api/v1/xxx
  • Authorization:请求头里是不是 Bearer <token>,token 值是否在当前的有效期内。
  • X-Tenant-ID:租户 ID 是否已经带上,值是否与当前用户所属租户匹配。
  • Status Code:是否 200/201。如果看到 401/403/404/500,需要针对排查。
  • Response:响应体大小和内容是否合理,流式接口应该能看到持续的 chunk 输出。

常见异常排查:401 先从 Network 里看发的头是否完整,再去后端确认 token 解析逻辑;403 多半是租户 ID 没传或没权限;404 通常是 API 前缀或路径写错;500 需要看后端日志,前端能确认的是请求体格式是否和接口文档一致。