本文目录导读:

我来详细介绍几种用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
优化建议
- 使用 Redis 做消息队列:处理高并发场景
- 实现连接池:管理 WebSocket 连接
- 添加心跳检测:保持连接活跃
- 实现断线重连:提高用户体验
- 数据压缩:减少传输数据量
完整示例项目结构
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 方案,能提供最好的实时体验。