普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月5日首页

胜通能源:七腾机器人已将要约收购保证金1.12亿元存入指定账户

2026年1月5日 20:56
36氪获悉,胜通能源公告,截至本公告披露日,收购方七腾机器人已将要约收购保证金合计1.12亿元存入证券登记结算机构指定的账户,七腾机器人参与本次收购的资金来源为自有资金及自筹资金,截至目前,自筹资金的申请仍在审批阶段,是否可以申请成功尚存在不确定性,敬请广大投资者注意投资风险,理性决策,审慎投资。

鹏辉能源:拟发行H股股票并在香港联交所主板上市

2026年1月5日 20:50
36氪获悉,鹏辉能源公告,公司拟发行境外上市外资股(H股)股票并申请在香港联交所主板挂牌上市,以深化全球化战略布局,提升国际市场综合竞争实力,打造国际化资本运作平台,增强境外融资能力。本次发行上市符合中国境内法律、法规和规范性文件的要求和条件,将根据《香港联交所上市规则》及中国香港法律进行,并需取得相关政府机构、监管机构的备案、审核或批准。公司将在股东会决议有效期内选择适当时机和发行窗口完成本次发行上市。

澜起科技通过港交所上市聆讯

2026年1月5日 20:45
36氪获悉,据港交所,澜起科技股份有限公司更新聆讯后资料集,意味着该公司港交所IPO通过聆讯。其联席保荐人为中金公司、摩根士丹利、UBS。

胜通能源:相关核查工作已完成,股票复牌

2026年1月5日 20:41
36氪获悉,胜通能源公告,停牌期间,公司就股票交易波动情况进行了核查。鉴于相关核查工作已完成,根据相关规定,经公司申请,公司股票将于2026年1月6日(星期二)开市起复牌。公司股票自2025年12月12日至12月29日期间价格涨幅为213.97%,公司股票短期内价格涨幅较大。公司主营业务及基本面未发生重大变化,不存在应披露而未披露的重大信息。

中国证监会召开资本市场财务造假综合惩防体系跨部门工作推进座谈会

2026年1月5日 20:35
36氪获悉,1月5日,中国证监会召开资本市场财务造假综合惩防体系跨部门工作推进座谈会。证监会党委书记、主席吴清主持会议并讲话,最高人民法院、最高人民检察院、国家发展改革委、公安部、司法部、财政部、中国人民银行、国务院国资委、税务总局、市场监管总局、金融监管总局等部门负责同志出席会议并发言,共同研究推进进一步加强财务造假综合惩防重点工作。证监会党委委员、副主席李超出席会议,证监会相关司局负责同志列席会议。

科伦药业:拟以5000万元-1亿元回购公司股份

2026年1月5日 20:20
36氪获悉,科伦药业公告,拟以5000万元-1亿元回购公司股份,用于后续员工持股计划或股权激励,回购价格不超过35元/股。本次回购所涉《关于回购公司股份方案的议案》已经2026年1月4日召开的科伦药业第八届董事会第十一次会议审议通过。

欧普康视:取得《药品生产许可证》

2026年1月5日 20:16
36氪获悉,欧普康视公告,公司近日收到安徽省药品监督管理局颁发的《药品生产许可证》。许可证编号皖20250658,有效期至2030年12月25日。公司表示,该许可证的取得有利于优化产品结构,对长远发展有积极推动作用,但短期内不会对经营业绩产生重大影响。公司还需通过在研产品注册申报、现场核查及药品GMP符合性检查等程序,并取得《药品注册证》后,方可进行商业化生产销售。医药产品的生产、销售受行业政策、市场环境等因素影响,存在不确定性。

GDAL 实现创建几何对象

作者 GIS之路
2026年1月5日 20:14

^ 关注我,带你一起学GIS ^

前言

几何对象作为图层要素对象,具有点、线、面、多点、多线、多线等多种类型。每种类型都有自己独特的结构,在GIS开发中,需要掌握其组成结构和创建方式。

在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL实现创建几何对象。

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 创建点几何对象

定义一个方法CreatePoint用于创建点Shp图层,接收一个参数pointPath指向点文件路径。

"""
说明:创建点Shp图层
参数:
    -pointPath:点文件路径
"""
def CreatePoint(pointPath):

GDAL中使用Geometry方法创建点几何对象,需要传入一个几何对象类型参数,创建点时该参数为ogr.wkbPointGeometryTypeToName方法可以获取几何对象类型名称,如下输出“Point”。接着使用AddPoint将坐标点添加到点对象中。

# 创建点几何对象
pointGeometry = ogr.Geometry(ogr.wkbPoint)
geomeTypeName = ogr.GeometryTypeToName(pointGeometry.GetGeometryType())
# print(f"几何对象名称:{geomeTypeName}")
# 添加坐标点
pointGeometry.AddPoint(100.225207,27.665721)

如下为创建点Shp图层完整方法。

"""
说明:创建点几何对象
参数:
    -pointPath:点文件路径
"""
def CreatePoint(pointPath):
    print("开始创建点对象")

    # 添加shp数据驱动
    shpDriver = gdal.GetDriverByName("ESRI Shapefile")

    checkShpPath(pointPath,shpDriver)

    # 创建shp数据源
    shpDataSource = shpDriver.CreateDataSource(pointPath)
    if shpDataSource is None:
        print("无法创建点Shp数据源,请检查文件!")
        return False

    # 创建点数据图层
    pointLayer = shpDataSource.CreateLayer("point",geom_type=ogr.wkbPoint)

    # 添加id字段
    fieldDefn = ogr.FieldDefn("id",ogr.OFTInteger)
    pointLayer.CreateField(fieldDefn)

    # 获取要素结构
    featureDefn = pointLayer.GetLayerDefn()

    # 创建要素对象
    pointFeature = ogr.Feature(featureDefn)

    points = [
       "100.225207,27.665721",
       "105.330257,29.948948",
       "105.848739,23.856316",
       "99.567134,23.892995"
    ]
    # pointGeometry.AddPoint(100.225207,27.665721)
    for i,point in enumerate(points):
        # print(f"点:{point}")
        res = point.split(",")
        # print(f"点:{res}")

        # 创建点几何对象
        pointGeometry = ogr.Geometry(ogr.wkbPoint)
        geomeTypeName = ogr.GeometryTypeToName(pointGeometry.GetGeometryType())
        # print(f"几何对象名称:{geomeTypeName}")

        pointGeometry.AddPoint(float(res[0]),float(res[1]))

        pointFeature.SetGeometry(pointGeometry)
        pointFeature.SetField("id",i)
        pointLayer.CreateFeature(pointFeature)

