
前阵子我们项目在做一个视频合成小工具前端负责把Canvas绘制过程录制成一系列帧再用gif.js合成GIF动图。开发环境一切正常直到某天图片预览区域开始频繁变空白点击导出GIF后要么卡在某一帧要么生成的动图只有个黑底。一开始怀疑是gif.js的兼容性坑也怀疑过Canvas跨域问题折腾了大半天最后定位到一个完全没想过的源头MockJS。没错就是那个平时开发时帮我们拦截接口、返回假数据的MockJS它硬生生把GIF的二进制数据流污染了。这种问题很隐蔽因为页面不报错、请求也不失败数据看起来“拿到了”其实是拿到了一个完全错误的JSON对象。如果你也遇到类似的“图片能画、但动图挂了”的诡异场景建议往下看。这篇文章我会把整个排查链路、MockJS的拦截原理以及几个可直接落地的解决方案都整理出来希望对你有帮助。1. 先看问题GIF绘制异常的几种真实表现1.1 导出GIF卡进度、动图变黑屏我们做的功能不算复杂用户录制一段操作轨迹前端按固定帧率截取Canvas快照然后把这些帧交给gif.js编码输出成GIF。问题出现在一次性截取了几十帧之后导出进度条走到一半就再也不动或者最终产出的GIF打开后是黑屏只有某个时间点闪了一帧。这个现象如果在Network面板里追最明显的一点是能正常看到图片资源的请求记录但点开响应内容发现返回的不是二进制流或图片预览而是一段JSON文本。正常浏览器加载一张GIF响应应该是一串乱码样的二进制数据Network面板里也会显示成图片缩略图。当Response变成一行JSON时基本可以断定是请求数据被某个环节替换了。1.2 图片标签加载GIF只出首帧或直接裂开另一种更常见的情况是页面里直接用img标签展示远程GIF地址做法很简单img srchttps://cdn.example.com/animation.gif /正常情况下浏览器会直接播放动图循环显示。但当我们启用了MockJS之后img标签的src如果被MockJS拦截加载到的就不是GIF二进制数据而是一段被转成字符串的JSON——浏览器解析不出来自然就裂开或者只显示一个无法解码的画面。有的浏览器甚至会反复重试表现成图片区一直闪。1.3 一切正常只偶发在特定环境最难受的情况是“时好时坏”。开发环境因为MockJS是全局开启的每次都可能命中不同规则所以偶发性很强。有些页面点几次好了有些页面点几次就废了很难复现也很难给产品解释。我当时排查到后面直接把问题范围锁定在了“加载图片/生成GIF的代码是否走了XHR”因为Canvas绘制本身不依赖请求真正依赖请求的是GIF数据源的获取。2. MockJS的“副作用”它拦截的不只是接口2.1 MockJS拦截XHR的工作原理要搞清楚为什么MockJS会污染GIF数据先得理解它内部做了什么。MockJS让人体感最舒服的一点是零配置劫持只要在项目入口import Mock from mockjs再配上几条Mock规则所有匹配的Ajax请求就自动返回假数据。它实现的核心手段是重写了window.XMLHttpRequest。具体流程大致是这样MockJS加载时把原生的XMLHttpRequest保存到一个内部变量里。之后把window.XMLHttpRequest替换成自己实现的一个类。页面里所有new XMLHttpRequest()拿到的都是MockJS的类而不是浏览器原生类。该类在open、send时会检查当前url是否匹配已注册的mock规则。如果匹配就直接填充mock生成的数据并触发readystatechange、load等事件完全不走真实网络。很多走XHR封装的上层库axios、jQuery的$.ajax、甚至某些第三方SDK都会在内部创建XHR实例所以它们都会中招。MockJS不管你是请求/api/user还是请求一个.gif地址它只看“url有没有命中我的规则”命中了就接管。这里有一个关键点MockJS默认只处理文本类型的响应数据。它把模板编译后的结果当成一个JavaScript对象然后走JSON.stringify再返回。它对responseType这个字段基本是无感的。也就是说即使你的请求明确写了responseType: blob或responseType: arraybufferMockJS也会照常返回一个JSON字符串。2.2 为什么GIF绘制会因此出错回到GIF绘制场景前端合成GIF通常有这么几步用一个Image对象加载GIF的URL。等图片解码完成把每一帧绘制到Canvas上。用gif.js把Canvas帧合成编码成GIF动图。第一步“加载GIF的URL”如果用的不是纯img标签而是脚本里创建new Image()并赋src那么这张图片的加载过程也是走浏览器资源请求的。如果你的Mock规则正巧匹配了这个URL比如规则写得很宽泛图片加载到的就是被Mock替换后的JSON字符串。图片对象无法解码这段字符串于是onload事件可能永远不会触发或者只触发一次异常回调Canvas绘制无从谈起。gif.js这类库内部也通过XHR去拉取GIF二进制数据var xhr new XMLHttpRequest(); xhr.open(GET, url, true); xhr.responseType arraybuffer; xhr.onload function() { // 这里期望拿到ArrayBuffer }; xhr.send();这个xhr.responseType arraybuffer在浏览器原生XHR下是会返回一个ArrayBuffer的。但在MockJS接管的情况下返回的是JSON字符串onload里拿到xhr.response不是一个ArrayBuffer后面调用解析器时就直接炸了。2.3 被“宽泛正则”误伤的典型场景我们项目当时的Mock规则里有一个非常偷懒的配置Mock.mock(/.*/, { code: 0, data: {} });开发阶段为了省事想对所有未知请求统一兜底返回一个假数据。这样的正则对接口来说也许“够用”但对图片、字体、音视频这些非接口请求就是灾难。GIF的URL被这个规则匹配后直接返回了{code:0,data:{}}的JSON字符串。浏览器按图片解析当然是黑的、裂的、不动的。后来复盘时我专门去看了一下这类写法在不少项目里都存在尤其是后端接口文档不全、前端需要快速联调时“全匹配兜底”是很多人会踩的选择。3. 排查实录从怀疑库到锁定MockJS的五步走3.1 第一步先确认是不是gif.js库的问题遇到GIF绘制异常第一反应当然是查库。我当时怀疑gif.js的worker线程在窗口尺寸过大或帧数太多时会崩所以先做了几个对照实验把GIF编码的帧数从60帧降到10帧问题依旧。把Canvas尺寸缩小问题依旧。换用另一台电脑、另一个浏览器问题依旧。直接加载本地一张静态GIF文件测试发现本地图片完全正常。这个实验结果很说明问题本地的静态GIF能正常绘制只有“远程URL加载GIF”时失败。于是怀疑点从“编码库”转移到了“图片资源加载”上。3.2 第二步检查Network面板里GIF请求的响应打开Chrome DevTools的Network面板筛选img或media类型逐个点开GIF请求看Response标签。正常情况下一张GIF的响应应该是图片预览二进制数据会渲染成动图。我们看到的是{code:0,data:{}}到这里基本已经可以确定问题出在“请求被拦截并篡改了响应数据”。但当时还没想到是MockJS在搞鬼因为MockJS默认配置里我们并没有给它匹配图片URL的任务。3.3 第三步检查XHR是否被替换我在控制台随手敲了一行console.log(window.XMLHttpRequest.toString());正常情况下打印出来应该是function XMLHttpRequest() { [native code] }浏览器会显示[native code]表示这是原生实现。结果这次打印出来的是一长串源码特征非常明显里面有类似MockXMLHttpRequest这样的名字或者一堆模拟生命周期的方法。这一下就实锤了页面里的XHR全被MockJS接管了所有请求都先经过它过滤。3.4 第四步临时禁用MockJS做对照为了确认到底是不是MockJS引发GIF绘制异常我做了个最简单的对照测试把全局的MockJS引用注释掉。刷新页面重新加载远程GIFNetwork面板里正常显示图片预览。重新打开MockJS相同的GIF地址再次变成JSON。同一个URL唯一变量是MockJS是否启用结果一个正常一个异常。到这里问题定位已经没有任何悬念了。3.5 第五步定位到具体的Mock规则最后一步是排查是哪条规则命中了GIF的URL。做法很简单在MockJS规则注册处加一行日志打印当前请求的url和method然后专门去看GIF请求进来时匹配到的是哪个pattern。我们最后锁定的是一个以/.*/为pattern的兜底规则。这个规则把所有没有显式匹配规则的请求全部吞了GIF URL没有任何招架之力。4. 解决方案让GIF请求绕开Mock或让Mock更“规矩”4.1 方案一收紧Mock规则避免宽泛匹配最直接、也最应该优先做的是检查现有Mock规则把所有过于宽泛的pattern改掉。统一约定Mock规则里的正则必须带明确的接口标识比如包含/api/前缀禁止出现/.*/这种“全匹配兜底”。我建议项目里Mock规则按下面这个模板走// 接口mock规则示例 Mock.mock(/\/api\/user\/list/, get, { code: 0, data|5-10: [ { id|1: 1, name: cname } ] }); // 资源请求明确不mock直接放行 Mock.mock(/\.(gif|jpg|jpeg|png|svg|woff2?|mp3|mp4)$/, get, function() { return false; // 返回false表示不拦截走真实网络 });这个写法的思路是给资源类URL单独注册一条“放行”规则。但这里要提醒一句不同版本的MockJS对“返回false”的处理不一定一致有的版本会直接当成响应数据返回。所以更保险的做法是直接把资源URL排除在匹配范围之外。4.2 方案二给MockJS增加“白名单”机制MockJS本身没有提供独立的“排除列表”API但我们可以在项目里封装一层入口函数统一控制哪些请求不交给MockJS处理。我当时的做法是写了一个createMock函数import Mock from mockjs; // 声明资源类请求和不需要mock的接口 const MOCK_EXCLUDE_REG [ /\.(gif|jpg|jpeg|png|svg|mp4|mp3|woff2?)(\?.*)?$/i, /\/api\/no-mock/ ]; // 判断url是否需要排除 function shouldExclude(url) { return MOCK_EXCLUDE_REG.some(function(reg) { return reg.test(url); }); } // 统一注册mock规则 export function createMock(rules) { rules.forEach(function(item) { Mock.mock(item.pattern, item.method, item.template); }); } // 拦截入口注册前先做一次排除判断 export function setupMock(rules) { const filteredRules rules.filter(function(item) { // 如果规则pattern过宽被排除列表命中就不注册 return !shouldExclude(item.pattern.toString()); }); createMock(filteredRules); }这种方式虽然不能阻止已有规则在运行时误伤但至少可以保证“资源类请求”这一大类根本不会落到宽泛pattern的兜底逻辑里。如果你不想动历史老代码用这个封装把新代码的Mock入口统一管理起来成本是最低的。4.3 方案三按环境动态加载MockJS很多项目把MockJS写在main.js入口一行import ./mock全环境生效。这会导致生产环境也带着MockJS跑偶尔出现类似“线上图片加载失败”这种莫名其妙的问题。更合理的做法是只在开发环境或指定测试环境动态加载MockJS。以Vue项目为例入口文件做这样一层判断// main.js if (process.env.VUE_APP_ENABLE_MOCK true) { import(/mock).then(function() { console.log([mock] MockJS enabled); }); }配合.env.development文件VUE_APP_ENABLE_MOCKtrue生产环境构建时process.env.VUE_APP_ENABLE_MOCK默认是falseMockJS代码根本不会被打包进主流程。这个方案的好处是从根上杜绝了MockJS在生产环境干扰GIF、字体、图片等资源加载的问题。如果你用的是Vite思路也一样用import.meta.env或者自定义环境变量做判断。核心原则是MockJS应该是一个开发期的工具而不是运行时依赖。4.4 方案四调整GIF绘制代码的取数方式如果你对GIF绘制代码有完全的控制权可以考虑把“加载GIF二进制数据”这一步改成先下载成Blob再交给Image或编码库处理。以gif.js为例它内部是通过XHR直接获取ArrayBuffer的。我们可以在外部先取到ArrayBuffer再把数据“喂”给gif.js绕开它内部的XHR请求// 统一封装安全的GIF加载 async function loadGifArrayBuffer(url) { const response await fetch(url); const buffer await response.arrayBuffer(); return buffer; } // 使用 const gif new GIF({ workers: 2, quality: 10 }); const buffer await loadGifArrayBuffer(gifUrl); // 自己的解析逻辑处理buffer当然这个方案有个前提你的GIF解码流程是自己写的或者所使用的库允许你传入ArrayBuffer而不是让它内部去请求URL。如果库内部逻辑是写死的“传入url就发XHR”那这个方案就不适用。顺带说一句如果你用fetch拉取数据而MockJS也配置了Mock的fetch插件那么fetch同样会被劫持。我们项目的MockJS只patch了XHRfetch还是原生行为所以用fetch能绕开。但如果你的项目里引入了mockjs-fetch之类的库这一步就没有效果了需要先检查清楚。4.5 方案五把二进制请求独立到一个请求模块如果你的项目里GIF、图片、字体这类资源请求分散在各处建议统一封装一个mediaRequest模块所有非接口类的资源请求都走这个模块// services/media.js export function loadMedia(url) { return fetch(url).then(function(res) { return res.blob(); }); } // 调用方 const blob await loadMedia(gifUrl); const imageUrl URL.createObjectURL(blob); const img new Image(); img.src imageUrl;这里有一个很实用的转换技巧把远程GIF先通过fetch取回成Blob再用URL.createObjectURL()生成一个本地地址最后把这个本地地址赋值给img.src。由于本地Blob URL不经过网络请求MockJS根本不可能拦截到它GIF绘制就完全不受影响。这个方法还可以顺带解决跨域问题因为URL.createObjectURL生成的地址是同源的不需要服务器额外配置Access-Control-Allow-Origin。5. 常见问题速查遇到这些现象可以直接对照我把这次排查中遇到的和一些朋友遇到的类似现象整理成了一张表方便你直接对照现象可能原因排查方向解决建议GIF图片裂开或显示空白MockJS拦截了GIF请求返回了JSON字符串Network面板查看响应内容是否为JSON收紧Mock规则或增加资源白名单页面图片偶发性打不开宽泛正则兜底部分图片被匹配逐个测试图片URL是否命中Mock规则禁用宽泛匹配按/api/前缀精确匹配gif.js导出时进度卡死gif.js内部XHR拿到的不是ArrayBuffer而是String断点查看gif.js请求的response类型方案四绕开库内部XHRcanvas导出时报错“Failed to execute drawImage”图片对象未加载完成或图片数据无效检查Image的onload是否正常触发先确保图片加载成功再调用drawImage生产环境也出现图片异常MockJS在生产环境也被引入检查打包产物中是否包含mockjs按环境动态加载生产环境禁用mock第三方SDK内部的GIF加载异常SDK内部XHR被MockJS接管查看SDK请求的URL是否被mock规则命中全局排除规则或禁用MockJS本地GIF正常远程GIF异常远程资源请求被截胡对照本地与远程请求路径差异对远程请求统一走fetchBlob方案5.1 快速定位技巧检查XHR是否被MockJS接管在控制台运行下面这段代码可以快速判断MockJS是否在页面上生效// 输出当前XMLHttpRequest对象是否被替换 console.log(window.XMLHttpRequest.toString());如果输出结果里包含function MockXMLHttpRequest说明MockJS已经接管了全局XHR。如果恰好你要排查的GIF请求也走XHR那就可以高度怀疑是MockJS引起的。5.2 快速定位技巧二分法禁用Mock在入口文件里注释掉MockJS注册代码刷新页面看GIF是否恢复正常。如果恢复正常基本可以下结论。这个方法看起来很笨但效率非常高比用断点一行行跟代码快得多。5.3 快速定位技巧Network面板按资源类型筛选Chrome DevTools的Network面板左上角有资源类型筛选器直接选择Img或Media只看GIF/图片这类资源请求。点击对应请求后查看“Preview”或“Response”标签正常情况显示图片预览或二进制乱码。异常情况显示JSON对象或字符串。这个操作十秒就能完成基本能锁定问题在资源请求层面。6. 一些额外的思考与经验6.1 类似问题不止出现在GIF上这次排查的经验同样适用于其他二进制资源。常见的受害者有字体文件.woff、.woff2字体加载失败页面图标变方块。音乐/视频.mp3、.mp4播放器初始化失败或拿到数据无法解析。PDF预览pdf.js内部用XHR拉取PDF文件同样会被MockJS污染。图片验证码验证码图片一直刷新不出来后台报错却找不到原因。任何一个通过XHR请求二进制数据的场景都有可能因为MockJS的“全量接管”而出问题。理解了这一点以后再遇到类似问题排查思路就能直接套用。6.2 团队协作中Mock规则需要立规矩我把这次问题总结成了一条给团队的标准所有Mock规则的URL匹配必须带上接口特征前缀例如/api/禁止使用绝对宽泛的正则。资源类请求图片、字体、音视频一律不注册Mock规则。同时我们还约定在Mock入口文件里统一维护一个“排除列表”凡是新增的第三方SDK、资源请求地址都会先确认是否命中排除列表。这件事给我的教训是MockJS本身是个好工具但它“全量拦截XHR”的设计思路天然对“非接口数据”不友好。用好它的前提是理解它的边界并且在实际项目中做好规则规范别图省事写一条兜底正则应万变。6.3 排查方向对了问题就解决了一半回过头看这次排查最大的成本反而不是“找到MockJS”而是“意识到问题可能在数据源”。如果一开始就打开Network面板去看GIF请求的响应内容可能半小时就能定位而不是折腾大半天。这也给我提了个醒很多前端疑难问题第一反应不要怀疑库、不要怀疑兼容性先看请求的数据到底长什么样。数据不对再正常的功能也会表现得很诡异。如果你现在正被“图片明明请求到了但画不出来”这类问题困扰照着上面的排查步骤走一遍大概率能少走很多弯路。