You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Google Apps Script Web App多用户实时同步比分板实现问询

解决多用户实时同步比分的方案

Hey there! The core problem here is that your current code only updates the score locally in each user's browser—there's no shared state or real-time communication between clients. To fix this, we need a centralized backend that syncs score changes to all connected users using WebSocket technology (since it enables bidirectional, real-time communication).

Below's a step-by-step implementation using Node.js + Socket.io (a popular WebSocket library) because it's easy to set up and integrates smoothly with your existing frontend code.


Step 1: Set Up the Real-Time Backend

First, create a simple Node.js server to manage the shared score state and broadcast updates to all clients.

  1. Initialize a new project and install dependencies:
mkdir score-sync-server && cd score-sync-server
npm init -y
npm install express socket.io
  1. Create a server.js file with this code:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: "*", // Replace with your frontend URL in production (e.g., "https://your-app.com")
    methods: ["GET", "POST"]
  }
});

// Shared state for scores (single source of truth)
let currentScores = {
  team1: 0,
  team2: 0
};

// Handle new client connections
io.on('connection', (socket) => {
  console.log('A user connected');
  
  // Send current scores to the new client immediately
  socket.emit('score-update', currentScores);

  // Listen for score change requests from clients
  socket.on('change-score', (action) => {
    // Update shared state with validation
    switch(action) {
      case 'a1':
        currentScores.team1++;
        break;
      case 's1':
        if(currentScores.team1 > 0) currentScores.team1--;
        break;
      case 'a2':
        currentScores.team2++;
        break;
      case 's2':
        if(currentScores.team2 > 0) currentScores.team2--;
        break;
      case 'reset':
        currentScores.team1 = 0;
        currentScores.team2 = 0;
        break;
    }

    // Broadcast updated scores to ALL connected clients
    io.emit('score-update', currentScores);
  });

  socket.on('disconnect', () => {
    console.log('User disconnected');
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Step 2: Modify Your Frontend Code

Update your HTML/JS to connect to the Socket.io server, listen for score updates, and send actions instead of modifying the DOM directly. Here's the revised code (key changes marked with comments):

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <!--Import Google Icon Font-->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <!--Import materialize.css-->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
  <!--Let browser know website is optimized for mobile-->
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <?!= include("8-general-css"); ?>
  <?!= include("8-landscape-css"); ?>
  <?!= include("8-portrait-css"); ?>
  <!-- Add Socket.io client library -->
  <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
</head>
<body>
  <div class="grid-container">
    <!-- Your existing HTML structure stays the same -->
    <div class="team-1-container">
      <label class="lbl-team blk-text font-header" id="lbl-team-1">Team 1</label>
    </div>
    <div class="player-1-container">
      <select class="browser-default player-select font-opt" id="player-opt-1">
        <option value="" disabled selected>Player 1</option>
        <?!= players;?>
      </select>
    </div>
    <div class="stats-1-container">
      <div class="lbl-elo-container">
        <label class="lbl-elo blk-text font-stats" id="lbl-elo-1">ELO: </label>
        <label class="lbl-elo-val blk-text font-stats" id="lbl-elo-val-1">-</label>
      </div>
      <div class="lbl-win-container">
        <label class="lbl-win blk-text font-stats" id="lbl-win-1">Win Prob: </label>
        <label class="lbl-win-val blk-text font-stats" id="lbl-win-val-1">-%</label>
      </div>
    </div>
    <div class="player-3-container">
      <select class="browser-default player-select font-opt" id="player-opt-3">
        <option value="">Player 3</option>
        <?!= players;?>
      </select>
    </div>
    <div class="sub-1-container">
      <button class="btn-sub font-btn" id="btn-sub-1"><span>-</span></button>
    </div>
    <div class="score-1-container">
      <label class="lbl-score blk-text font-score" id="lbl-score-1">0</label>
    </div>
    <div class="add-1-container">
      <button class="btn-add font-btn" id="btn-add-1">+</button>
    </div>
    <div class="reset-container">
      <button class="btn-reset-submit font-reset-submit" id="btn-reset">Reset</button>
    </div>
    <div class="submit-container">
      <button class="btn-reset-submit font-reset-submit" id="btn-submit">Submit</button>
    </div>
    <div class="sub-2-container">
      <button class="btn-sub font-btn" id="btn-sub-2">-</button>
    </div>
    <div class="score-2-container">
      <label class="lbl-score blk-text font-score" id="lbl-score-2">0</label>
    </div>
    <div class="add-2-container">
      <button class="btn-add font-btn" id="btn-add-2">+</button>
    </div>
    <div class="player-2-container">
      <select class="browser-default player-select font-opt" id="player-opt-2">
        <option value="" disabled selected>Player 2</option>
        <?!= players;?>
      </select>
    </div>
    <div class="stats-2-container">
      <div class="lbl-elo-container">
        <label class="lbl-elo blk-text font-stats" id="lbl-elo-2">ELO: </label>
        <label class="lbl-elo-val blk-text font-stats" id="lbl-elo-val-2">-</label>
      </div>
      <div class="lbl-win-container">
        <label class="lbl-win blk-text font-stats" id="lbl-win-2">Win Prob: </label>
        <label class="lbl-win-val blk-text font-stats" id="lbl-win-val-2">-%</label>
      </div>
    </div>
    <div class="player-4-container">
      <select class="browser-default player-select font-opt" id="player-opt-4">
        <option value="">Player 4</option>
        <?!= players;?>
      </select>
    </div>
    <div class="team-2-container">
      <label class="lbl-team blk-text font-header" id="lbl-team-2">Team 2</label>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
  <?!= include("8-ball-js"); ?>
  <?!= include("8-ball-swipe-score"); ?>

  <script>
    // Connect to the Socket.io server (replace with your production server URL)
    const socket = io('http://localhost:3000');

    // Update UI whenever we receive score updates from the server
    socket.on('score-update', (scores) => {
      document.getElementById("lbl-score-1").innerHTML = scores.team1;
      document.getElementById("lbl-score-2").innerHTML = scores.team2;
    });

    document.addEventListener('DOMContentLoaded', function() {
      // Button clicks now send actions to the server instead of modifying local state
      document.getElementById("btn-sub-1").addEventListener("click", function() {
        socket.emit('change-score', 's1');
      }, false);
      document.getElementById("btn-add-1").addEventListener("click", function() {
        socket.emit('change-score', 'a1');
      }, false);
      document.getElementById("btn-sub-2").addEventListener("click", function() {
        socket.emit('change-score', 's2');
      }, false);
      document.getElementById("btn-add-2").addEventListener("click", function() {
        socket.emit('change-score', 'a2');
      }, false);
      // Add reset button handler to sync reset across all clients
      document.getElementById("btn-reset").addEventListener("click", function() {
        socket.emit('change-score', 'reset');
      }, false);
    });
  </script>
</body>
</html>

Key Changes Explained

  1. Single Source of Truth: The server holds the official score state, so all clients get consistent data.
  2. Real-Time Sync:
    • Frontend connects to the server and listens for score-update events to refresh the UI automatically.
    • Button clicks send lightweight change-score events to the server instead of modifying the DOM directly.
  3. Validation: The server handles score rules (like preventing negative scores) to ensure no client can break consistency.

How to Test It

  1. Run the backend server: node server.js
  2. Open your frontend in multiple browsers/tabs—any score change or reset will instantly sync across all instances!

For production, make sure to:

  • Restrict the cors origin in the server to your actual frontend URL (avoid using * in production).
  • Deploy the server to a hosting platform like Heroku, AWS, or DigitalOcean.
  • Add database persistence if you want scores to save between server restarts.

内容的提问来源于stack exchange,提问作者Cody Baker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 17:07:35