You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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实现的,不需要额外依赖包,我之前做的系统用方案一稳定跑了大半年的赛事,完全没问题~

火山引擎 最新活动