3. 创建线几何对象

定义一个方法CreatePolyline用于创建点Shp图层,接收一个参数polylinePath指向线文件路径。

"""
说明:创建线Shp图层
参数:
    -polylinePath:线文件路径
"""   
def CreatePolyline(polylinePath):

使用Geometry方法创建线几何对象,将几何对象类型改为ogr.wkbLineString。接着使用AddPoint将坐标点添加到线对象中。

# 创建线几何对象
polylineGeometry = ogr.Geometry(ogr.wkbLineString)
geomeTypeName = ogr.GeometryTypeToName(polylineGeometry.GetGeometryType())
# 添加坐标点
polylineGeometry.AddPoint(100.225207,27.665721)
polylineGeometry.AddPoint(105.330257,29.948948)
polylineGeometry.AddPoint(105.848739,23.856316)
polylineGeometry.AddPoint(99.567134,23.892995)

如下为创建线Shp图层完整方法。

"""
说明:创建线Shp图层
参数:
    -polylinePath:线文件路径
"""   
def CreatePolyline(polylinePath):
    print("开始创建线对象")

    # 添加shp数据驱动
    shpDriver = gdal.GetDriverByName("ESRI Shapefile")

    checkShpPath(polylinePath,shpDriver)

    # 创建shp数据源
    shpDataSource = shpDriver.CreateDataSource(polylinePath)
    if shpDataSource is None:
        print("无法创建点Shp数据源,请检查文件!")
        return False

    # 创建线数据图层
    polylineLayer = shpDataSource.CreateLayer("polyline",geom_type=ogr.wkbLineString)

    # 添加id字段
    fieldDefn = ogr.FieldDefn("id",ogr.OFTInteger)
    polylineLayer.CreateField(fieldDefn)

    # 获取要素结构
    featureDefn = polylineLayer.GetLayerDefn()

    # 创建要素对象
    pointFeature = ogr.Feature(featureDefn)
    points = [
       "100.225207,27.665721",
       "105.330257,29.948948",
       "105.848739,23.856316",
       "99.567134,23.892995",
       "100.225207,27.665721"
    ]
    # pointGeometry.AddPoint(100.225207,27.665721)
    # 创建线几何对象
    polylineGeometry = ogr.Geometry(ogr.wkbLineString)
    geomeTypeName = ogr.GeometryTypeToName(polylineGeometry.GetGeometryType())
    polylineGeometry.AddPoint(100.225207,27.665721)
    polylineGeometry.AddPoint(105.330257,29.948948)
    polylineGeometry.AddPoint(105.848739,23.856316)
    polylineGeometry.AddPoint(99.567134,23.892995)
    # print(f"几何对象名称:{geomeTypeName}")
    for i,point in enumerate(points):
        # print(f"点:{point}")
        res = point.split(",")
        # print(f"点:{res}")

        polylineGeometry.AddPoint(float(res[0]),float(res[1]))

    pointFeature.SetGeometry(polylineGeometry)
    pointFeature.SetField("id",1)

    polylineLayer.CreateFeature(pointFeature)

4. 创建面几何对象

定义一个方法CreatePolygon用于创建面Shp图层,接收一个参数polygonPath指向线文件路径。

"""
说明:创建面Shp图层
参数:
    -polygonPath:面文件路径
"""     
def CreatePolygon(polygonPath):

在创建面几何对象之前需要先创建环形几何对象,类型参数为ogr.wkbLinearRing

# 创建一个环形Geometry
ringGeometry = ogr.Geometry(ogr.wkbLinearRing)
geomeTypeName = ogr.GeometryTypeToName(ringGeometry.GetGeometryType())

ringGeometry.AddPoint(100.225207,27.665721)
ringGeometry.AddPoint(105.330257,29.948948)
ringGeometry.AddPoint(105.848739,23.856316)
ringGeometry.AddPoint(99.567134,23.892995)

然后使用Geometry方法创建线面何对象,将几何对象类型改为ogr.wkbPolygon。接着使用AddGeometry将环形对象添加到面中。

# 创建面几何对象
polygonGeometry = ogr.Geometry(ogr.wkbPolygon)
# 添加环形对象
polygonGeometry.AddGeometry(ringGeometry)

此外还有一种带有孔洞的多边形需要注意一下,此时环形几何对象分为内环和外环两种类型。

# 创建一个外环Geometry
outRingGeometry = ogr.Geometry(ogr.wkbLinearRing)

outRingGeometry.AddPoint(100.225207,27.665721)
outRingGeometry.AddPoint(105.330257,29.948948)
outRingGeometry.AddPoint(105.848739,23.856316)
outRingGeometry.AddPoint(99.567134,23.892995)
outRingGeometry.AddPoint(100.225207,27.665721)
# 关闭环状几何
# outRingGeometry.CloseRings()

# 创建一个内环Geometry
inRingGeometry = ogr.Geometry(ogr.wkbLinearRing)

inRingGeometry.AddPoint(100.415207,27.665721)
inRingGeometry.AddPoint(104.550257,27.665721)
inRingGeometry.AddPoint(104.550257,24.456316)
inRingGeometry.AddPoint(100.415207,24.456316)
inRingGeometry.AddPoint(100.415207,27.665721)
# 关闭环状几何
# inRingGeometry.CloseRings()

# 创建面几何对象
polygonGeometry = ogr.Geometry(ogr.wkbPolygon)
polygonGeometry.AddGeometry(outRingGeometry)
polygonGeometry.AddGeometry(inRingGeometry)

对于面几何对象创建完成后需要关闭环状几何,可以调用几何对象CloseRings()或者在最后添加与第一个坐标相同点进行关闭。

outRingGeometry.CloseRings()
inRingGeometry.CloseRings()

如下为创建面Shp图层完整方法。

