Skip to content

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。

javascript
// 创建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参数的方法。

javascript
// 从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中安全传输。

javascript
// 编码:将特殊字符转换为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)表示二进制数据的对象。

javascript
// 从Canvas获取Blob对象
const blob = await new Promise(resolve => {
  signatureCanvas.value.toBlob(resolve, 'image/png');
});

2.3.2 File对象

File对象继承自Blob,包含额外的文件信息如名称和修改日期。

javascript
// 从Blob创建File对象
const file = new File([blob], `signature_${sid}.png`, { type: 'image/png' });

2.4 location对象属性与方法

window.location对象提供了当前文档的URL信息和导航能力。

javascript
// 读取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跨窗口通信利用了:

  1. 修改哈希部分不会导致页面刷新
  2. 可以通过JavaScript访问哈希值
  3. URL可以在不同窗口/应用间传递

3.2 实现步骤

  1. 发送端:将数据编码后附加到URL的哈希部分
  2. 接收端:解析URL哈希部分,提取并解码数据

3.3 代码示例:电子签名场景

3.3.1 生成签名并编码到URL哈希

javascript
// 确认签名并上传
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哈希中提取签名数据

javascript
// 从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来实现跨域通信:

javascript
// 通知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 二维码签名流程

示例代码中实现了移动设备扫码签名并返回签名数据的完整流程:

  1. 生成带参数的签名页面URL

    javascript
    const 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}`;
    };
  2. 将URL转为二维码,用户通过移动设备扫描

  3. 移动设备上进行签名,完成后将数据通过URL哈希传回

  4. 检测URL参数变化,接收签名数据

    javascript
    const 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可能被网络监听者获取

最佳实践:

  1. 始终使用HTTPS
  2. 避免在URL中传递敏感信息
  3. 实现数据的过期机制
  4. 考虑使用更安全的通信机制(如WebSockets、postMessage)处理敏感数据

五、总结

Hash URL跨窗口通信是一种简单但强大的技术,适用于:

  • 跨窗口/应用数据传递
  • 状态持久化(保存在URL中)
  • 基于URL的路由和导航

通过合理使用URL、URLSearchParams、编码/解码函数和Blob/File对象,可以实现多种复杂的前端通信场景,如示例中的移动设备扫码签名流程。

在现代Web应用开发中,了解这些API的使用方法和底层原理,对于构建无缝的用户体验至关重要。