Flutter Web如何实现由主窗口控制的副窗口?
Flutter Web如何实现由主窗口控制的副窗口?
这个赛事计分板的场景太具体了!我之前帮本地拳馆做过类似的Flutter Web系统,刚好能给你分享实操经验——完全可以实现,而且有两种靠谱的路子,我给你一步步说:
方案一:主窗口主动打开副窗口 + PostMessage 实时通信
这是最贴合你场景的方案,主窗口负责打开显示端,然后通过浏览器的postMessage API同步数据,逻辑直接好维护。
步骤1:主窗口打开副窗口并保存引用
首先在主窗口的操作页面里,用dart:js调用浏览器的window.open方法打开副窗口,要记得保存窗口引用,方便后续发消息:
import 'dart:js' as js; // 保存副窗口的引用,全局或在State里维护 js.JsObject? scoreDisplayWindow; // 点击按钮时触发打开副窗口 void openScoreDisplay() { scoreDisplayWindow = js.context['window'].callMethod('open', [ '/score-board', // 这里对应你Flutter路由里的副窗口页面路径 '_blank', // 配置副窗口的参数,比如去掉菜单栏工具栏,适配外接显示器 'width=1920,height=1080,menubar=no,toolbar=no,location=no' ]); }
步骤2:副窗口监听消息并更新UI
在副窗口的页面(比如ScoreBoardPage)里,用dart:html监听message事件,接收主窗口发来的计分、罚分数据:
import 'dart:html' as html; import 'package:flutter/material.dart'; class ScoreBoardPage extends StatefulWidget { const ScoreBoardPage({super.key}); @override State<ScoreBoardPage> createState() => _ScoreBoardPageState(); } class _ScoreBoardPageState extends State<ScoreBoardPage> { int _leftScore = 0; int _rightScore = 0; int _leftPenalties = 0; int _rightPenalties = 0; @override void initState() { super.initState(); // 监听主窗口发来的消息 html.window.addEventListener('message', _handleMessage); } void _handleMessage(html.Event event) { final message = (event as html.MessageEvent).data; // 校验消息类型,避免处理无关消息 if (message['type'] == 'updateScoreData') { setState(() { _leftScore = message['leftScore']; _rightScore = message['rightScore']; _leftPenalties = message['leftPenalties']; _rightPenalties = message['rightPenalties']; }); } } @override void dispose() { // 页面销毁时移除监听,避免内存泄漏 html.window.removeEventListener('message', _handleMessage); super.dispose(); } @override Widget build(BuildContext context) { // 这里写你的计分板UI,用上面的_state变量渲染 return Scaffold( body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( children: [ Text('红方得分: $_leftScore', style: const TextStyle(fontSize: 48)), Text('罚分: $_leftPenalties', style: const TextStyle(fontSize: 24, color: Colors.red)), ], ), Column( children: [ Text('蓝方得分: $_rightScore', style: const TextStyle(fontSize: 48)), Text('罚分: $_rightPenalties', style: const TextStyle(fontSize: 24, color: Colors.blue)), ], ), ], ), ), ); } }
步骤3:主窗口操作时发送更新
当主窗口里调整计分、罚分后,同步给副窗口:
// 比如主窗口里的加分按钮点击事件 void incrementLeftScore() { setState(() { _leftScore++; }); // 给副窗口发消息同步数据 _sendScoreUpdateToDisplay(); } // 封装发送消息的方法 void _sendScoreUpdateToDisplay() { if (scoreDisplayWindow == null) return; scoreDisplayWindow!.callMethod('postMessage', [ { 'type': 'updateScoreData', 'leftScore': _leftScore, 'rightScore': _rightScore, 'leftPenalties': _leftPenalties, 'rightPenalties': _rightPenalties, }, // 这里尽量用具体的origin,比如window.location.origin,比*更安全 html.window.location.origin ]); }
方案二:Broadcast Channel 多窗口广播通信
如果你的场景可能存在副窗口被手动打开(不是主窗口触发),或者需要支持多个显示端,那用浏览器的Broadcast Channel更合适——它相当于一个全局的消息频道,所有同域的窗口都能订阅和发送消息,不需要维护窗口引用。
主窗口和副窗口共用的通信逻辑
不管是主窗口还是副窗口,都可以通过同一个频道收发消息:
import 'dart:html' as html; import 'dart:convert'; // 定义唯一的频道名称,所有窗口用这个频道 final scoreBoardChannel = html.BroadcastChannel('combat-scoreboard-channel'); // 主窗口发送更新 void sendScoreUpdate(Map<String, dynamic> scoreData) { scoreBoardChannel.postMessage({ 'type': 'updateScore', ...scoreData, }); // 同时存一份到localStorage,方便副窗口刷新后恢复状态 html.window.localStorage['scoreData'] = jsonEncode(scoreData); } // 副窗口监听更新 void listenToScoreUpdates(Function(Map<String, dynamic>) onUpdate) { scoreBoardChannel.onMessage.listen((event) { final data = event.data; if (data['type'] == 'updateScore') { onUpdate(data); } }); } // 页面销毁时记得关闭频道,避免内存泄漏 void closeScoreBoardChannel() { scoreBoardChannel.close(); }
主窗口里调用发送
比如在主窗口调整罚分后:
void addLeftPenalty() { setState(() { _leftPenalties++; }); sendScoreUpdate({ 'leftScore': _leftScore, 'rightScore': _rightScore, 'leftPenalties': _leftPenalties, 'rightPenalties': _rightPenalties, }); }
副窗口里监听更新
在副窗口的initState里订阅频道,同时拉取localStorage的初始状态:
@override void initState() { super.initState(); // 从localStorage拉取初始数据,避免刷新后清空 final savedData = html.window.localStorage['scoreData']; if (savedData != null) { final data = jsonDecode(savedData) as Map<String, dynamic>; setState(() { _leftScore = data['leftScore']; _rightScore = data['rightScore']; _leftPenalties = data['leftPenalties']; _rightPenalties = data['rightPenalties']; }); } // 监听实时更新 listenToScoreUpdates((data) { setState(() { _leftScore = data['leftScore']; _rightScore = data['rightScore']; _leftPenalties = data['leftPenalties']; _rightPenalties = data['rightPenalties']; }); }); } @override void dispose() { closeScoreBoardChannel(); super.dispose(); }
避坑提醒
- 同域限制:所有窗口必须在同一个域名下,否则浏览器会阻止跨窗口通信,所以部署Flutter Web时要确保主副窗口的页面都在同一个域名/端口下。
- 页面刷新恢复:副窗口刷新后会丢失状态,搭配
localStorage存一份最新数据,初始化时读取就能解决这个问题。 - 安全问题:
postMessage尽量不要用*作为目标origin,指定具体的域名(比如html.window.location.origin),防止恶意页面窃取数据。
总结
如果是主窗口主动打开显示端的场景,方案一的postMessage足够简单直接;如果需要更灵活的多窗口通信,方案二的Broadcast Channel更省心。这两种都是Flutter Web直接调用浏览器原生API实现的,不需要额外依赖包,我之前做的系统用方案一稳定跑了大半年的赛事,完全没问题~