"""
说明:创建面Shp图层
参数:
    -polygonPath:面文件路径
"""     
def CreatePolygon(polygonPath):
    print("开始创建面对象")

    # 添加shp数据驱动
    shpDriver = gdal.GetDriverByName("ESRI Shapefile")

    checkShpPath(polygonPath,shpDriver)

    # 创建shp数据源
    shpDataSource = shpDriver.CreateDataSource(polygonPath)
    if shpDataSource is None:
        print("无法创建点Shp数据源,请检查文件!")
        return False

    # 创建面数据图层
    polygonLayer = shpDataSource.CreateLayer("polygon",geom_type=ogr.wkbPolygon)

    # 添加id字段
    fieldDefn = ogr.FieldDefn("id",ogr.OFTInteger)
    polygonLayer.CreateField(fieldDefn)

    # 获取要素结构
    featureDefn = polygonLayer.GetLayerDefn()

    # 创建要素对象
    polygonFeature = ogr.Feature(featureDefn)

    points = [
       "100.225207,27.665721",
       "105.330257,29.948948",
       "105.848739,23.856316",
       "99.567134,23.892995",
       "100.225207,27.665721"
    ]
    # pointGeometry.AddPoint(100.225207,27.665721)
    # 创建一个环形Geometry
    ringGeometry = ogr.Geometry(ogr.wkbLinearRing)
    geomeTypeName = ogr.GeometryTypeToName(ringGeometry.GetGeometryType())

    # print(f"几何对象名称:{geomeTypeName}")
    for i,point in enumerate(points):
        print(f"点:{point}")
        res = point.split(",")
        print(f"点:{res}")

        ringGeometry.AddPoint(float(res[0]),float(res[1]))

    # 关闭环状几何
    # or ringGeometry.CloseRings()

    # 创建面几何对象
    polygonGeometry = ogr.Geometry(ogr.wkbPolygon)
    polygonGeometry.AddGeometry(ringGeometry)

    polygonFeature.SetGeometry(polygonGeometry)
    polygonFeature.SetField("id",1)

    polygonLayer.CreateFeature(polygonFeature)

5. 总结

创建图层前第一步都是要需要获取数据驱动,如Shp数据驱动为"ESRI Shapefile"

# 添加shp数据驱动
shpDriver = gdal.GetDriverByName("ESRI Shapefile")

然后便可以创建数据源,并检查数据源是否创建成功。

# 创建shp数据源
shpDataSource = shpDriver.CreateDataSource(polygonPath)
if shpDataSource is None:
    print("无法创建点Shp数据源,请检查文件!")
    return False

创建目标图层,"polygon"为自定义参数,geom_type为图层几何类型,可以根据实际需求进行修改。此外,创建图层方法还可以传入一个坐标系参数,例子中就不展示了。

# 创建面数据图层
polygonLayer = shpDataSource.CreateLayer("polygon",geom_type=ogr.wkbPolygon)

图层创建完成之后,便可以添加一些属性表结构。通过ogr.FieldDefn创建字段域,并通过图层方法CreateField创建字段,使用方法GetLayerDefn()获取要素域。

# 添加id字段
fieldDefn = ogr.FieldDefn("id",ogr.OFTInteger)
polygonLayer.CreateField(fieldDefn)

# 获取要素结构
featureDefn = polygonLayer.GetLayerDefn()

# 创建要素对象
polygonFeature = ogr.Feature(featureDefn)

创建几何对象。

ringGeometry = ogr.Geometry(ogr.wkbLinearRing)
geomeTypeName = ogr.GeometryTypeToName(ringGeometry.GetGeometryType())

for i,point in enumerate(points):
    # print(f"点:{point}")
    res = point.split(",")
    # print(f"点:{res}")

    ringGeometry.AddPoint(float(res[0]),float(res[1]))

# 关闭环状几何
# or ringGeometry.CloseRings()

# 创建面几何对象
polygonGeometry = ogr.Geometry(ogr.wkbPolygon)
polygonGeometry.AddGeometry(ringGeometry)

将几何对象和属性字段添加到要素中,之后使用图层方法CreateFeature创建图层要素。

polygonFeature.SetGeometry(polygonGeometry)
polygonFeature.SetField("id",1)

polygonLayer.CreateFeature(polygonFeature)

至此,大功告成。

ArcMap中打开成果显示如下。

小提示:

  1. 例子中创建的数据图层并未加上坐标信息,有兴趣的读者可以自行实现。
  2. 文中只实现了点、线、面对象,对于多点、多线和多面几何类型也留给感兴趣的读者自行实现!

对于GDAL中几何对象类型不太了解的同学可以参考文章:GDAL 数据类型大全

参考资料[1] 

GDAL 数据类型大全

OpenLayers示例数据下载,请在公众号后台回复:ol数据

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集

GDAL 实现自定义数据坐标系

GDAL 实现矢量数据读写

GDAL 数据类型大全

GDAL 实现 GIS 数据读取转换(全)

GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据

GIS 数据转换:使用 GDAL 将 GeoJSON 转换为 Shp 数据

GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据

使用命令行工具 ogr2ogr 将 CSV 转换为 Shp 数据(二)

GDAL 实现将 CSV 转换为 Shp 数据(一)

GamingonPhone:2025年《恋与深空》流水位列全球二游榜第二

2026年1月5日 20:10
36氪获悉,近日,海外游戏网站GamingonPhone公布了2025年全球二游收入排行榜前10,该榜数据统计来源于移动应用统计工具App Magic。榜单显示,2025年二次元手游中收入最高的游戏为《崩坏:星穹铁道》,叠纸游戏旗下超现实3D沉浸恋爱互动手游《恋与深空》则以3.6亿美元(约合人民币25亿元)位列第二,这也是国产乙女游戏首次登上这个榜单前列。

陕国投A:拟参与长安银行增资,增资金额不超过8亿元

2026年1月5日 20:08
36氪获悉,陕国投A公告,公司拟参与长安银行增资,增资金额不超过8亿元,增资股份不超过2.09亿股。由于赵忠琦先生在两家公司担任董事,本次交易构成关联交易。公司已与长安银行签署增资协议,审议该议案的董事会会议中,关联董事已回避表决。本次交易无需提交股东会审议,也不构成重大资产重组。

商米科技港股IPO及境内未上市股份“全流通”获中国证监会备案

2026年1月5日 20:03
36氪获悉,中国证监会国际合作司发布关于上海商米科技集团股份有限公司境外发行上市及境内未上市股份“全流通”备案通知书,公司拟发行不超过46,000,000股境外上市普通股并在香港联合交易所上市。公司13名股东拟将所持合计261,415,724股境内未上市股份转为境外上市股份,并在香港联合交易所上市流通。

石头科技港股IPO获中国证监会备案

2026年1月5日 20:00
36氪获悉,证监会国际合作司发布北京石头世纪科技股份有限公司境外发行上市备案通知书,公司拟发行不超过33,108,000股境外上市普通股并在香港联合交易所上市。

