本文目录导读:

我来详细介绍几种用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 方案,能提供最好的实时体验。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播