Hash URL跨窗口通信机制详解
Hash URL跨窗口通信是一种在不同窗口或应用间传递数据的轻量级方法,通过修改和读取URL的哈希部分实现数据传递,特别适用于前端应用间的消息传递。本文详细介绍相关的API、使用方法和底层原理。
一、基础概念
1.1 URL结构
一个完整的URL通常包含以下部分:
https://example.com:443/path/to/page?query=value&name=test#hashValue
- 协议(protocol):
https:
- 主机名(hostname):
example.com
- 端口(port):
443
- 路径(pathname):
/path/to/page
- 查询参数(search):
?query=value&name=test
- 哈希(hash):
#hashValue
1.2 哈希(Hash)部分特性
哈希部分具有以下特点:
- 不会随HTTP请求发送到服务器
- 修改哈希不会导致页面刷新
- 可以通过JavaScript读取和修改
- 修改后可以被添加到浏览器历史记录中
二、核心API介绍
2.1 URL与URLSearchParams
2.1.1 URL对象
URL
是一个Web API,用于解析、构造、规范化和编码URL。
// 创建URL对象
// // 1. 首先使用 decodeURIComponent 函数解码 callback 参数,将其中的百分号编码(如 %20)转换回原始字符
// 2. 然后使用解码后的 URL 字符串创建一个新的 URL 对象
// 3. 这样就可以方便地操作 URL 的各个部分(如域名、路径、查询参数、哈希值等)
const callbackUrl = new URL(decodeURIComponent(callback));
// 修改URL的各个部分
callbackUrl.hostname = '192.162.1.113'; // 修改主机名
callbackUrl.port = '3000'; // 修改端口
callbackUrl.protocol = window.location.protocol; // 修改协议
// 修改哈希部分
// encodeURIComponent 函数用于对 URL 组件进行编码,将特殊字符转换为 URL 安全的形式
// 它会将除了字母、数字、(-_.!~*'()) 这些字符外的所有字符转换为百分号编码
// 例如:空格 → %20、/ → %2F、+ → %2B、中文 → %E4%B8%AD%E6%96%87
// 在这里,我们对 objectKey 进行编码,确保其中的特殊字符不会破坏 URL 的结构
// 这对于传递包含特殊字符的数据(如文件路径、中文内容等)非常重要
callbackUrl.hash = `signature=${encodeURIComponent(objectKey)}`;
// 转换为字符串
window.location.href = callbackUrl.toString();
2.1.2 URLSearchParams
URLSearchParams
是处理URL查询字符串的接口,提供了读取、修改、添加和删除URL参数的方法。
// 从URL中获取查询参数
// 创建URLSearchParams对象,用于解析URL中的查询参数
// window.location.search返回URL中的查询字符串部分,包括问号(?)
// 例如:对于URL "https://example.com/page?name=test&id=123"
// window.location.search将返回 "?name=test&id=123"
// URLSearchParams提供了一组方法来方便地操作这些查询参数
// 如get()获取参数值、set()设置参数值、has()检查参数是否存在等
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('projectId');
const sid = urlParams.get('sid');
// 添加或修改查询参数
urlParams.set('scanned', scanFlag);
callbackUrl.search = urlParams.toString();
2.2 编码解码函数
2.2.1 encodeURIComponent与decodeURIComponent
这对函数用于对URL组件进行编码和解码,确保特殊字符能在URL中安全传输。
// 编码:将特殊字符转换为URL安全的形式
callbackUrl.hash = `signature=${encodeURIComponent(objectKey)}`;
// 解码:将编码后的字符串转换回原始形式
const callback = decodeURIComponent(urlParams.get('callback'));
2.2.2 编码解码原理
编码(
encodeURIComponent
):将非字母数字字符转换为%后跟两位十六进制数字- 空格→
%20
/
→%2F
+
→%2B
- 等等
- 空格→
解码(
decodeURIComponent
):将%后跟两位十六进制数字转换回原始字符
2.3 Blob与File对象
2.3.1 Blob对象
Blob
(Binary Large Object)表示二进制数据的对象。
// 从Canvas获取Blob对象
const blob = await new Promise(resolve => {
signatureCanvas.value.toBlob(resolve, 'image/png');
});
2.3.2 File对象
File
对象继承自Blob
,包含额外的文件信息如名称和修改日期。
// 从Blob创建File对象
const file = new File([blob], `signature_${sid}.png`, { type: 'image/png' });
2.4 location对象属性与方法
window.location
对象提供了当前文档的URL信息和导航能力。
// 读取URL的哈希部分
const hash = window.location.hash;
// 读取URL的查询字符串部分
const search = window.location.search;
// 修改URL(会导航到新URL)
window.location.href = callbackUrl.toString();
// 替换当前历史记录(不增加新历史记录)
window.history.replaceState({}, '', `${window.location.pathname}?projectId=${projectId}`);
三、Hash URL跨窗口通信实现
3.1 基本原理
Hash URL跨窗口通信利用了:
- 修改哈希部分不会导致页面刷新
- 可以通过JavaScript访问哈希值
- URL可以在不同窗口/应用间传递
3.2 实现步骤
- 发送端:将数据编码后附加到URL的哈希部分
- 接收端:解析URL哈希部分,提取并解码数据
3.3 代码示例:电子签名场景
3.3.1 生成签名并编码到URL哈希
// 确认签名并上传
const confirmSignature = async () => {
try {
// 将Canvas内容转为Blob
const blob = await new Promise(resolve => {
signatureCanvas.value.toBlob(resolve, 'image/png');
});
// 获取会话ID参数
const urlParams = new URLSearchParams(window.location.search);
const sid = urlParams.get('sid');
// 创建File对象
const file = new File([blob], `signature_${sid}.png`, { type: 'image/png' });
// 上传文件并获取ObjectKey
const objectKey = await ossService.uploadFile(file);
if (objectKey) {
// 获取回调URL
const callback = urlParams.get('callback');
if (callback) {
// 解码并创建URL对象
const callbackUrl = new URL(decodeURIComponent(callback));
// 配置回调地址(本地开发环境)
if (callbackUrl.hostname === 'localhost') {
callbackUrl.hostname = '192.168.1.116';
callbackUrl.port = '3000';
callbackUrl.protocol = window.location.protocol;
}
// 将签名数据编码到URL哈希部分
callbackUrl.hash = `signature=${encodeURIComponent(objectKey)}`;
// 导航到回调URL
window.location.href = callbackUrl.toString();
}
}
} catch (error) {
console.error('上传签名失败:', error);
}
};
3.3.2 从URL哈希中提取签名数据
// 从URL中提取项目ID和签名URL
const getCleanProjectIdAndSignature = () => {
// 获取projectId参数
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('projectId');
// 从哈希中获取签名URL
const hash = window.location.hash;
const signatureUrl = hash.startsWith('#signature=')
? decodeURIComponent(hash.replace('#signature=', ''))
: '';
return { projectId, signatureUrl };
};
// 使用解构赋值获取数据
const { projectId, signatureUrl } = getCleanProjectIdAndSignature();
// 如果有签名URL,更新表单状态
if (signatureUrl) {
formState.auditSignatureImage = signatureUrl;
// 清除哈希,保持URL整洁
window.history.replaceState({}, '', `${window.location.pathname}?projectId=${projectId}`);
}
3.4 跨域考量
当在不同域之间进行通信时,需要考虑同源策略限制。代码示例中使用了iframe来实现跨域通信:
// 通知PC端二维码已被扫描
if (callback) {
const callbackUrl = new URL(decodeURIComponent(callback));
// 添加scanned参数以通知PC端
const urlParams = new URLSearchParams(callbackUrl.search);
urlParams.set('scanned', scanFlag);
callbackUrl.search = urlParams.toString();
// 创建隐藏iframe实现跨域通信
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = callbackUrl.toString();
document.body.appendChild(iframe);
// 3秒后移除iframe
setTimeout(() => {
if (document.body.contains(iframe)) {
document.body.removeChild(iframe);
}
}, 3000);
}
四、高级用例
4.1 二维码签名流程
示例代码中实现了移动设备扫码签名并返回签名数据的完整流程:
生成带参数的签名页面URL
javascriptconst getSignaturePageUrl = () => { const currentUrl = window.location.href; const protocol = window.location.protocol; const ipAndPort = getLocalIP(); const baseUrl = `${protocol}//${ipAndPort}`; scanSessionId.value = `sign_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `${baseUrl}/m/sign?sid=${props.sid}&callback=${encodeURIComponent(currentUrl)}&scanFlag=${scanSessionId.value}`; };
将URL转为二维码,用户通过移动设备扫描
移动设备上进行签名,完成后将数据通过URL哈希传回
检测URL参数变化,接收签名数据
javascriptconst checkSignatureUrl = () => { const urlParams = new URLSearchParams(window.location.search); const signatureUrl = urlParams.get('signatureUrl'); const scannedFlag = urlParams.get('scanned'); if (scannedFlag && scannedFlag === scanSessionId.value) { showQRModal.value = false; clearAllIntervals(); router.push('/examine-project'); return; } if (signatureUrl) { emit('signature-complete', decodeURIComponent(signatureUrl)); const newUrl = window.location.pathname + window.location.search.replace(/[?&]signatureUrl=[^&]*/, ''); window.history.replaceState({}, '', newUrl); showQRModal.value = false; clearAllIntervals(); router.push('/examine-project'); } };
4.2 安全考量
使用Hash URL传递敏感信息存在安全风险:
- URL可能会被记录在浏览器历史、服务器日志中
- 对于非HTTPS连接,URL可能被网络监听者获取
最佳实践:
- 始终使用HTTPS
- 避免在URL中传递敏感信息
- 实现数据的过期机制
- 考虑使用更安全的通信机制(如WebSockets、postMessage)处理敏感数据
五、总结
Hash URL跨窗口通信是一种简单但强大的技术,适用于:
- 跨窗口/应用数据传递
- 状态持久化(保存在URL中)
- 基于URL的路由和导航
通过合理使用URL、URLSearchParams、编码/解码函数和Blob/File对象,可以实现多种复杂的前端通信场景,如示例中的移动设备扫码签名流程。
在现代Web应用开发中,了解这些API的使用方法和底层原理,对于构建无缝的用户体验至关重要。