跨标签页数据同步完全指南:如何选择最优通信方案

作者 pauldu
2026年1月5日 19:57

五大跨标签页通信方案对比与最佳实践

深入对比 postMessage、MessageChannel、BroadcastChannel、sessionStorage、localStorage 五种跨窗口通信技术,帮助开发者做出正确的技术选择。

前言

本文详细分析五种跨浏览器标签页通信方案的优劣,通过实际场景分析和代码示例,最终给出决策指南。

快速导航

  • 🎯 想快速了解推荐方案?→ 跳到第 11 节
  • 📊 想看技术对比表?→ 查看附录
  • 💻 想看代码实现?→ 跳到第 6 节

一、问题场景(通用化描述)

背景

在现代 Web 应用中,经常需要在多个浏览器标签页之间进行实时数据同步。这种需求在许多场景下都很常见:

典型应用场景

  • 📋 用户在后台管理系统修改用户信息,同时其他标签页的用户列表需要实时刷新
  • 🔄 在线编辑工具中,一个标签页新增了内容,其他标签页需要同步更新
  • ✏️ 购物车在多个标签页打开,任意一个标签页修改商品数量,其他页面自动同步
  • 📊 仪表板中的多个图表来自不同标签页,需要保持数据一致
  • 🎮 多人协作应用中,一个用户在标签页 A 作出操作,标签页 B 需要实时感知

现象:数据孤岛问题

在没有跨页面通信机制的情况下:

  • ❌ 弹窗更新数据后,新标签页仍显示旧数据
  • ❌ 用户需要手动刷新才能看到最新内容
  • ❌ 多个工作窗口间信息不同步,影响工作效率
  • ❌ 容易造成数据一致性问题

需求定义

实现跨标签页的透明、实时数据同步机制。


二、五大方案快速对比

特性 postMessage MessageChannel BroadcastChannel sessionStorage localStorage
通信模式 父子单向 一对一双向 多端广播 事件监听 事件监听
需要窗口引用 ✅ 是 ✅ 是 ❌ 否 ❌ 否 ❌ 否
跨源支持 ✅ 是 ✅ 是 ❌ 同源 ❌ 同源 ❌ 同源
窗口刷新后 ❌ 断开 ❌ 断开 ✅ 有效 ✅ 保留 ✅ 保留
实时性 ✅ 立即 ✅ 立即 ✅ 立即 ⚠️ 延迟 ⚠️ 延迟
同页面通信 ✅ 可以 ✅ 可以 ✅ 可以 ❌ 不能 ❌ 不能
实现复杂度 中等 中等 中等
推荐指数 ⭐⭐ ⭐⭐⭐⭐⭐

推荐:BroadcastChannel ⭐⭐⭐⭐⭐(同源场景)


三、postMessage 方案

原理

postMessage 是最基础的跨窗口通信方式,通过向特定窗口发送消息实现通信。

代码示例

// 发送端(主窗口)
const newWindow = window.open(url, '_blank');
newWindow.postMessage({
  type: 'DATA_UPDATE',
  data: { id: 123 }
}, '*');

// 接收端(新窗口)
window.addEventListener('message', (event) => {
  if (event.origin !== window.location.origin) return;
  if (event.data.type === 'DATA_UPDATE') {
    console.log('收到数据:', event.data.data);
  }
});

局限性

问题 影响
需要窗口引用 主窗口刷新后引用丢失,无法恢复
新窗口刷新后无法通信 需手动重建连接
新窗口独立打开不支持 无法建立通信
管理复杂 多窗口时代码重复且难维护

适用场景

  • ✅ 跨源通信
  • ✅ 浏览器兼容性要求极高
  • ❌ 不适合:实时同步、多窗口、自动恢复

四、MessageChannel 方案

原理

MessageChannel 提供一对一的双向通信通道,通过传递 Port 对象建立通道。

代码示例

// 发送端(主窗口)
const newWindow = window.open(url, '_blank');
const { port1, port2 } = new MessageChannel();

// 将 port2 传给新窗口
newWindow.postMessage({ port: port2 }, '*', [port2]);

// 通过 port1 收发消息
port1.onmessage = (event) => {
  console.log('收到:', event.data);
};
port1.postMessage({ type: 'SYNC', data: {...} });

// 接收端(新窗口)
window.addEventListener('message', (event) => {
  if (event.ports.length) {
    const port = event.ports[0];
    port.onmessage = (msg) => {
      console.log('接收到:', msg.data);
    };
    port.start();
  }
});

局限性

问题 影响
需要管理端口引用 引用丢失导致通信断开
新窗口刷新需重连 需额外的重连逻辑
实现复杂 端口转移、start() 等机制复杂
不支持一对多 多窗口需建立多条通道

适用场景

  • ✅ 需要双向通信
  • ✅ 跨源通信
  • ✅ 通信频繁且可靠性要求高

五、BroadcastChannel 方案 ⭐ 推荐

原理

BroadcastChannel 提供一个命名通道,同一浏览上下文内所有同源的标签页都可以自动订阅该通道,实现真正的广播通信。

代码示例

// 发送端(任意窗口)
const channel = new BroadcastChannel('alarm_sync_channel');

channel.postMessage({
  alarmId: 123,
  deviceId: 456,
  devicePath: '/factory/workshop',
  deviceName: 'Device-A'
});

// 接收端(所有同源标签页自动接收)
const channel = new BroadcastChannel('alarm_sync_channel');

channel.onmessage = (event) => {
  const { alarmId, deviceId, devicePath, deviceName } = event.data;
  console.log('自动同步:', { alarmId, deviceId, devicePath, deviceName });
  updateDeviceData(deviceId);
};

// 组件卸载时关闭通道
onUnmounted(() => {
  channel.close();
});

核心优势

优势 说明
无需窗口引用 通过通道名称自动发现,完全解耦
自动恢复 窗口刷新后自动重新连接
独立打开支持 任何方式打开的同源窗口都自动加入
代码简洁 API 简单直观,代码量少 50%+
广播特性 一条消息所有监听者都接收
内存高效 无需管理端口生命周期

六、Storage 方案对比

sessionStorage 与 localStorage 的问题

这两种存储方案原本用于数据持久化,不是为了消息通信。用来实现跨页面通信存在根本性缺陷:

