← 全部文章

API 与辅助工具

通过拦截 fetch 和 XHR 模拟 API 响应

后端还没好。或者它好了,但你没法让它按命令返回一个空列表、一个 500 错误和三秒延迟。本文介绍如何用一条 JustZix JS 规则,直接在浏览器里拦截 fetchXMLHttpRequest,返回假的 JSON,让你能针对你喜欢的任何场景构建和测试前端。

为什么要在浏览器里拦截

对「我需要假数据」的常规答案是一个 mock 服务器 —— MSW、json-server,或开发环境里的一个桩。它们很好,对一整个团队来说也是正确的选择。但有时你想要更轻量的东西:

一条 JustZix JS 规则活在浏览器里,按 URL 模式生效,且不需要改动任何项目。这正是它的定位:快速、精准、用完即弃的 mock。

包装 window.fetch

核心技巧是用你自己的函数替换 window.fetch,让它逐请求地决定是 mock 还是放行。在 document_start 运行这段代码,确保它在任何应用代码调用 fetch 之前就装好了:

// Save the original so we can fall through to the real network
const realFetch = window.fetch.bind(window);

// Your mock table: match by URL substring -> response factory
const mocks = [
  {
    match: '/api/user/profile',
    response: () => jsonResponse({ id: 1, name: 'Ada Lovelace', plan: 'pro' })
  },
  {
    match: '/api/orders',
    response: () => jsonResponse([])   // the empty-list edge case
  }
];

window.fetch = async (input, init) => {
  const url = typeof input === 'string' ? input : input.url;
  const hit = mocks.find(m => url.includes(m.match));
  if (hit) {
    console.log('[mock] intercepted', url);
    return hit.response();
  }
  return realFetch(input, init);   // not mocked -> real request
};

构造一个合成的 Response

你的应用期待一个真正的 Response 对象 —— 它会调用 .json()、检查 .ok、读取 .statusResponse 构造函数免费给你这一切:

// Helper: a 200 OK JSON response
function jsonResponse(data, status = 200) {
  return new Response(JSON.stringify(data), {
    status,
    headers: { 'Content-Type': 'application/json' }
  });
}

因为它是一个货真价实的 Response,那些写 const r = await fetch(...); if (r.ok) return r.json(); 的代码无需修改就能工作。这正是要点 —— 应用分辨不出区别。

模拟延迟

空列表和顺利路径很简单。Bug 藏在慢路径里:永远不消失的转圈、竞态条件、重复提交。在 resolve 之前加一段延迟:

// A small sleep helper
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// In the mock table, await it before returning
{
  match: '/api/search',
  response: async () => {
    await sleep(3000);            // 3 seconds of "loading..."
    return jsonResponse({ results: [] });
  }
}

模拟错误码

现在轮到不顺利的路径。一个 500、一个 404、一个应该把你弹回登录的 401 —— 用正确的状态码返回它们,让你的错误处理真的跑起来:

// 500 Internal Server Error
{
  match: '/api/checkout',
  response: () => jsonResponse(
    { error: 'payment_gateway_timeout' },
    500
  )
}

// A network failure (fetch rejects, not resolves)
{
  match: '/api/flaky',
  response: () => Promise.reject(new TypeError('Failed to fetch'))
}

注意区别:一个 500 仍然会以 r.ok === false resolve,而一个断开的连接会 reject。两者都测 —— 应用经常处理了其中一个,却在另一个上崩溃。

包装 XMLHttpRequest

大量旧代码,以及像 axios 这样的库,底层仍然用 XMLHttpRequestfetch 拦截碰不到它们。XHR 伪造起来更笨重,但一个薄薄的包装能覆盖常见情况:

const RealXHR = window.XMLHttpRequest;

window.XMLHttpRequest = function () {
  const xhr = new RealXHR();
  const realOpen = xhr.open;
  let mockedUrl = null;

  xhr.open = function (method, url, ...rest) {
    if (url.includes('/api/legacy')) mockedUrl = url;
    return realOpen.call(this, method, url, ...rest);
  };

  const realSend = xhr.send;
  xhr.send = function (...args) {
    if (!mockedUrl) return realSend.apply(this, args);

    // Fake a successful response without hitting the network
    setTimeout(() => {
      Object.defineProperty(xhr, 'readyState', { value: 4 });
      Object.defineProperty(xhr, 'status', { value: 200 });
      Object.defineProperty(xhr, 'responseText', {
        value: JSON.stringify({ legacy: true })
      });
      xhr.onreadystatechange && xhr.onreadystatechange();
      xhr.onload && xhr.onload();
    }, 0);
  };

  return xhr;
};

这是刻意写得极简的 —— 它用 JSON 文本覆盖了 onload / onreadystatechange,这正是 90% 的 XHR 代码所需要的。如果某个库会检查 getAllResponseHeaders(),你还得把那个也打桩。到了那一步,老实说,还是去用一个真正的 mock 服务器吧。

什么时候改用真正的 mock 服务器

浏览器拦截是快速、限定范围、用完即弃的活儿的正确工具。在以下情况切换到 MSW 或 json-server:

经验法则:如果 mock 应该被提交,用服务器。如果它应该在你关掉规则时就消失,用 JustZix。

另见

别再等后端了。安装 JustZix,把一个 fetch 包装放进一条限定范围的 JS 规则,今天就针对每个场景构建前端。

为这篇文章评分

暂无评分 — 成为第一个。

自己动手试试

安装 JustZix,粘贴本文中的任意代码片段。两分钟,从零到一条在你所有设备上生效的规则。

获取 JustZix

功能 · 工作原理 · 示例 · 应用场景