如何用PHP项目实现实时看板?

wen java案例 2

本文目录导读:

如何用PHP项目实现实时看板?

  1. 技术架构选择
  2. 前端实现
  3. 数据库设计
  4. 自动化部署脚本
  5. 优化建议
  6. 完整示例项目结构

我来详细介绍几种用PHP实现实时看板的方案:

技术架构选择

WebSocket + PHP (推荐)

// 使用 Ratchet 实现 WebSocket 服务
// composer require cboden/ratchet
// server.php - WebSocket 服务端
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Board;
require 'vendor/autoload.php';
class Board implements MessageComponentInterface {
    protected $clients;
    public function __construct() {
        $this->clients = new \SplObjectStorage;
        echo "看板服务启动\n";
    }
    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        echo "新连接: {$conn->resourceId}\n";
    }
    public function onMessage(ConnectionInterface $from, $msg) {
        $data = json_decode($msg, true);
        // 更新看板数据
        $this->updateBoard($data);
        // 广播给所有客户端
        foreach ($this->clients as $client) {
            $client->send(json_encode($data));
        }
    }
    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
    }
    public function onError(ConnectionInterface $conn, \Exception $e) {
        $conn->close();
    }
    private function updateBoard($data) {
        // 更新数据库
        $db = new PDO('mysql:host=localhost;dbname=board', 'user', 'pass');
        $stmt = $db->prepare("UPDATE board_items SET status = ? WHERE id = ?");
        $stmt->execute([$data['status'], $data['id']]);
    }
}
// 启动服务
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Board()
        )
    ),
    8080
);
$server->run();

Server-Sent Events (SSE)

// sse.php - SSE 事件流
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
// 获取看板数据
function getBoardData() {
    $db = new PDO('mysql:host=localhost;dbname=board', 'user', 'pass');
    $stmt = $db->query("SELECT * FROM board_items ORDER BY position");
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$lastCheck = time();
while (true) {
    $currentData = getBoardData();
    echo "data: " . json_encode([
        'items' => $currentData,
        'timestamp' => time()
    ]) . "\n\n";
    ob_flush();
    flush();
    // 每5秒检查一次
    sleep(5);
}

轮询(简单但效率低)

// api.php - 数据接口
header('Content-Type: application/json');
$boardId = $_GET['board_id'] ?? 1;
$lastUpdate = $_GET['last_update'] ?? 0;
$db = new PDO('mysql:host=localhost;dbname=board', 'user', 'pass');
// 只返回更新后的数据
$stmt = $db->prepare("
    SELECT * FROM board_items 
    WHERE board_id = ? AND updated_at > ?
    ORDER BY updated_at
");
$stmt->execute([$boardId, date('Y-m-d H:i:s', $lastUpdate)]);
$updates = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
    'success' => true,
    'updates' => $updates,
    'timestamp' => time()
]);

前端实现

WebSocket 客户端

// websocket-client.js
class BoardClient {
    constructor() {
        this.ws = new WebSocket('ws://localhost:8080');
        this.board = document.getElementById('board');
        this.init();
    }
    init() {
        this.ws.onopen = () => {
            console.log('Connected to board server');
            this.loadBoard();
        };
        this.ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.updateBoardUI(data);
        };
        this.ws.onerror = (error) => {
            console.error('WebSocket error:', error);
            this.reconnect();
        };
    }
    loadBoard() {
        fetch('/api/board.php?id=1')
            .then(response => response.json())
            .then(data => this.renderBoard(data));
    }
    renderBoard(items) {
        const columns = ['todo', 'in_progress', 'done'];
        items.forEach(item => {
            const card = document.createElement('div');
            card.className = 'card';
            card.dataset.id = item.id;
            card.innerHTML = `
                <h3>${item.title}</h3>
                <p>${item.description}</p>
                <span class="status">${item.status}</span>
            `;
            const column = document.getElementById(item.status);
            column.appendChild(card);
        });
    }
    updateBoardUI(data) {
        // 更新单个卡片
        const card = document.querySelector(`[data-id="${data.id}"]`);
        if (card) {
            if (data.action === 'delete') {
                card.remove();
            } else {
                card.querySelector('.status').textContent = data.status;
            }
        }
    }
    sendUpdate(itemId, newStatus) {
        this.ws.send(JSON.stringify({
            id: itemId,
            status: newStatus,
            action: 'update'
        }));
    }
    reconnect() {
        setTimeout(() => {
            this.ws = new WebSocket('ws://localhost:8080');
            this.init();
        }, 5000);
    }
}