核心问题
问题 说明 影响
同页面无法触发事件 同一标签页修改无法通知自己 发送端和接收端必须分开
消息会被覆盖 快速发送多条消息时,后面的覆盖前面的 需要版本号/时间戳机制
需要轮询 无法实时感知更新,需定时检查 消耗 CPU,延迟大
消息易丢失 ⚠️ 如果接收端未及时检查,消息被覆盖就丢了 导致 10%+ 的消息丢失
存储污染 大量消息占满 5-10MB 限制 影响其他功能
序列化开销 频繁 JSON 转换 性能下降

深入分析:为什么会有消息丢失

场景模拟:购物车同步失败

想象你在两个浏览器标签页打开了购物应用:

标签页 A(购物车)标签页 B(商品详情) 都在监听 storage 事件。

问题发生过程

时间线          标签页 A(购物车)      标签页 B(商品详情)       Storage
────────────────────────────────────────────────────────────────────
T1  用户点击 +5 件商品
    ├─ sessionStorage.setItem('cart', {productId:1, qty:5})
    └─ storage 事件触发 ──────────→ ✅ 收到并更新 UI             ← storage 中: {id:1,qty:5}

T2  用户快速再 +3 件
    ├─ sessionStorage.setItem('cart', {productId:1, qty:8})
    └─ storage 事件触发 ──────────→ ⏳ 正在处理 (JS 执行中)      ← storage 中: {id:1,qty:8}
                                    (还没来得及读取)

T3  用户再次 +2 件(网络卡顿)
    ├─ sessionStorage.setItem('cart', {productId:1, qty:10})
    └─ storage 事件触发 ──────────→ 📢 新事件来了!             ← storage 中: {id:1,qty:10}
                                    ❌ 之前 T2 的 qty:8 丢失了
                                    
T4  标签页 B 事件处理完毕
    └─ 从 storage 读取: {id:1, qty:10}
    └─ 展示: 购物车中有 10 件商品
    └─ 问题: 漏掉了中间的 qty:8 更新!

核心原因

  1. Storage 设计是覆盖式的

    // sessionStorage 只能存一个值
    sessionStorage.setItem('cart', data);  // 新值覆盖旧值
    sessionStorage.getItem('cart');         // 始终只能取到最后一个
    
    // 中间的更新消息丢失了
    
  2. 事件触发滞后

    标签页 A 发送消息
       ↓ (网络/线程延迟)
    标签页 B 事件队列
       ↓ (等待 JS 执行时间)
    标签页 B 处理事件
       ↓
    此时已经错过了好几条消息
    
  3. 轮询方式无法捕获所有更新

    // 接收端每 500ms 检查一次
    setInterval(() => {
      const latest = sessionStorage.getItem('cart');
      // 问题:如果两次轮询之间发生了多条更新,只能看到最后一条
      // 所有中间的更新都丢失了
    }, 500);
    
数据丢失的具体场景

场景 1:高频更新导致的丢失

// 用户在 100ms 内快速修改购物车
for (let i = 0; i < 10; i++) {
  sessionStorage.setItem('cart_qty', i);  // 快速写入 0, 1, 2, 3..., 9
}

// 接收端可能只收到:
// - 事件 1:qty = 2
// - 事件 2:qty = 5 
// - 事件 3:qty = 9

// 丢失消息数:7 条(70% 丢失率)

场景 2:接收端处理延迟导致的丢失

// 发送端:快速发送 3 条消息
sessionStorage.setItem('msg', {id: 1, data: 'message 1'});  // 时间 T1
sessionStorage.setItem('msg', {id: 2, data: 'message 2'});  // 时间 T2
sessionStorage.setItem('msg', {id: 3, data: 'message 3'});  // 时间 T3

// 接收端的处理时间线:
// T1 时刻:storage 事件触发 → 开始处理第 1 条消息
// T2 时刻:收到新消息,但上一条还没处理完
// T2 + 50ms:处理完第 1 条,但 storage 值已经是第 3 条了
// 结果:消息 2 和消息 3 被合并,实际上只能读到消息 3

// 消息丢失数:2 条(66% 丢失率)
为什么 BroadcastChannel 不会丢失
// BroadcastChannel 有事件队列机制
const channel = new BroadcastChannel('sync');

// 即使接收端忙,消息也会被排队
channel.postMessage({id: 1});  // ✅ 队列中
channel.postMessage({id: 2});  // ✅ 队列中
channel.postMessage({id: 3});  // ✅ 队列中

channel.onmessage = (event) => {
  // 每条消息都会触发单独的事件
  console.log(event.data.id);  // 输出: 1, 2, 3 (无丢失)
};

对比总结

方案 消息处理方式 丢失率 原因
Storage 覆盖式存储 + 事件 10-70% 中间消息被覆盖,轮询也无法捕获
BroadcastChannel 事件队列 0% 每条消息都有独立事件,保证送达
postMessage 消息队列 0% 浏览器保证消息顺序和送达
MessageChannel 端口队列 0% 双向可靠通道
完整实现示例(仍然不推荐)
// ========== 消息队列包装类 ==========
class StorageMessageQueue {
  constructor(channelName = 'message_queue') {
    this.channelName = channelName;
    this.messageId = 0;
    this.listeners = [];
    this.setupListener();
  }

  setupListener() {
    window.addEventListener('storage', (event) => {
      if (event.key === this.channelName && event.newValue) {
        const message = JSON.parse(event.newValue);
        this.listeners.forEach(cb => cb(message));
        // 清理消息
        sessionStorage.removeItem(this.channelName);
      }
    });
  }

  send(data) {
    const message = {
      id: ++this.messageId,
      timestamp: Date.now(),
      data,
    };
    sessionStorage.setItem(this.channelName, JSON.stringify(message));
  }

  onMessage(callback) {
    this.listeners.push(callback);
  }

  close() {
    this.listeners = [];
  }
}

// ========== 使用方式 ==========
// 发送端
const queue = new StorageMessageQueue('alarm_sync');
function nextAlarm(row) {
  queue.send({
    alarmId: row.id,
    deviceId: row.deviceId,
  });
}

// 接收端
const queue = new StorageMessageQueue('alarm_sync');
queue.onMessage((message) => {
  updateDevice(message.data.deviceId);
});

问题

  • ❌ 代码量大 3 倍以上
  • ❌ 需要手动清理消息
  • ❌ 仍有消息丢失风险
  • ❌ 性能低于 BroadcastChannel
  • ❌ 难以调试和维护

七、五种方案深度场景对比

场景 1:主窗口刷新后的通信恢复

用户场景:主窗口处理报警,新窗口打开诊断页面。此时主窗口意外刷新。

