Pretender 是一个轻量级的请求拦截库,最初设计用于浏览器环境中拦截 XMLHttpRequest 和 Fetch 请求,后来也可以借助 polyfill 在 Node.js 环境中运行。它的核心价值在于:前端开发者无需等待后端接口就绪,只需要定义好路由规则,就能拿到结构符合预期的模拟数据。本文将以 Node.js 环境为载体,带大家完整实现一个可以拦截请求、返回动态模拟图片与 JSON 数据的 Mock 服务,也就是所谓的 Pretender2Image 思路。

Pretender 的核心原理是什么
Pretender 的工作机制并不复杂。它通过替换(或者说代理)全局的 XMLHttpRequest 构造函数和 fetch 函数,在请求真正发出之前先经过一层拦截器。拦截器会拿着请求的 URL 和方法去匹配开发者注册的路由表,一旦命中,就不再走真实的网络请求,而是直接调用对应的处理函数并构造一个伪造的响应对象返回。
这套设计有几个值得注意的细节。第一,路由匹配支持动态段,例如 /api/users/:id 这样的写法可以匹配任意数字或字符串,并将捕获的值作为参数传给处理函数。第二,响应可以是同步返回的,也可以通过 Promise 延迟返回,这使得模拟网络延迟变得非常容易。第三,Pretender 保留了原始请求的上下文信息,处理函数可以读取请求头、请求体,从而实现根据入参返回不同数据的动态 Mock。
理解了这些原理之后,你会发现 Pretender 与 Mock Service Worker、json-server 这类工具的定位差异:Pretender 更偏向开发期测试场景下的精细控制,而不是独立部署的 Mock 服务器。不过在 Node.js 中配合 jsdom 这类环境,Pretender 同样可以胜任服务端测试的拦截工作。
在 Node.js 中搭建基础拦截环境
Pretender 默认面向浏览器,在 Node.js 中直接 require 会因为缺少 window 对象而报错。解决办法是引入 jsdom,先构造一个虚拟的 DOM 全局环境,再加载 Pretender。下面是完整的环境搭建代码:
const { JSDOM } = require('jsdom');
// 构造一个带 fetch 与 XMLHttpRequest 的虚拟浏览器环境
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'https://ipipp.com/',
pretendToBeVisual: true
});
// 将 jsdom 的全局对象注入到当前 Node 进程
global.window = dom.window;
global.document = dom.window.document;
global.XMLHttpRequest = dom.window.XMLHttpRequest;
global.fetch = dom.window.fetch;
const Pretender = require('pretender');
// 创建 Pretender 实例,此时它已经接管了请求发送
const server = new Pretender();
server.unhandledRequest = function (verb, path) {
console.warn('未匹配的请求:', verb, path);
};
console.log('Mock 环境初始化完成');这段代码的关键在于 global 对象的赋值。Node.js 的模块作用域中没有浏览器全局变量,必须手动把 jsdom 提供的对象挂到 global 上,Pretender 在初始化时才能找到需要替换的目标。初始化完成后,后续所有通过 fetch 或 XMLHttpRequest 发出的请求都会先经过 Pretender 的路由表。
还需要注意 unhandledRequest 回调的配置。默认情况下 Pretender 遇到未注册的请求会直接抛出异常,这在调试阶段很有用,可以暴露遗漏的接口;但如果希望未匹配的请求透传到真实服务器,则可以在这个回调里调用 passthrough 方法。
注册路由并返回模拟数据与图片
环境就绪后,接下来注册具体的路由规则。下面的示例演示了三类常见场景:返回 JSON 数据、根据参数返回动态结果,以及返回一张模拟图片。
// 场景一:返回基础 JSON 数据
server.get('/api/users', () => {
return [200, { 'Content-Type': 'application/json' },
JSON.stringify([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
])];
});
// 场景二:动态路由,根据 id 返回不同数据
server.get('/api/users/:id', (request) => {
const id = request.params.id;
return [200, { 'Content-Type': 'application/json' },
JSON.stringify({ id: Number(id), name: '用户' + id, avatar: '/mock/avatar/' + id })];
});
// 场景三:返回模拟图片(Base64 编码的 PNG)
server.get('/mock/avatar/:id', (request) => {
const pixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
return [200, { 'Content-Type': 'image/png' }, Buffer.from(pixel, 'base64')];
});
// 场景四:模拟延迟与错误
server.get('/api/slow', () => {
return new Promise((resolve) => {
setTimeout(() => resolve([200, {}, '延迟返回的数据']), 1500);
});
});
server.get('/api/error', () => [500, {}, '服务器内部错误']);处理函数的返回值是一个三元数组,依次为状态码、响应头对象和响应体,这种结构简单直接。返回图片时,可以预先用 Canvas 或 sharp 生成图片数据,也可以像示例中那样使用 Base64 字符串,这就是 Pretender2Image 思路的体现——接口返回的不是文本,而是一张可以真实渲染的图片资源。
模拟延迟和错误场景对前端开发尤为重要。真实网络不可能永远秒回,通过 Promise 加 setTimeout 可以模拟弱网环境,验证页面的 loading 状态;而返回 500 或 404 状态码则可以检验前端的异常处理逻辑是否健壮。
用 Node.js 发起请求验证拦截效果
路由注册完成后,写一段验证代码确认拦截是否生效:
(async () => {
// 请求 JSON 接口
const res1 = await fetch('https://ipipp.com/api/users');
const users = await res1.json();
console.log('用户列表:', users);
// 请求动态路由
const res2 = await fetch('https://ipipp.com/api/users/42');
console.log('单个用户:', await res2.json());
// 请求模拟图片接口
const res3 = await fetch('https://ipipp.com/mock/avatar/1');
const buffer = await res3.arrayBuffer();
console.log('图片字节数:', buffer.byteLength);
// 请求错误接口
const res4 = await fetch('https://ipipp.com/api/error');
console.log('错误状态码:', res4.status);
})();运行这段代码,可以看到所有请求都被 Pretender 拦截并返回了模拟数据,没有任何真实的网络流量产生。特别提醒一点,由于 jsdom 环境中 URL 的解析依赖设置的 base url,请求地址的域名部分要与初始化 jsdom 时传入的 url 参数保持一致,否则路由可能匹配失败。
如果需要验证图片是否可渲染,可以把返回的二进制数据写入本地文件查看,或者改用 Node 的 http 模块起一个真实端口,把 Pretender 的拦截结果透传出去,这样浏览器也能直接访问 Mock 接口,团队协作时其他成员同样可以受益。
进阶技巧与常见坑点
第一,路由的注册顺序会影响匹配结果。Pretender 按注册顺序匹配,如果同时注册了 /api/users/new 和 /api/users/:id,必须把更具体的路由放在前面,否则 new 会被当作 id 参数捕获。
第二,请求体需要手动解析。对于 POST 请求,request.requestBody 在某些情况下是字符串形式,需要用 JSON.parse 处理后再使用,建议在处理函数入口处做一次类型判断:
server.post('/api/login', (request) => {
let body = request.requestBody;
if (typeof body === 'string') {
body = JSON.parse(body);
}
if (body.username === 'admin' && body.password === '123456') {
return [200, {}, JSON.stringify({ token: 'mock-token-abc' })];
}
return [401, {}, JSON.stringify({ message: '用户名或密码错误' })];
});第三,测试结束后记得调用 server.shutdown() 还原被替换的全局对象。如果不还原,同一个进程中后续的测试可能会因为拦截器残留而出现诡异的超时或数据错乱。这个方法在单元测试的 afterEach 钩子中调用尤为合适。
第四,Pretender 对 HTTP 方法的支持覆盖了 GET、POST、PUT、DELETE、PATCH 等常见动词,分别对应 server.get、server.post 等便捷方法,也可以用 server.handle 注册多个方法共用的处理器。善用这些 API,可以把整个前端项目的接口依赖完全模拟出来,实现真正意义上的前后端并行开发。
总的来说,Pretender 配合 Node.js 与 jsdom,能够以极低的成本搭建出一套可控、可复现、可扩展的接口模拟方案。无论是日常开发、组件调试还是自动化测试,这套 Pretender2Image 式的 Mock 思路都值得一试。