SSE 客户端

// sse-client.js
class SSEClient {
    constructor() {
        this.eventSource = new EventSource('/sse.php');
        this.init();
    }
    init() {
        this.eventSource.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.renderBoard(data.items);
        };
        this.eventSource.onerror = () => {
            console.error('SSE connection failed');
            this.reconnect();
        };
    }
    renderBoard(items) {
        const board = document.getElementById('board');
        board.innerHTML = '';
        const columns = {
            'todo': { title: '待办', color: '#f0f0f0' },
            'in_progress': { title: '进行中', color: '#fff3cd' },
            'done': { title: '已完成', color: '#d4edda' }
        };
        Object.entries(columns).forEach(([key, column]) => {
            const columnDiv = document.createElement('div');
            columnDiv.className = 'column';
            columnDiv.style.backgroundColor = column.color;
            columnDiv.innerHTML = `<h2>${column.title}</h2>`;
            items.filter(item => item.status === key)
                 .forEach(item => {
                     const card = document.createElement('div');
                     card.className = 'card';
                     card.innerHTML = `
                         <h3>${item.title}</h3>
                         <p>${item.description || ''}</p>
                         <small>${item.assignee || ''}</small>
                     `;
                     columnDiv.appendChild(card);
                 });
            board.appendChild(columnDiv);
        });
    }
}

拖拽功能(使用 SortableJS)

// drag-and-drop.js
import Sortable from 'sortablejs';
class DragDropBoard {
    constructor() {
        this.initSortable();
        this.client = new BoardClient();
    }
    initSortable() {
        document.querySelectorAll('.column').forEach(column => {
            new Sortable(column, {
                group: 'board',
                animation: 150,
                onEnd: (evt) => {
                    const itemId = evt.item.dataset.id;
                    const newStatus = evt.to.id;
                    // 发送更新到服务器
                    this.client.sendUpdate(itemId, newStatus);
                    // 更新UI
                    this.updateCounter(evt.from.id, -1);
                    this.updateCounter(evt.to.id, 1);
                }
            });
        });
    }
    updateCounter(columnId, delta) {
        const counter = document.querySelector(`#${columnId} .counter`);
        if (counter) {
            counter.textContent = parseInt(counter.textContent) + delta;
        }
    }
}

数据库设计

-- board_items 表
CREATE TABLE board_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    board_id INT NOT NULL,VARCHAR(255) NOT NULL,
    description TEXT,
    status ENUM('todo', 'in_progress', 'done') DEFAULT 'todo',
    position INT DEFAULT 0,
    assignee VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_board_status (board_id, status),
    INDEX idx_updated (updated_at)
);
-- board 表
CREATE TABLE boards (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

自动化部署脚本

#!/bin/bash
# deploy-board.sh
# 安装依赖
composer install
npm install
# 启动 WebSocket 服务
php server.php &
# 启动 Web 服务器
php -S localhost:8000 &
# 使用 Supervisor 或 PM2 管理进程
# npm install -g pm2
# pm2 start server.php --interpreter php

优化建议

  1. 使用 Redis 做消息队列:处理高并发场景
  2. 实现连接池:管理 WebSocket 连接
  3. 添加心跳检测:保持连接活跃
  4. 实现断线重连:提高用户体验
  5. 数据压缩:减少传输数据量

完整示例项目结构

board-app/
├── public/
│   ├── index.html
│   ├── style.css
│   └── js/
│       ├── app.js
│       ├── websocket-client.js
│       └── drag-drop.js
├── src/
│   ├── WebSocket/
│   │   └── BoardServer.php
│   ├── Database/
│   │   └── Connection.php
│   └── API/
│       └── BoardController.php
├── server/
│   └── start.php
├── config/
│   └── database.php
├── composer.json
└── package.json

选择哪种方案取决于你的具体需求:

  • WebSocket:适合实时性要求高的场景
  • SSE:适合服务器推送场景,实现简单
  • 轮询:适合简单场景,但效率较低

推荐使用 WebSocket 方案,能提供最好的实时体验。

抱歉,评论功能暂时关闭!