postMessage 方案
// ❌ 主窗口刷新后,newWindow 引用丢失
const newWindow = window.open(url);
// 页面刷新...
// newWindow 变量被重置,无法继续通信
MessageChannel 方案
// ⚠️ 需要重新建立连接
// 主窗口刷新后,原有 port1 失效
// 需要额外的重连机制,增加复杂度
BroadcastChannel 方案 ⭐
// ✅ 自动恢复
const channel = new BroadcastChannel('alarm_sync_channel');
// 页面刷新...
// 自动重新连接到通道
channel.postMessage(data);  // 可以继续使用
sessionStorage 方案
// ⚠️ 数据保留,但需轮询
sessionStorage.setItem('alarm_sync', JSON.stringify(data));
// 页面刷新...
// 数据仍在,但接收端需轮询检测版本号
let lastId = 0;
setInterval(() => {
  const msg = JSON.parse(sessionStorage.getItem('alarm_sync'));
  if (msg?.id > lastId) {
    lastId = msg.id;
    updateDevice(msg.deviceId);
  }
}, 500);  // 延迟 500ms 才能感知

场景 2:新窗口独立打开

用户场景:用户通过直接在地址栏打开新标签页,访问诊断页面。

postMessage 方案
// ❌ 无法建立通信
// 主窗口没有对新窗口的引用
// 新页面无法与主窗口通信
MessageChannel 方案
// ❌ 无法直接通信
// 新窗口不知道要连接到哪个通道
BroadcastChannel 方案 ⭐
// ✅ 自动连接
const channel = new BroadcastChannel('alarm_sync_channel');
channel.onmessage = (event) => {
  // 自动接收其他标签页的消息,无需任何额外配置
};
sessionStorage 方案
// ⚠️ 可见数据,但无法感知更新
const data = JSON.parse(sessionStorage.getItem('alarm_sync'));
// 问题:新窗口打开后,无法知道 "有新消息来了"
// 需要定时轮询或使用其他机制通知

场景 3:多窗口同步

用户场景:用户同时打开了 3 个诊断页面,需要它们共享同一个设备的数据更新。

postMessage 方案
// ❌ 无法实现优雅
const window1 = window.open(url1);
const window2 = window.open(url2);
const window3 = window.open(url3);

// 发送消息时需要逐一发送
window1.postMessage(data, '*');
window2.postMessage(data, '*');
window3.postMessage(data, '*');
// 代码重复,难以维护
MessageChannel 方案
// ⚠️ 可以但复杂
// 需要为每个窗口建立单独的 MessageChannel
const { port1: port1_w1, port2: port2_w1 } = new MessageChannel();
const { port1: port1_w2, port2: port2_w2 } = new MessageChannel();
const { port1: port1_w3, port2: port2_w3 } = new MessageChannel();

// 分别初始化每个连接
window1.postMessage({ port: port2_w1 }, '*', [port2_w1]);
window2.postMessage({ port: port2_w2 }, '*', [port2_w2]);
window3.postMessage({ port: port2_w3 }, '*', [port2_w3]);

// 发送消息时仍需逐一发送
port1_w1.postMessage(data);
port1_w2.postMessage(data);
port1_w3.postMessage(data);
BroadcastChannel 方案 ⭐
// ✅ 完美支持
const channel = new BroadcastChannel('alarm_sync_channel');
// 所有 3 个诊断页面都自动监听同一通道
// 发送一条消息,所有监听者都接收
channel.postMessage(data);
// 简洁、高效、完全自动化
sessionStorage 方案
// ⚠️ 可以但需复杂逻辑
sessionStorage.setItem('alarm_sync_v2', JSON.stringify({
  version: Date.now(),
  data: { deviceId: 456 }
}));

// 3 个窗口都需要定时轮询
let lastVersion = 0;
setInterval(() => {
  const stored = JSON.parse(sessionStorage.getItem('alarm_sync_v2') || '{}');
  if (stored.version && stored.version > lastVersion) {
    lastVersion = stored.version;
    updateDevice(stored.data.deviceId);
  }
}, 500);

// 问题:3 个窗口都在轮询,消耗 CPU
// 有消息丢失风险(版本号被覆盖)

八、实际项目实现

项目背景:购物应用跨标签页同步

用户在购物应用中,同时打开了两个标签页:

  • 标签页 A:购物车页面(cart.vue)
  • 标签页 B:商品详情页面(productDetail.vue)

需求

  • 在标签页 A 修改商品数量,标签页 B 自动更新对应商品信息
  • 在标签页 B 加入购物车,标签页 A 的购物车数据实时刷新
  • 页面刷新后仍能保持数据同步

发送端实现 (cart.vue)

<script setup>
import { onMounted, onUnmounted } from 'vue';

// 创建广播通道
const syncChannel = new BroadcastChannel('shopping_sync_channel');

onMounted(() => {
  getCartList();
});

onUnmounted(() => {
  // 组件卸载时关闭通道,防止内存泄漏
  syncChannel.close();
});

// 获取购物车列表
async function getCartList() {
  const res = await fetchCartItems();
  state.cartItems = res.data || [];
}

// 用户修改购物车中商品的数量
function updateItemQuantity(item, newQuantity) {
  // 1. 更新本地数据
  item.quantity = newQuantity;
  updateCart(item);
  
  // 2. ✨ 发送同步消息给其他标签页
  syncChannel.postMessage({
    type: 'CART_UPDATED',
    productId: item.productId,
    productName: item.productName,
    quantity: newQuantity,
    price: item.price,
    timestamp: Date.now(),
  });
}

// 用户清空购物车
function clearCart() {
  state.cartItems = [];
  clearCartAPI();
  
  // ✨ 通知其他标签页购物车已清空
  syncChannel.postMessage({
    type: 'CART_CLEARED',
    timestamp: Date.now(),
  });
}
</script>

接收端实现 (productDetail.vue)

<script setup>
import { onMounted, onUnmounted } from 'vue';

const router = useRouter();
const routes = useRoute();

// 创建同名广播通道
const syncChannel = new BroadcastChannel('shopping_sync_channel');

