接手过好几个在IE下崩掉的Vue后台项目,印象最深的是一个报表审核系统:父页面有一整套筛选条件对象,点“详情”要传给iframe子页面渲染。Chrome下一切正常,IE11下子页面打开总是提示“找不到对象属性”,排查半天发现传过去的参数早就变成了字符串"[object Object]",对象里的字段一个不剩。这不是个例,Vue项目在IE下父页面给子页面传对象时“数据丢失”,九成都是被旧浏览器的通信机制和序列化规则坑掉的。这篇文章就把这个问题的根源、排查路径和最终实行的解决方案完整拆开讲一遍,适合正在折腾浏览器兼容、被iframe传参折磨的Vue开发者参考。
问题表现虽然五花八门,但核心只有两件事:旧浏览器的通信管道只认字符串,以及JSON序列化本身就会“丢”一部分数据。搞懂了这两点,问题基本就解决了一半。
1. 问题现场与根因拆解
1.1 先还原一下崩溃现场
某后台系统,Vue2技术栈,父页面维护一个查询参数对象,大概长这样:
queryParams: { pageNum: 1, pageSize: 20, startDate: '2024-06-01', endDate: '2024-06-30', keyword: '', statusList: [1, 2], filterMap: { dept: '研发部', level: 'P6' }, onSearch: function () { /* 某个回调函数 */ } }用户在父页面点击“在新页面打开详情”,代码把整个对象直接塞给iframe子页面:
this.$refs.childFrame.contentWindow.name = this.queryParams;注意,这里没有JSON序列化。Chrome里一切正常,IE11里子页面接收时,window.name已经变成了一个字符串,内容是"[object Object]"。子页面再怎么解析也拿不回任何属性,于是所有字段全部丢失。
1.2 表层原因:IE的通信管道只认字符串
所有跨页面通信手段,在IE上都有个共同限制——不认对象。
window.name属性声明为字符串,往里面赋对象时,浏览器会强制调用toString(),得到"[object Object]";- URL参数拼接对象,同样会先转成字符串,结果还是
"[object Object]"; window.postMessage在IE下不执行结构化克隆,传对象进去按字符串处理,依然会丢失结构;- 本地存储(localStorage/sessionStorage)只能存字符串,直接存对象取出来也是
"[object Object]"。
现代浏览器因为内置了结构化克隆算法,才让“直接传对象”这件事变得理所当然。IE没有这套东西,所以它严格要求只能走字符串。
1.3 深层原因:JSON序列化本身会丢数据
如果把上面的代码改成JSON.stringify(this.queryParams)再传,也不是万事大吉。JSON格式本身有表达边界,下面这些值都会被悄悄丢掉:
- 值为
undefined的字段:序列化时整体省略; - 函数字段:整体消失;
Symbol类型的键或值:被忽略;Date对象:变成ISO字符串,但类型信息丢失;- 正则表达式:变成空对象
{}; NaN、Infinity:变成null;- 循环引用的对象:直接抛异常,程序中断。
所以很多开发者在IE下测试时发现“有些字段传过去了,有些字段没了”,其实就是上述规则在起作用。
1.4 环境层因素:Vue响应式对象的额外干扰
如果是Vue3项目,reactive()返回的是Proxy对象,序列化时通常能正常处理,但某些特定结构(比如被Proxy包裹的Map、Set、Date)在IE下的表现与Chrome不同。Vue2走的是Object.defineProperty,IE8以下对普通JS对象不支持该API,IE9+虽然支持,但对象的Getter/Setter在某些操作下也会引发序列化异常。
这些环境差异叠加到一起,现象就是各种“数据神秘失踪”。结论先行:问题十有八九出在“没做显式字符串化封装”或者“做了但没考虑序列化丢字段”上,跟Vue框架本身关系不大。
2. 传输通道选型:IE下哪些通道会“吞”对象
2.1 先列出所有可用通道
父页面给iframe子页面传参,常见的通道有5种,它们在IE下的表现完全不同:
| 传输通道 | 支持的数据类型 | 跨域支持 | IE下的实际表现 |
|---|---|---|---|
URL query参数(?a=1&b=2) | 只能字符串 | 支持 | 对象被toString,中文乱码问题严重,URL长度受限 |
window.name | 只能字符串 | 跨域时读取受限(同域可靠) | 赋对象变成[object Object] |
window.postMessage | 字符串(IE下不支持对象) | 支持 | 传对象被转字符串处理,字段结构丢失 |
localStorage/sessionStorage | 只能字符串 | 跨域不共享 | 键值对存储,需要手动序列化和清理 |
直接操作DOM属性(如iframe.dataset) | 只能字符串 | 同域可写 | 只能存字面量,同样需要序列化 |
可以看到,在IE下没有一个通道能直接透传对象。凡是UI框架和配套组件能在Chrome里“直接扔对象”,那是现代浏览器在替你收拾残局,IE不干这活。
2.2 重点讲两个最常见通道
方式一:window.name
它本质是个窗口名称属性,页面跳转和刷新后保留,这是它作为传输通道的最大价值。要知道,页面跳转过程中,普通JS变量会全部清空,但window.name不会,所以它可以跨页面携带数据。
问题在于:给window.name赋非字符串对象,IE会静默调用toString()。我调试过很多次,在赋值语句后面立刻读取,拿到的就是"[object Object]"。这种失败没有报错、没有警告,所以很多人不会立刻意识到问题出在赋值环节。
方式二:window.postMessage
IE8+对postMessage的支持,只是支持了“发字符串消息”这一层基础能力。它不是标准的结构化克隆实现。在Chrome里这样写没问题:
otherWindow.postMessage({ type: 'detail', query: this.queryParams }, '*');到IE里,这个对象参数会被强制转成字符串。接收方可能收到"[object Object]",甚至在某些场景下字符串里还会带上[object Object]这种无意义的内容。
所以我后来定了一条铁律:
在IE兼容环境下,不要把任何对象直接丢给任何通信通道,一律先
JSON.stringify再传,到了接收端再JSON.parse。
2.3 同域与跨域的不同决策
如果是同域页面,优先用window.name,简单可靠。流程是:父页面等待iframe加载完成后,把序列化字符串写入iframe.contentWindow.name;子页面在mounted阶段读取window.name并解析。
如果是跨域页面,window.name在IE下访问受限,此时改用postMessage,但发送端和接收端都必须做字符串序列化与解析。
3. 序列化才是“丢数据”的元凶
3.1 JSON.stringify到底会弄丢什么
用一个例子直观演示。假如父页面的对象是:
const source = { title: '5月报表', count: 30, careful: undefined, cb: function () {}, publishDate: new Date('2024-06-30T10:00:00'), pattern: /^reg$/g, price: NaN, cyclic: null }; source.cyclic = source; // 循环引用直接JSON.stringify(source),结果如下:
// 循环引用时直接抛异常:TypeError: Converting circular structure to JSON // 不抛异常时,实际序列化结果为: '{"title":"5月报表","count":30,"publishDate":"2024-06-30T10:00:00.000Z","pattern":{},"price":null}'对比原对象,丢失情况一目了然:
| 原字段 | 序列化结果 | 结果说明 |
|---|---|---|
careful: undefined | 字段消失 | JSON无undefined概念 |
cb: function | 字段消失 | 函数不可被JSON表达 |
publishDate: Date | "2024-06-30T10:00:00.000Z" | 变成字符串,类型丢失 |
pattern: /^reg$/g | {} | 正则丢失source和flags |
price: NaN | null | 数值变null |
cyclic: 循环引用 | 抛异常 | 无法处理环 |
3.2 特殊类型字段的还原思路
如果子页面需要的是一个完整可用的字段结构,就得做定制化序列化方案,核心是给特殊类型加类型标记。
我的做法是在对象遍历时针对不同类型做处理:
- 遇到
Date,序列化为{ __type: 'Date', value: date.toISOString() },解析时new Date(value)还原; - 遇到
RegExp,序列化为{ __type: 'RegExp', source: regExp.source, flags: getRegExpFlags(regExp) },解析时new RegExp(source, flags)还原; - 遇到
undefined和函数,根据业务需求选择忽略或单独标记。我的经验是多数查询参数里的函数根本不需要传,忽略反而更干净;但如果确实要把函数“带过去”,可以在接收端用一个预定义函数映射表来还原,而不是试图序列化函数体; - 遇到
NaN、Infinity,序列化为{ __type: 'NaN' }等标记,解析时再转回来。
3.3 循环引用的兜底方案
给对象做深度遍历前,先用一个WeakMap或数组记录已经访问过的对象引用,一旦发现重复引用,直接抛错或截断。不要等到JSON.stringify抛TypeError,因为那段异常信息在IE下非常不友好,问题定位效率很低。
建议在生产代码里做一层try-catch包裹:
function safeStringify(source) { try { return JSON.stringify(source); } catch (err) { console.warn('[serialize] 循环引用或非法数据,已忽略', err); return '{}'; } }这样即使管线断裂,也不会把整个业务阻塞掉,最多是子页面拿不到参数、展示空状态,排查起来反而更快。
4. 一个可落地的跨页面传输方案
4.1 自定义序列化工具函数
为了把上面这些坑一次性踩平,我封装了一套通用的传输工具,命名就叫transport.js。核心思路:发送端先对对象做“类型强化”序列化,再转成JSON字符串;接收端先做JSON解析,再做“类型还原”。
// transport.js function getRegExpFlags(reg) { let flags = ''; if (reg.global) flags += 'g'; if (reg.ignoreCase) flags += 'i'; if (reg.multiline) flags += 'm'; return flags; } function encodeValue(value, seen) { // 基础类型直接返回 if (value === null) return null; const type = typeof value; if (type === 'string' || type === 'number' || type === 'boolean') { return value; } if (type === 'undefined') { return { __transType: 'undefined' }; } // 特殊包装对象 if (value instanceof Date) { return { __transType: 'Date', value: value.toISOString() }; } if (value instanceof RegExp) { return { __transType: 'RegExp', source: value.source, flags: getRegExpFlags(value) }; } if (typeof value === 'function') { return { __transType: 'Function', name: value.name || 'anonymous' }; } // 数组逐项处理 if (Array.isArray(value)) { if (seen.has(value)) { throw new Error('数据存在循环引用'); } seen.add(value); const result = value.map(function (item) { return encodeValue(item, seen); }); seen.delete(value); return result; } // 普通对象逐属性处理 if (type === 'object') { if (seen.has(value)) { throw new Error('数据存在循环引用'); } seen.add(value); const result = {}; Object.keys(value).forEach(function (key) { result[key] = encodeValue(value[key], seen); }); seen.delete(value); return result; } return value; } function decodeValue(value, seen) { if (value === null) return null; if (Array.isArray(value)) { if (seen.has(value)) return null; seen.add(value); const result = value.map(function (item) { return decodeValue(item, seen); }); seen.delete(value); return result; } if (typeof value === 'object') { // 先判断是否为特殊类型标记 if (value.__transType === 'Date') { return new Date(value.value); } if (value.__transType === 'RegExp') { return new RegExp(value.source, value.flags); } if (value.__transType === 'undefined') { return undefined; } if (value.__transType === 'Function') { return undefined; // 函数不还原,调用方自行处理 } if (seen.has(value)) return null; seen.add(value); const result = {}; Object.keys(value).forEach(function (key) { result[key] = decodeValue(value[key], seen); }); seen.delete(value); return result; } return value; } function encodeTransfer(data) { return JSON.stringify(encodeValue(data, new WeakSet())); } function decodeTransfer(sourceStr) { if (!sourceStr) return null; try { return decodeValue(JSON.parse(sourceStr), new WeakSet()); } catch (err) { console.warn('[transport] 参数解析失败', err); return null; } }这个思路很简单:在JSON字符串之外,用__transType标记记录原始类型,从而最大程度减少“类型丢失导致的字段丢失”。
4.2 调用方式
父页面发送时:
import { encodeTransfer } from './transport'; // 等待iframe加载完成 const iframeEl = this.$refs.childFrame; iframeEl.addEventListener('load', function () { iframeEl.contentWindow.name = encodeTransfer(this.queryParams); });子页面接收时:
import { decodeTransfer } from './transport'; const params = decodeTransfer(window.name) || {}; // params.statusList、params.filterMap 等字段都能完整拿到如果走postMessage,发送端只要把encodeTransfer的结果传入即可,接收端从event.data拿到字符串后同样调decodeTransfer。
4.3 为什么不用现成的json3或替代品
市面上有第三方库能补IE的JSON空白,比如json3,但它的作用只是让IE8以下环境多一个JSON对象。我们遇到的真正问题不是“没有JSON”,而是“JSON格式天然丢失类型信息”,所以自定义一套带类型标记的序列化规则才是关键一步。
5. 实操流程:把方案嵌入现有Vue项目
5.1 改造前先做一次现状评估
动手前先确认三件事:
- 组件传参还是iframe传参?如果是Vue父子组件
props传对象,IE下极少丢对象,丢的是Vue响应式依赖本身,问题要往别处排查。本文实际聚焦的是“页面”级(iframe或窗口)传参; - 同域还是跨域?同域用
window.name,跨域用postMessage,两种通道的改造点不同; - 对象里是否有函数和Date字段?有函数就要确认子页面是否依赖该函数;有Date就得在解码时主动还原。
5.2 父页面发送端改造要点
核心是不要在iframe还没加载完时赋值。一开始我用this.$refs.childFrame.contentWindow.name = ...,结果经常赋值时机太早,子页面读取时值为空。后来统一改为监听iframe的load事件后再写入:
methods: { openDetail() { const iframeEl = this.$refs.childFrame; if (iframeEl.attachEvent) { // IE8/IE9兼容写法 iframeEl.attachEvent('onload', () => { this.writeParamToChild(iframeEl); }); } else { iframeEl.addEventListener('load', () => { this.writeParamToChild(iframeEl); }); } }, writeParamToChild(iframeEl) { const transferStr = encodeTransfer(this.queryParams); iframeEl.contentWindow.name = transferStr; } }这里有个细节,queryParams里的函数因为被encodeTransfer转成了标记对象,在子页面解码后会被还原成undefined,不会报错。但如果你担心业务代码在子页面调用函数导致异常,建议父页面在传参前做一层白名单字段提取,只传出子页面真正用到的字段。
5.3 子页面接收端改造要点
子页面在created或mounted里读取即可:
created() { // 解析父页面通过window.name传递的参数 const raw = window.name; this.params = decodeTransfer(raw) || {}; }这里要注意:读取后尽量把window.name重置为空字符串,否则用户关闭子页面再次打开,旧参数会残留,表现形式就是“参数莫名其妙不刷新”:
window.name = '';不过重置动作要小心,如果同页其他逻辑还在用window.name,会一起被清掉。为了稳妥,只在当前页面不再需要该值后清空。
5.4 跨域场景的postMessage改造
跨域场景下,父页面没法直接访问iframe.contentWindow.name,要用postMessage:
iframeEl.contentWindow.postMessage( encodeTransfer(this.queryParams), '*' );子页面监听:
window.addEventListener('message', function (event) { // 生产环境务必校验event.origin,防止任意页面投递数据 if (event.origin !== 'https://your-company-domain.com') return; const params = decodeTransfer(event.data) || {}; // 业务逻辑 });IE浏览器对event.origin支持不完整,某些版本只能用event.originalEvent.origin兜底。这是个容易被忽略的兼容细节。
5.5 验收清单
改造完成后,至少跑一轮下面的用例:
| 用例 | 期望结果 |
|---|---|
| 普通对象传参 | 子页面拿到完整字段结构 |
| 含Date字段 | 子页面拿到Date实例,能调用getTime() |
| 含正则表达式 | 子页面拿到可运行的RegExp,能test() |
| 含undefined与函数 | 子页面不报错,undefined安全降级 |
| 循环引用对象 | 父页面不崩溃,控制台输出明确警告 |
| 连续两次打开子页面 | 第二次不发旧参数残留问题 |
| IE11 + Chrome 并行回归 | 两边行为一致 |
这套用例是我的固定测试清单,凡是涉及页面通信的改动都要过一遍。
6. 常见问题与排查技巧实录
6.1 典型问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
子页面收到"[object Object]" | 没做JSON序列化,对象被toString | 统一走encodeTransfer |
子页面收到的字符串被二次解析成[object Object] | 子页面代码里多了一次JSON.parse | 检查接收端是否重复解析 |
日期字段显示成"2024-06-30T00:00:00.000Z" | 序列化丢失Date类型标记 | 用encodeValue/decodeValue还原 |
| 函数字段全部消失,子页面调用报错 | JSON格式不认函数 | 白名单字段提取,或函数映射表还原 |
| 第二次打开子页面参数还是旧的 | window.name未清空 | 子页面消费后置空 |
| IE8以下白屏,报JSON未定义 | 环境缺少JSON对象 | 引入json3或自行垫片 |
| 页面间传完整对象时什么也没发生 | iframe还没load好就赋值 | 挂在load事件后写入 |
| URL传参时中文变乱码或参数过长 | URL长度限制+编码不一致 | 改用window.name/postMessage |
6.2 几个值得单独说说的坑
坑一:你以为的JSON.stringify很安全
有人做了序列化,但传输后还是“丢”了字段。仔细检查,丢的是undefined字段。如果业务上确实需要把undefined也传过去表达“用户没填”,就用encodeTransfer的类型标记方案。否则就在父页面提前把这类字段改成空字符串或null。
坑二:给window.name赋值字符串后,不同页面跳转时的继承问题
window.name在页面跳转和刷新后依然保留,这是它当传输通道的优势。但反过来,如果子页面有二级页面导航,跳到另一个页面时也能读到这个值,这算副作用。建议子页面在拿到参数后即时清空。
坑三:IE下WeakSet不可用怎么办
代码里我用了WeakSet做循环引用检测,但IE11及以上才支持WeakSet,IE9/IE10没有这个对象。如果你还要兼容IE9、IE10,就用普通数组代替:
const seenList = []; if (seenList.indexOf(value) !== -1) { throw new Error('循环引用'); } seenList.push(value);代价是数组遍历性能略差,但页面通信的数据量通常很小,完全够用。
坑四:Vue的响应式对象往往会带__ob__
Vue2会给响应式对象附加__ob__标记,JSON.stringify本来会忽略掉它,但自定义遍历时如果不做过滤,会把__ob__相关的观察者对象也序列化进去,导致数据异常膨胀。给encodeValue增加一个判断,跳过__ob__开头或__开头的内部字段。
我做过的方案里直接过滤:
if (key.indexOf('__') === 0) { return; // 跳过Vue内部标记 }6.3 避坑技巧:加一个调试开关
兼容性问题最大的麻烦是定位困难。我在transport.js里加了一个调试开关:
const DEBUG = true; function logTransfer(action, data) { if (DEBUG) { console.log('[transfer]', action, data); } }父页面发送前打印一次序列化结果,子页面接收后打印一次解析结果,两边一对比,哪个环节丢数据一眼就能看出来。上线前把DEBUG设为false即可,成本极低,但排查效率能提升好几倍。
6.4 最后再分享一个经验
这类问题的核心心法只有一句:在任何兼容环境里,永远不要依赖隐式的类型转换或隐式结构化克隆,显式序列化才是唯一可靠的路。我现在给新项目写通信代码时,不管目标浏览器是Chrome还是Electron还是IE,一律统一走encodeTransfer/decodeTransfer这套封装。表面上多写了两行代码,实际上把跨浏览器、跨页面、跨版本的兼容性坑全部堵住了,省下的排查时间远远超过这点开发成本。
如果你手头正好有一个在IE下时好时坏的Vue传参问题,大概率就是上面某个原因,按顺序排查一遍,基本都能收工。