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.
- Initialize a new project and install dependencies:
mkdir score-sync-server && cd score-sync-server npm init -y npm install express socket.io
- Create a
server.jsfile 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
- Single Source of Truth: The server holds the official score state, so all clients get consistent data.
- Real-Time Sync:
- Frontend connects to the server and listens for
score-updateevents to refresh the UI automatically. - Button clicks send lightweight
change-scoreevents to the server instead of modifying the DOM directly.
- Frontend connects to the server and listens for
- Validation: The server handles score rules (like preventing negative scores) to ensure no client can break consistency.
How to Test It
- Run the backend server:
node server.js - 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
corsorigin 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