onMounted(() => {
  // 初始加载商品详情
  if (routes.query.productId) {
    loadProductDetail(routes.query.productId);
  }
  
  // ✨ 监听购物车同步消息
  syncChannel.onmessage = (event) => {
    const { type, productId, quantity, timestamp } = event.data;
    
    if (type === 'CART_UPDATED') {
      // 1. 更新 URL 查询参数(确保刷新后数据一致)
      router.replace({
        path: routes.path,
        query: {
          ...routes.query,
          productId,
          lastUpdate: timestamp,
        },
      });
      
      // 2. 重新加载商品数据
      loadProductDetail(productId);
      
      // 3. 显示同步提示
      proxy.$message.info(`购物车已更新:${productId} 的数量变为 ${quantity}`);
    } 
    else if (type === 'CART_CLEARED') {
      // 购物车被清空,更新UI显示
      updateCartStatus('empty');
      proxy.$message.warning('购物车已在其他标签页被清空');
    }
  };
});

onUnmounted(() => {
  // 关闭通道,防止内存泄漏
  syncChannel.close();
});

// 加载商品详情
const loadProductDetail = (productId) => {
  fetchProductDetail(productId).then((res) => {
    state.product = res.data;
    state.productId = productId;
  });
};

// 更新购物车状态显示
const updateCartStatus = (status) => {
  state.cartStatus = status;
};
</script>

核心流程

┌─────────────────────────────────────┐
│   cart.vue (标签页 A)               │
│ 用户修改购物车商品数量             │
│         ↓                           │
│ updateItemQuantity(item) 更新本地  │
│         ↓                           │
│ 🛒 syncChannel.postMessage()       │
│    发送 {productId, quantity, ...} │
└────────────┬──────────────────────┘
             │ BroadcastChannel
             ↓
┌─────────────────────────────────────┐
│ productDetail.vue (标签页 B)        │
│                                     │
│ syncChannel.onmessage 触发          │
│         ↓                           │
│ 📍 router.replace() 更新 URL       │
│         ↓                           │
│ 🔄 loadProductDetail() 重新加载    │
│         ↓                           │
│ ✅ 两个标签页购物数据完全同步      │
└─────────────────────────────────────┘

九、关键设计点

1. 通道名称管理

// ✅ 推荐:使用明确的命名规范
const CHANNEL_NAMES = {
  SHOPPING_SYNC: 'shopping_sync_channel',
  USER_PROFILE: 'user_profile_sync',
  NOTIFICATION_UPDATE: 'notification_update',
  DATA_DASHBOARD: 'data_dashboard_sync',
};

const channel = new BroadcastChannel(CHANNEL_NAMES.SHOPPING_SYNC);

2. 生命周期管理

onMounted(() => {
  channel = new BroadcastChannel('sync_channel');
  channel.onmessage = handleMessage;
});

onUnmounted(() => {
  // ✅ 必须关闭,否则会泄漏内存
  channel.close();
  channel = null;
});

3. URL 同步机制

// ✅ 使用 router.replace() 而非 push()
// 这样刷新后能恢复到正确的状态
router.replace({
  path: routes.path,
  query: {
    ...routes.query,
    deviceId,
  },
});

4. 消息体设计

// ✅ 只传递必要数据,减少序列化开销
syncChannel.postMessage({
  type: 'CART_UPDATED',
  productId: item.productId,
  quantity: newQuantity,
  price: item.price,
  timestamp: Date.now(),
});

// ❌ 避免:传递整个商品对象(包含无关信息)
// syncChannel.postMessage(item);

5. 安全考虑

// BroadcastChannel 仅支持同源通信
// 浏览器自动规避安全隐患,无需手动检查
// 不同源的页面无法访问该通道

// 如果需要跨源,使用 postMessage:
if (event.origin !== window.location.origin) return;

十、浏览器兼容性

浏览器 postMessage MessageChannel BroadcastChannel Storage
Chrome ✅ 全版本 ✅ 全版本 ✅ 54+ ✅ 全版本
Firefox ✅ 全版本 ✅ 全版本 ✅ 38+ ✅ 全版本
Safari ✅ 全版本 ✅ 全版本 ✅ 15.1+ ✅ 全版本
Edge ✅ 全版本 ✅ 全版本 ✅ 79+ ✅ 全版本
IE ✅ 11+ ✅ 11+ ❌ 不支持 ✅ 8+

处理兼容性

// 检测 BroadcastChannel 支持
if (typeof BroadcastChannel !== 'undefined') {
  // 使用 BroadcastChannel
  const channel = new BroadcastChannel('sync');
} else {
  // 降级方案:使用 postMessage 或 localStorage
  console.warn('浏览器不支持 BroadcastChannel,使用降级方案');
}

十一、最终决策指南

11.1 选择标准与决策树

需要跨标签页通信吗?
  ├─ 不需要 → 使用本地 Vue 状态管理
  └─ 需要
      ├─ 需要跨源吗?
      │   ├─ 是 → 使用 postMessage 或 MessageChannel
      │   └─ 否
      │       ├─ 需要实时通信吗?
      │       │   ├─ 是 → 🎯 使用 BroadcastChannel
      │       │   └─ 否
      │       │       ├─ 需要数据持久化吗?
      │       │       │   ├─ 是 → localStorage + BroadcastChannel
      │       │       │   └─ 否 → sessionStorage + 轮询(不推荐)

11.2 推荐方案汇总

场景 推荐方案 理由
实时同步(同源) 🎯 BroadcastChannel 简洁、高效、无需轮询
数据持久化+实时同步 localStorage + BroadcastChannel 数据持久 + 实时通知
跨源通信 postMessage 唯一支持跨源的方案
点对点可靠通信 MessageChannel 双向可靠通道
浏览器兼容性最高 postMessage 最广泛的浏览器支持
不推荐 ❌ Storage 作消息队列 需轮询、易丢失、难维护

11.3 最佳实践清单

  1. 优先选择 BroadcastChannel(在同源场景下)
  2. 使用明确的通道命名(避免冲突)
  3. 总是在组件卸载时关闭通道(防止内存泄漏)
  4. 使用 router.replace 同步 URL(确保刷新后状态一致)
  5. 只传递必要的数据(减少序列化开销)
  6. 提供降级方案(考虑旧浏览器兼容性)
  7. 不要用 Storage 作消息队列(这是对 API 的误用)

十二、常见问题 FAQ

Q1: 为什么 BroadcastChannel 收不到消息?

A: 检查以下几点:

  1. ✅ 通道名称是否一致
  2. ✅ 是否为同源页面(协议、域名、端口都要相同)
  3. ✅ 是否调用了 channel.close()
  4. ✅ 消息是否发送在监听器创建之后

Q2: 为什么不用 Storage 作消息队列?

