今天是 2025 年 06 月 19 日,下班之前给大家分享一下我在项目里通过 URL 传递数据遇到的一些问题。
我们有一个后台系统,产品经理提出一个需求:在当前系统内部调用另一个系统的一些服务。
两个系统之间的代码虽然可以通用,但之前并没有做 Monorepo 之类的规划,因此我们决定通过 iframe 引入另一个系统的服务。
细节方面不再赘述 🤣
在常规引入 iframe 之前,我对目标项目做了修改,新增了一个无鉴权的路由,通过 URL 接收当前系统传来的基础数据和鉴权 token,然后用 token 换取目标系统的 JWT 数据,再重定向到最终的路由。
整个流程非常简单,直到今天测试同事找到我说页面打开报错了。
起因很简单,后端提供了一个超大的 JSON 数据,我编码后通过 URL 传递给目标系统的 Next.js 服务进行处理,导致 Next.js 返回了 431 状态码(Request Header Fields Too Large)。这可能是 URL 过长导致的问题。
我首先想到,JSON 数据序列化为字符串后,经 encodeURIComponent()
编码可能过长。此前数据序列化后长度正常。
encodeURIComponent()
编码可确保 URL 格式合法,防止查询分隔符破坏结构,处理非 ASCII 字符,并提升跨平台兼容性。
于是我灵光一闪,想到此前处理过一个场景:通过 btoa()
将字符串转为 Base64 字符串,其长度通常比 encodeURIComponent()
编码的结果短。于是,我尝试通过 Base64 编码超大数据并传递。
btoa()
("binary to ASCII")是浏览器提供的内置方法,用于将二进制字符串编码为 Base64。
可惜我忽略了一点:btoa()
方法仅支持 Latin1 范围内的字符,无法处理中文、emoji 或特殊符号。
结果我犯蠢了,很显然:Base64 编码后字符串很长,甚至比 encodeURIComponent()
处理后更长。
于是我立即引入了 pieroxy/lz-string 库来压缩 Base64 字符串!
pieroxy/lz-string: LZ-based compression algorithm for JavaScript:压缩 Base64 字符串的利器,常用于 localStorage 存储或将图片解压为 Base64 嵌入 HTML 等场景。
🚀 不错,Base64 字符串压缩后变小了,lz-string
还能自动处理 Latin1 范围外的字符,省心省力!
但是...
Base64 字符串可能包含 URL 中的分隔字符。为确保数据完整性,仍需通过 encodeURIComponent()
进行编码。
通过 URL 传输超大 JSON 数据就像一条岔路,也许我们一开始就不应选择这种方式。
RESTful API 规范建议开发者通过 GET 请求 API 接口,通常通过 URL 传递参数实现,但不适合数据量大的场景。浏览器、后端、网关等都可能破坏 URL 中数据的完整性。
通过 Channel Messaging API 机制,在父窗口和 iframe 之间建立安全的双向通信信道。
如果你想了解原生 Channel Messaging API 的通信机制,可查看 Channel Messaging API - Web APIs | MDN。
如果是一年前,我可能会自己封装代码实现所需功能,打造一把“锤子”。
但现在,我直接使用了 Aaronius/penpal 库,它基于 postMessage 提供简化的通信功能。
让我们花几分钟看看示例,在父窗口创建 iframe 和 messenger 实例来连接子窗口:
import { WindowMessenger, connect } from 'penpal';
const iframe = document.createElement('iframe');
iframe.src = 'https://childorigin.example.com/path/to/iframe.html';
document.body.appendChild(iframe);
const messenger = new WindowMessenger({
remoteWindow: iframe.contentWindow,
// Defaults to the current origin.
allowedOrigins: ['https://childorigin.example.com'],
// Alternatively,
// allowedOrigins: [new URL(iframe.src).origin]
});
const connection = connect({
messenger,
// Methods the parent window is exposing to the iframe window.
methods: {
add(num1, num2) {
return num1 + num2;
},
},
});
const remote = await connection.promise;
// Calling a remote method will always return a promise.
const multiplicationResult = await remote.multiply(2, 6);
console.log(multiplicationResult); // 12
const divisionResult = await remote.divide(12, 4);
console.log(divisionResult); // 3
在子窗口也创建 messenger 来连接父窗口:
import { WindowMessenger, connect } from 'penpal';
const messenger = new WindowMessenger({
remoteWindow: window.parent,
// Defaults to the current origin.
allowedOrigins: ['https://parentorigin.example.com'],
});
const connection = connect({
messenger,
// Methods the iframe window is exposing to the parent window.
methods: {
multiply(num1, num2) {
return num1 * num2;
},
divide(num1, num2) {
// Return a promise if asynchronous processing is needed.
return new Promise((resolve) => {
setTimeout(() => {
resolve(num1 / num2);
}, 1000);
});
},
},
});
const remote = await connection.promise;
// Calling a remote method will always return a promise.
const additionResult = await remote.add(2, 6);
console.log(additionResult); // 8
配置明确的 allowedOrigins
后,消息传递具有较高的安全性。
此外,penpal
还适用于 Web Worker 之间的通信,尤其适合超大数据传输,能节省大量时间以专注于更有意义的工作。
注意:任何时候都可通过调用 connection.destroy()
销毁连接,以移除事件监听器并正确进行垃圾回收。
准备下班,经验总是在不经意间积累起来的,下次再见。
当然,期待大家都点赞、转发和支持!