ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MockJS拦截GIF请求导致动画卡顿?排查与解决全记录

MockJS拦截GIF请求导致动画卡顿?排查与解决全记录 前几天项目群里有人反馈页面上的GIF动图不转了玩法页面里那几张加载动画卡在第一帧点开控制台也没报错。我随手开了下线上地址一切正常再切回本地Mock环境稳定复现。排查到最后罪魁祸首是MockJS——它不止拦截了Ajax数据请求还给GIF的XHR请求塞了一份模拟数据浏览器拿到的根本不是图片绘制GIF的Canvas当然直接罢工。这篇文章就把这次排查的思路、确认过程和最终解决方案完整记录下来给那些同样遇到“Mock环境图片异常”的兄弟一个参照。1. 项目概述与现象还原1.1 先看清MockJS在项目里扮演的角色MockJS是前端开发里很常用的数据模拟工具核心作用就是拦截浏览器端的Ajax请求按我们配置的规则返回假数据。它解决的核心痛点是前后端并行开发时前端不用干等后端接口只要提前约定好接口文档就能用Mock数据渲染页面甚至能模拟超时、异常、空列表这些特殊场景。它的工作原理其实不复杂MockJS全局替换了浏览器原生的XMLHttpRequest对象在你调用new XMLHttpRequest()或者用jQuery、axios这类基于XHR封装的库时请求会先经过MockJS内部的MockXHR处理。MockXHR拿到请求URL后会跟已注册的mock规则做匹配匹配上了就返回预设的mock数据不再真正发网络请求。这个过程对业务代码是透明的我们平时开发基本无感也正是这种“透明”埋下了隐患。大多数情况下MockJS只处理业务数据接口但有些项目为了省事把mock规则写得很宽或者开发环境统一走一套接口网关导致图片、文件、字体这类静态资源也被MockJS“接管”了。GIF绘制异常就是这种误拦截的典型结果。1.2 GIF异常的现象分类不是所有动图都挂了出现异常后我第一件事是在团队里收集了具体现象发现这个“GIF不转”其实分好几种情况第一种页面里的img标签直接引用GIF动画卡在第一帧不动。这种情况如果只看页面表现很像资源加载失败或者图片损坏。第二种通过axios.get(url, { responseType: blob })拉取GIF数据再用URL.createObjectURL生成地址赋给img结果图片完全不显示或者Canvas调用drawImage时报解码错误。第三种GIF请求在Network面板里显示返回的不是image/gif而是一段JSON字符串但接口状态码还是200所以部分监控系统不会报警坑得很。一个关键的区别是同样是GIFimg标签直接引用的一般不受影响只有走了XHR/fetch链路再转成Blob或ArrayBuffer绘制的才会挂。这个差异本身就是排查方向的重要切入点。1.3 排查前先梳理关键线索开始动手之前我列了三条已知线索第一线上环境完全正常只有本地开发环境异常说明问题不在后端也不在图片源本身。第二Network面板里能看到GIF请求确实发出去了但Response不是图片数据说明请求被某个中间层改写了。第三业务代码里GIF不是简单用img标签加载而是先请求接口拿到图片地址再通过XHR把图片二进制流拉回来交给Canvas绘制。这三条线索叠加在一起基本把嫌疑锁定在了“开发环境特有的请求拦截层”。而本地环境相比线上多出来的东西就是MockJS和前端工程化的代理转发。跨域代理主要影响请求可达性不会篡改响应体内容会篡改响应体的MockJS是头号嫌疑。2. 根因分析MockJS为什么会劫持图片请求2.1 MockJS的拦截机制只认XHR不认识img要彻底理解这个问题得把MockJS的拦截范围说透。MockJS重写的是XMLHttpRequest对象这意味着所有基于XHR发起的请求都会被它过一遍。但img标签加载图片走的是浏览器底层网络栈不是XHR所以MockJS管不到img。这也是为什么很多页面里其他GIF正常、唯独走接口拉取的那个GIF异常。再看axios这一类请求库。axios在浏览器环境下有XHR适配器和HTTP适配器默认走XHR适配器也就是XMLHttpRequest。既然MockJS已经替换了全局的XMLHttpRequest那么axios发起的请求自然会被拦截。同理jQuery的$.ajax、老项目里的原生XHR只要是同源或跨域的XHR请求都在MockJS的射程范围内。MockJS内部的匹配方式也很有迷惑性。它支持字符串、正则、函数三种匹配方式。如果配置里写的是/\/api\//这种正则那所有包含/api/的URL都会命中不管你是接口文档还是图片资源。像这个项目里GIF地址恰好是/api/portal/gif/loading.gif?typeevent正则/\/api\/portal/一匹配就中招了。2.2 MIME类型与二进制数据GIF解码失败的直接原因MockJS命中规则后做了什么它会把预设的mock数据通过MockXHR返回给调用方。问题就出在这个“返回”动作上。浏览器要正确解析一张GIF图片必须满足两个条件响应头里的Content-Type是image/gif响应体是合法的GIF二进制流。MockJS返回的是JS对象或字符串比如{ code: 0, data: { gifUrl: } }Content-Type仍然是JSON相关的MIME类型。业务代码拿到的所谓res.data根本不是一个Blob对象更不是ArrayBuffer而是一段普通文本。这时候如果代码里有这行const blob new Blob([res.data], { type: image/gif }); img.src URL.createObjectURL(blob);你等于是把一段普通的JSON字符串文本包装成了image/gif类型的Blob。浏览器强行解码这段数据找不到GIF文件头GIF89a或者GIF87a解码必然失败。Canvas绘制时drawImage拿到的图像源不可解码就会直接抛错或者画出一个空白区域。更隐蔽的情况是如果Mock规则返回的数据恰好是空对象{}那么JSON.stringify({})之后是{}字符串长度为2但浏览器按图片解码时同样失败。表现形式不同但根因是一个请求被打包成普通文本返回了。2.3 命中规则的偶然性为什么有的GIF正常、有的异常排查过程中有个现象特别值得注意同一个页面上有好几张GIF有的正常有的不正常。有同事一度怀疑是图片大小问题去对比文件体积查了半天没结论。正常和不正常的差异其实非常简单正常的GIF是用img标签直接加载不经过XHR异常的GIF是通过接口网关下发的地址然后用axios.get去拉二进制流恰好那个地址又匹配了mock规则。所以问题的出现要同时满足两个条件第一GIF资源的加载路径必须走XHR比如用axios({ url, responseType: blob })或者原生XMLHttpRequest。第二GIF的URL必须命中MockJS里注册的规则。正则规则越宽泛命中率越高。两个条件缺一个都不会出问题。反过来讲这也解释了为什么这种Bug很难在写代码的时候发现它不取决于代码写得好不好而取决于开发和Mock配置之间的一次“意外碰撞”。3. 排查过程实录从现象到源码的四步定位3.1 第一步Network面板先行确认响应内容我在本地环境打开页面调出开发者工具的Network面板先按Img过滤发现有一批GIF显示加载成功状态码200再按Fetch/XHR过滤看到了那条记录异常GIF地址的XHR请求。点击这条请求看Response标签页内容不是图片预览而是一段JSON{ code: 0, data: { gifUrl: /api/portal/gif/loading.gif?typeevent, text: mock data } }这个响应内容一出来基本实锤了。真实的GIF接口不可能返回这种JSON结构。我又切到Header标签页看到Content-Type是application/json不是image/gif。到这里可以确认请求没有打到后端而是被本地某个中间层劫持了。这一步的要点是不要只看状态码要看响应体和响应头。200不代表一切正常像这种“假200”特别容易迷惑人。3.2 第二步临时禁用MockJS做对照实验为了验证是MockJS干的我没有直接去翻源码而是先做了个最简单的对照实验临时把入口文件里MockJS的初始化代码注释掉重新跑本地环境。具体来说是在项目入口main.js里找到类似这两行import /mock; // 或 require(./mock);注释之后刷新页面那张异常GIF立刻恢复了Network面板里显示的响应类型也变成了image/gif还带图片预览。这就明明白白告诉我们MockJS是导致异常的充分条件。这一步不要跳过它把所有“玄学”因素都排除了。一旦禁用MockJS后问题消失那就可以确定排查方向是MockJS的配置或实现而不是业务代码、后端接口、浏览器缓存之类的东西。3.3 第三步顺着业务代码找到GIF的加载路径确认了MockJS是罪魁祸首之后我回头看业务代码里GIF是怎么加载的。在某个抽奖组件里看到这样一段逻辑async function loadGif(gifUrl) { const res await axios.get(gifUrl, { responseType: blob }); const objectUrl URL.createObjectURL(res.data); const img new Image(); img.onload () canvas.drawImage(img, 0, 0, width, height); img.src objectUrl; }这段代码本身没问题。它先通过XHR把GIF图片的二进制数据取回来封装成Blob用URL.createObjectURL生成临时地址再用canvas.drawImage绘制第一帧。问题出在MockJS把整个XHR请求拦截掉了。业务代码这边的axios.get拿到的是一个被MockXHR包装过的响应res.data根本不是一个Blob。在MockJS执行的时候即使你设置了responseType: blobMockXHR也不一定会严格按这个类型给你构造数据它更倾向于直接返回配置的mock数据。于是URL.createObjectURL(res.data)就不是预期的GIF Blob而是字符串或其他无效对象Canvas绘制自然失败。3.4 第四步翻源码确认MockJS的匹配逻辑最后我去翻了MockJS的源码把最后一层窗户纸捅破。在MockJS的mockxhr.js里核心逻辑是在send方法中遍历已经注册的规则判断当前请求URL与规则是否匹配。关键代码如下经过精简// mockjs/src/mockxhr.js 中的核心匹配逻辑 if (type GET url Mock._mocked[url]) { // 匹配命中使用mock数据 this.responseText Mock._mocked[url]; }这里Mock._mocked记录了所有注册过的mock规则。如果我用的是字符串URL注册MockJS会做精确匹配如果我用的是正则注册MockJS会用正则去测试请求URL。像/\/api\/portal/这种正则自然把/api/portal/gif/loading.gif也包含进去了。MockJS内部还有一个特点它默认会设置responseType 也就是按纯文本返回。如果你注册的mock数据是对象它会JSON.stringify之后塞给responseText。这一步就把原本应该是二进制流的东西彻底变成了普通文本。看到源码里这两段行为整个问题就闭环了。MockJS不是故意针对GIF它只是按规则匹配匹配上了就用自己的数据格式返回完全不管原本应该是什么类型。4. 解决方案与落地实现四种办法和我的最终选择4.1 方案一修正Mock规则把正则从宽匹配改成精确匹配最早的解决方式很直接就是改Mock规则。项目里原来有一个全站兜底规则Mock.mock(/\/api\/.*/, { code: 0, data: {} });这个规则把所有/api/开头的请求全部mock掉了虽然方便但副作用很大。我把它拆成每个业务接口一条独立规则并且用字符串精确匹配或带结尾符的严格正则Mock.mock(/api/dashboard/summary, { code: 0, data: { total: 100, list: [] } }); Mock.mock(/\/api\/user\/info$/, { code: 0, data: { nickname: test, avatar: } });注意我用了/\/api\/user\/info$/末尾的$强制匹配结尾这样/api/user/info?debug1不会命中/api/user/info/extra也不会命中更不会误伤/api/user/info/avatar.gif。这个方案改造成本最低但要求团队里每个人在写mock规则时都足够克制不能图省事写宽泛正则。适合规则数量不多、团队成员能达成共识的小项目。4.2 方案二增加Mock开关让MockJS只在需要的环境加载MockJS这种工具就不应该在生产环境出现也不应该在联调环境里常驻。我给项目加了一个环境开关只有需要mock的开发环境才加载MockJS。在Vite项目中可以这么处理// src/main.js if (import.meta.env.VITE_USE_MOCK true) { import(./mock).then(({ setupMock }) { setupMock(); }); }在Vue 2或React Webpack项目中可以写成if (process.env.VUE_APP_USE_MOCK true) { require(./mock); }然后在.env.development里配置VUE_APP_USE_MOCKtrue生产环境默认不给这个变量或者设为false。这样做的最大好处是前端工程在非mock环境下根本不加载MockJS没有初始化就不会有拦截所有XHR请求都走原生逻辑图片、文件、字体资源完全不受影响。就算规则写得再宽也不会影响生产环境。这也是后来我们长期保留的开关。4.3 方案三业务层改用img标签直接渲染GIF针对那个抽奖组件我后来把GIF的加载方式改了。业务上原本需要先XHR拉取GIF数据再绘制到Canvas但仔细分析后发现场景只是做一个抽奖结果展示动画不需要逐帧操作直接用img标签就可以满足需求。改造前const res await axios.get(gifUrl, { responseType: blob }); const blobUrl URL.createObjectURL(res.data); document.getElementById(gifBox).innerHTML img src${blobUrl} /;改造后img :srcgifUrl altloading /直接用gifUrl作为img的src让浏览器原生去拉取和播放GIF动画。这个方案彻底绕开了XHR这条链路MockJS管不到img标签CDN和后端也没有额外压力。不过要提醒一句如果GIF地址带签名参数或临时token直接用img标签可能会遇到过期问题如果GIF是敏感资源需要带自定义Header鉴权img标签也不支持。这种场景还是得走XHR方案那就要优先确保放在Mock规则之外。4.4 方案四用fetch拉取二进制数据绕开MockJS拦截还有一个在技术上有意思的方案MockJS默认只拦截XMLHttpRequest不拦截fetch。如果业务场景确实需要把GIF二进制拉下来、传给Canvas绘制可以用fetch实现async function loadGifWithFetch(gifUrl) { const res await fetch(gifUrl); const blob await res.blob(); const objectUrl URL.createObjectURL(blob); return objectUrl; }用这段代码替换原有axios逻辑后即使MockJS已经被加载只要MockJS没有额外做fetch相关的拦截有些二次封装可能会做需要检查项目GIF请求就会绕过MockJS真正打到后端返回正常的image/gif二进制流。这个方案适合那些确实需要对GIF做逐帧绘制、裁剪、合成处理的场景因为必须拿到原始二进制数据。但它有一个副作用如果后端接口没就绪本地就不能用Mock数据调试这个GIF流程了。所以这个方案更适合“后端已联调完毕、仅本地残留MockJS”的场景。4.5 方案五推荐组合策略白名单 精确规则 环境开关最终我在项目里落地的是组合方案也是我更推荐的工程化处理方式。首先在utils/resource.js里加一个静态资源判断函数export const isStaticResource (url) { try { const path new URL(url, window.location.origin).pathname; return /\.(gif|png|jpe?g|webp|bmp|svg)(\/|$)/i.test(path); } catch (e) { return false; } };然后在MockJS规则注册前统一校验// mock/index.js import Mock from mockjs; import { isStaticResource } from /utils/resource; const RULES [ { url: /api/dashboard/summary, data: { code: 0, data: { total: 100 } } } ]; RULES.forEach((rule) { if (typeof rule.url string isStaticResource(rule.url)) { console.warn([mock] 静态资源不允许mock已跳过: ${rule.url}); return; } Mock.mock(rule.url, rule.data); });同时保留第4.2小节的Mock开关生产环境完全不加载MockJS。这个组合的防守层次是第一层环境开关控制MockJS整体不进入生产环境。第二层静态资源URL不允许注册mock规则从源头避免图片被打包成JSON。第三层业务接口尽量用精确匹配注册减少泛匹配带来的误伤。这三层叠加之后即使有人手滑写了一个宽泛的正则也会被第二层拦截一部分即使静态资源判断因为URL格式特殊没拦住第一层也能保证生产环境不受影响。5. 常见问题与排查技巧实录5.1 常见问题速查表这次排查过程中我也整理了团队里容易出现的几个相关问题做成速查表后面再遇到可以直接对着看。现象原因解决办法页面GIF卡在第一帧无报错MockJS拦截了GIF的XHR请求返回模拟JSON文本精确mock规则或用img标签直接加载GIFCanvas调用drawImage抛解码错误传进drawImage的图片源不是合法二进制流无法解码检查响应Content-Type用fetch绕开MockJS拉取blob所有/api开头的图片全部变成JSONMock规则写成/\/api\//全链路通配改为精确接口正则增加静态资源白名单判断关闭MockJS后GIF恢复正常确认根因是MockJS干扰增加环境开关生产环境不加载MockJS图片直接显示成一行JSON文本请求被MockJS拦截且数据被序列化为字符串先看Network响应体再调整Mock规则同一张GIF有的页面正常有的页面异常正常页面用img加载异常页面走XHR统一GIF加载方式或明确静态资源不走mock规则这张表覆盖了我遇到的大部分情况。特别提醒一下“无报错”不代表没问题GIF卡住不转本身就是一个强信号要多留意那些状态码200但响应体不符合Content-Type的请求。5.2 排查MockJS干扰的几个实用技巧排查这类问题效率最高的不是一上来就翻源码而是先用排除法缩小范围。我总结几个对实战有帮助的小技巧。第一个技巧看Network面板时不要只看状态码重点看Response预览。如果图片请求的Response显示成一段JSON或纯文本那基本就是本地中间层篡改了响应体。把鼠标移到Preview标签页图片类响应会被渲染成图片预览这个特征非常直观。第二个技巧全项目统计一下哪里用了responseType: blob或者URL.createObjectURL这些地方都是二进制资源加载的重灾区。用IDE全局搜索重点审查这些代码的调用URL比漫无目的地打断点高效得多。第三个技巧直接把GIF地址复制粘到浏览器地址栏打开。如果浏览器地址栏能正常播放这张GIF说明图片资源和后端接口都没问题问题只会出在本地请求链路的拦截层。这一步能快速排除后端背锅。第四个技巧临时禁用MockJS。这个实验做起来非常快成本也低。把入口文件的Mock初始化代码注释掉刷新页面如果问题消失那90%就是MockJS的问题如果问题还在再去查代理转发、跨域、缓存等方向。这个对照实验在任何工具类库可疑时都建议先做。5.3 顺带澄清Canvas绘制GIF的动画误区排查过程中有同事提出一个很有意思的猜测是不是MockJS把Canvas动画帧的驱动搞坏了因为现象看起来就像“GIF在Canvas里停住了”。这里要澄清一下这属于两件事不能混淆。Canvas本身不具备播放GIF动画的能力。canvas.drawImage(img, 0, 0)执行时浏览器只会把当前图像帧画上去GIF后续帧不会被自动绘制。要实现Canvas里的GIF动画需要借助gifuct-js这类库解析GIF帧数据再用定时器或者requestAnimationFrame按帧渲染。所以如果代码是drawImage后就不管了那GIF在任何环境下画到Canvas都只会显示第一帧这跟MockJS无关。这次项目里真正影响GIF的是MockJS把原始图片数据换成了文本导致连第一帧都画不出来。理解这个区别很重要否则你会把MockJS的问题错误归类为Canvas API的问题导致排查方向完全跑偏。最后再分享一点我的心得体会排查完这个GoLive事件后我再也没有把MockJS当成一个“无副作用”的开发工具来看待。所有拦截HTTP请求的工具本质上都是在浏览器和服务器之间插了一脚拦截得越激进出问题的可能性越大。我现在的习惯是把Mock的边界画得非常清楚静态资源原则上不进mock规则业务接口能精确匹配就不用宽泛正则MockJS的加载用独立环境变量控制。这套习惯后来救了我好多次不仅是GIF包括SVG字体、PDF预览、Excel导出文件都曾经因为MockJS的过度拦截出现过类似问题。排查工具类库的干扰第一件事永远是把工具禁用跑一遍效率远超盲读源码。
返回列表