A: 因为 Storage 的设计本就不是为了消息通信:

  • ❌ 同一标签页修改无法触发事件
  • ❌ 消息会被后续消息覆盖
  • ❌ 需要轮询,延迟大
  • ❌ 需要版本号/时间戳等复杂机制
  • ❌ 消息可靠性低

这就是为什么 BroadcastChannel 被设计出来的原因。

Q3: localStorage 可以用于跨标签页通信吗?

A: 可以,但不推荐作为主要通信机制:

  • ⚠️ 需要轮询或定时检查
  • ⚠️ 延迟大(通常 100ms+)
  • ⚠️ 消息易丢失(被新消息覆盖)

适用场景

  • 需要数据持久化时,配合 BroadcastChannel 使用
  • 离线数据同步时
  • 跨会话数据恢复时

Q4: 如何实现跨源通信?

A: 使用 postMessage 或 MessageChannel,但要注意安全性:

// postMessage 跨源通信
newWindow.postMessage(data, 'https://trusted-domain.com');

// 接收端必须验证来源
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted-domain.com') return;
  // 安全处理消息
});

Q5: BroadcastChannel 中的错误如何处理?

A: 监听 messageerror 事件:

channel.onmessageerror = (event) => {
  console.error('消息解析错误:', event);
};

// 或使用 addEventListener
channel.addEventListener('messageerror', (event) => {
  console.error('消息错误:', event);
});

十三、应用场景拓展

BroadcastChannel 的应用远不止跨标签页数据同步,还包括:

场景 1:电商库存实时同步

// 多个门店系统标签页同时打开
const inventoryChannel = new BroadcastChannel('inventory_sync');

// 门店 A 在标签页 1 扫描商品入库
inventoryChannel.postMessage({
  type: 'STOCK_IN',
  productId: 'SKU-123',
  quantity: 50,
  location: 'warehouse-1',
  timestamp: Date.now(),
});

// 门店 B 在标签页 2 自动收到更新
inventoryChannel.onmessage = (event) => {
  if (event.data.type === 'STOCK_IN') {
    updateInventory(event.data);
    showNotification(`库存已更新:${event.data.quantity}件`);
  }
};

场景 2:订单状态实时同步

const orderChannel = new BroadcastChannel('order_sync');

// 后台管理页面在标签页 1 更新订单状态
orderChannel.postMessage({
  orderId: 'ORD-2024-001',
  status: 'shipped',
  trackingNo: 'TRK123456',
  updatedAt: Date.now(),
});

// 客服页面在标签页 2 自动获取最新状态
orderChannel.onmessage = (event) => {
  updateOrderUI(event.data);
  notifyCustomer(`订单 ${event.data.orderId}${event.data.status}`);
};

场景 3:用户账户设置实时同步

const userSettingsChannel = new BroadcastChannel('user_settings_sync');

// 用户在账户设置页面(标签页 1)修改主题
userSettingsChannel.postMessage({
  type: 'THEME_CHANGED',
  theme: 'dark',
  userId: '12345',
});

// 所有打开的应用页面(标签页 2、3、4...)自动同步
userSettingsChannel.onmessage = (event) => {
  if (event.data.type === 'THEME_CHANGED') {
    applyTheme(event.data.theme);
    showMessage('主题已切换为深色模式');
  }
};

场景 4:实时数据仪表板

const dashboardChannel = new BroadcastChannel('analytics_dashboard');

// 数据分析工具在后台收集数据(标签页 1)
dashboardChannel.postMessage({
  metric: 'daily_sales',
  value: 15000,
  region: 'east',
  timestamp: Date.now(),
});

// 多个仪表板视图(标签页 2、3、4...)自动刷新
dashboardChannel.onmessage = (event) => {
  updateChart(event.data);
  updateDataCard(event.data);
  playNotificationSound(); // 新数据到达时提醒
};

结语

BroadcastChannel 代表了现代 Web 应用通信的最佳实践——简洁、高效、易维护

通过充分理解五种跨窗口通信方案的优劣,我们能够在实际项目中做出最合理的技术选择。正确的通信机制是系统可靠性的基石。

关键要点

  1. 🎯 同源场景下,BroadcastChannel 是首选
  2. 📊 Storage 用于持久化,不用于消息通信
  3. ✅ 总是记得关闭通道,防止内存泄漏
  4. 🔄 URL 同步确保刷新后状态一致
  5. 🚀 简单场景也能做到优雅实现

附录:技术对比速查表

五种方案对比一览表

维度 postMessage MessageChannel BroadcastChannel sessionStorage localStorage
代码行数 20-30 40-50 10-15 30-40 30-40
学习难度
实时性 ms 级 ms 级 ms 级 秒级+ 秒级+
可靠性
内存效率
消息丢失率 0% 0% 0% 10%+ 10%+
性能评分 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
综合评分 ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐

实现代码对比

BroadcastChannel(3 行)

const ch = new BroadcastChannel('sync');
ch.postMessage(data);
ch.onmessage = (e) => handle(e.data);

sessionStorage(15+ 行)

const queue = new StorageMessageQueue('sync');
queue.send(data);
queue.onMessage(handle);

postMessage(25+ 行)

const win = window.open(url);
win.postMessage(data, '*');
window.addEventListener('message', (e) => handle(e.data));

参考资源

德适生物港股IPO获中国证监会备案

2026年1月5日 19:51
36氪获悉,证监会国际合作司发布杭州德适生物科技股份有限公司境外发行上市及境内未上市股份“全流通”备案通知书,公司拟发行不超过31,004,000股境外上市普通股并在香港联合交易所上市。

驭势科技港股IPO获中国证监会备案

2026年1月5日 19:50
36氪获悉,证监会国际合作司发布驭势科技(北京)股份有限公司境外发行上市及境内未上市股份“全流通”备案通知书,公司拟发行不超过18,914,150股境外上市普通股并在香港联合交易所上市。

海致科技港股IPO获中国证监会备案

2026年1月5日 19:50
36氪获悉,证监会国际合作司发布北京海致科技集团股份有限公司境外发行上市及境内未上市股份“全流通”备案通知书,公司拟发行不超过47,584,600股境外上市普通股并在香港联合交易所上市。

豪鹏科技:拟定增募资不超8亿元

2026年1月5日 19:42
36氪获悉,豪鹏科技公告,公司拟向特定对象发行股票募集资金不超过8亿元,扣除发行费用后的募集资��净额将用于储能电池建设项目和钢壳叠片锂电池建设项目。
❌
❌