在前后端联调和客户端测试环节,接口数据可以通过Mock工具轻松伪造,但页面里的图片资源往往还是走线上地址。一旦测试需要验证图片加载失败、超清大图、异常尺寸等边界场景,直接改线上资源显然不现实。Charles Proxy提供了强大的请求映射能力,只要在本地用Node.js起一个图片生成服务,再把目标图片请求转发过来,就能实现随心所欲的图片Mock,这套方案通常被称作Mock2Image。

一、用Node.js搭建图片Mock服务器
图片Mock服务的核心是:接收任意URL请求,根据请求参数(尺寸、文件名、查询串)动态生成并返回一张图片。最简单的方案是返回一张固定图片,进阶方案是根据URL解析出宽高和颜色,实时绘制一张PNG。
下面是一个基于原生http模块的完整示例,它解析URL中的尺寸参数,用canvas实时生成图片。如果你不想引入canvas依赖,也可以用Sharp库预先准备好一批测试图,按文件名直接返回Buffer。
const http = require('http');
const { createCanvas } = require('canvas');
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1');
// 从查询参数中取出宽高,默认 200x200
const w = parseInt(url.searchParams.get('w')) || 200;
const h = parseInt(url.searchParams.get('h')) || 200;
const color = url.searchParams.get('color') || '#4A90D9';
const canvas = createCanvas(w, h);
const ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = '#fff';
ctx.font = '20px sans-serif';
ctx.fillText(`${w}x${h}`, 10, h / 2);
const buf = canvas.toBuffer('image/png');
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': buf.length,
// 关闭缓存,保证每次刷新都能拿到最新Mock图
'Cache-Control': 'no-store'
});
res.end(buf);
});
server.listen(3001, () => console.log('Mock image server on 3001'));这里有几个细节值得注意。第一,响应头里的Content-Type必须与实际图片格式一致,否则某些客户端会拒绝渲染;第二,显式设置Cache-Control: no-store,避免浏览器或App缓存住上一次的Mock结果,让你以为改了图却没生效;第三,如果目标请求本身是.jpg后缀,最好用canvas.toBuffer('image/jpeg')输出JPEG,保持格式一致能规避不少奇怪的解码问题。
如果被测环境对图片格式敏感(比如只认WebP),可以改用sharp做格式转换:
const sharp = require('sharp');
async function toWebP(canvas) {
const png = canvas.toBuffer('image/png');
return sharp(png).webp({ quality: 80 }).toBuffer();
}二、Charles Proxy的三种映射方式怎么选
Node.js服务跑起来之后,剩下的问题是如何让目标图片请求“拐弯”打到本地3001端口。Charles提供了三种主要工具,适用场景各不相同。
1. Map Remote(远程映射)是最常用的方式。在Tools菜单打开Map Remote,添加一条规则:把cdn.ippipp.com:443/images/*映射到127.0.0.1:3001。Charles会在转发时保留原始路径和查询参数,你的Node.js服务就能通过req.url拿到原始图片地址,进而解析出尺寸、文件名等信息。这种方式对客户端完全透明,App不需要做任何修改。
2. Map Local(本地映射)适合静态场景。它把请求直接映射到本地磁盘上的文件,比如把https://cdn.ippipp.com/banner.png映射到D:\mock\banner.png。优点是零延迟、零依赖,缺点是每个URL都要手动配置,无法动态生成,也没法模拟网络延迟和失败。如果你的测试图片集合是固定的,Map Local反而更省事。
3. Rewrite(重写规则)更多是辅助角色。它可以改写请求头、响应头、响应体,比如把线上图片的响应头改成Content-Type: image/png配合Map Local使用,或者给响应注入特定的缓存策略来测试缓存逻辑。
三种方式的对比可以概括为:动态生成就用Map Remote,静态替换就用Map Local,需要篡改响应属性时叠加Rewrite。Mock2Image方案的主体是Map Remote + Node.js,Map Local作为补充。
三、HTTPS抓包与证书配置
线上图片基本都是HTTPS请求,Charles默认无法直接解密。需要在Proxy菜单的SSL Proxying Settings里添加要拦截的域名(例如cdn.ippipp.com,也可以填*拦截全部,但流量大时性能会受影响)。同时,客户端所在设备必须信任Charles的根证书:iOS设备访问chls.pro/ssl下载描述文件后,还要在“关于本机-证书信任设置”里手动开启完全信任;Android 7以上的App默认不信任用户证书,如果是测试自家App,需要给Debug包配置networkSecurityConfig允许用户证书。
另外要注意一个坑:Map Remote把HTTPS请求映射到本地127.0.0.1:3001时,目标写成http协议即可,Charles会先完成SSL解密再把明文请求转发给Node.js,所以本地服务用http模块就够了,不需要在本地配证书。
四、常见坑与进阶技巧
缓存问题是最常见的坑。浏览器对图片的缓存非常激进,即使Charles已经替换了响应,页面可能仍然显示旧图。除了前面提到的no-store,还可以在Rewrite里给响应头加上Pragma: no-cache双保险。
模拟异常场景是Mock2Image真正有价值的地方。可以在Node.js服务里根据文件名规则返回不同结果:
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1');
// 文件名带 fail 时返回 404
if (url.pathname.includes('fail')) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
return res.end('not found');
}
// 文件名带 slow 时延迟 5 秒,模拟弱网
if (url.pathname.includes('slow')) {
return setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'image/png' });
res.end(emptyPngBuffer);
}, 5000);
}
// 文件名带 broken 时返回被截断的图片数据,模拟损坏文件
if (url.pathname.includes('broken')) {
res.writeHead(200, { 'Content-Type': 'image/png' });
return res.end(validPngBuffer.slice(0, 50));
}
// 正常生成图片...
});这套规则让你的测试覆盖面大大扩展:加载失败时客户端是否显示占位图、超时是否有重试、损坏文件是否导致崩溃,都可以通过URL命名约定来触发,不需要动任何业务代码。
性能方面,如果被测页面一次加载几十张图,canvas逐张生成可能成为瓶颈。建议给生成结果加一层内存缓存,相同尺寸和颜色的图直接复用Buffer:
const cache = new Map();
function getImageBuffer(w, h, color) {
const key = `${w}-${h}-${color}`;
if (!cache.has(key)) {
cache.set(key, drawImage(w, h, color));
}
return cache.get(key);
}最后提醒一点:Charles的Map Remote规则匹配的是Host和Path,支持通配符。配置规则时尽量精确到图片路径前缀(如/images/*),避免把整个域名的接口请求也误转到图片服务,导致页面数据加载全部失败。规则配好后记得在Sequence面板里确认请求确实被映射成功,状态码和响应大小对得上,就说明整条Mock链路已经打通了。
Node.jsCharles Proxy图片Mock修改时间:2026-09-07 18:00:45