技術棧
後端:PHP + MySQL
前端:Prototype.js 1.6 (經典 AJAX 框架,現代項目建議改用 Fetch/Axios)
快取:檔案快取機制,減少資料庫查詢
資料庫結構
CREATE TABLE `categories` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL,
`parent_id` INT UNSIGNED DEFAULT 0,
INDEX `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 範例資料
INSERT INTO categories (name, parent_id) VALUES
('電子產品', 0), ('服飾鞋包', 0), ('家居生活', 0),
('手機', 1), ('筆電', 1), ('配件', 1),
('男裝', 2), ('女裝', 2), ('鞋類', 2);
後端實現
1. 共用配置 (include/config_base.php)
<?php
define('DB_HOST', 'localhost');
define('DB_NAME', 'shop');
define('DB_USER', 'root');
define('DB_PASS', 'password');
define('CACHE_DIR', __DIR__ . '/../cache/');
2. 快取工具 (include/inc_cache.php)
<?php
function cache_write(string $file, string $content): int {
$path = CACHE_DIR . $file;
$dir = dirname($path);
if (!is_dir($dir)) mkdir($dir, 0755, true);
return file_put_contents($path, $content, LOCK_EX);
}
function cache_read(string $file): string {
$path = CACHE_DIR . $file;
return file_exists($path) ? file_get_contents($path) : '';
}
function cache_exists(string $file): bool {
return file_exists(CACHE_DIR . $file);
}
3. 分類模型 (include/inc_type.php)
<?php
class Type {
private $pdo;
public function __construct() {
$this->pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
// 取得大分類選項
public function getBigOptions(): string {
$stmt = $this->pdo->query("SELECT id, name FROM categories WHERE parent_id = 0 ORDER BY id");
$html = '';
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$html .= "{$row['name']} ";
}
return $html;
}
// 取得子分類選項
public function getSmallOptions(int $parentId): string {
$stmt = $this->pdo->prepare("SELECT id, name FROM categories WHERE parent_id = ? ORDER BY id");
$stmt->execute([$parentId]);
$html = "-- 不限子分類 -- ";
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$html .= "{$row['name']} ";
}
return $html;
}
public function close() { $this->pdo = null; }
}
4. 大分類 AJAX 端點 (GetBigType.php)
<?php
header("Content-Type: text/html; charset=utf-8");
require_once __DIR__ . '/include/config_base.php';
require_once __DIR__ . '/include/inc_type.php';
require_once __DIR__ . '/include/inc_cache.php';
$cacheFile = 'bigtype.cache.txt';
if (!cache_exists($cacheFile)) {
$type = new Type();
$options = $type->getBigOptions();
cache_write($cacheFile, $options);
$type->close();
} else {
$options = cache_read($cacheFile);
}
echo "";
echo "-- 不限大分類 -- ";
echo $options;
echo " ";
5. 子分類 AJAX 端點 (GetSmallType.php)
<?php
header("Content-Type: text/html; charset=utf-8");
require_once __DIR__ . '/include/config_base.php';
require_once __DIR__ . '/include/inc_type.php';
require_once __DIR__ . '/include/inc_cache.php';
$fatherId = (int)($_GET['fatherid'] ?? 0);
if ($fatherId <= 0) exit;
$cacheFile = "sun_{$fatherId}_cache.txt";
if (!cache_exists($cacheFile)) {
$type = new Type();
$options = $type->getSmallOptions($fatherId);
cache_write($cacheFile, $options);
$type->close();
} else {
$options = cache_read($cacheFile);
}
echo "";
echo $options;
echo " ";
前端頁面
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>二級聯動選單</title>
<script src="https://cdn.jsdelivr.net/npm/prototypejs@1.7.3/dist/prototype.min.js"></script>
<style>
select { margin: 5px; padding: 5px; }
#loading { color: #666; font-size: 12px; }
</style></head><body>
<h3>商品分類篩選</h3>
<div id="BigType"></div>
<div id="SmallType"></div>
<div id="loading" style="display:none">載入中...</div>
<script>
// 載入大分類
function GetBigType() {
$('loading').show();
new Ajax.Updater('BigType', 'GetBigType.php', {
method: 'get',
onComplete: function() { $('loading').hide(); }
});
}
// 載入子分類
function GetSmallType() {
const fatherId = $F('fatherid');
if (!fatherId) { $('SmallType').update(''); return; }
$('loading').show();
new Ajax.Updater('SmallType', 'GetSmallType.php', {
method: 'get',
parameters: { fatherid: fatherId },
onComplete: function() { $('loading').hide(); }
});
}
// 初始化
document.observe('dom:loaded', GetBigType);
</script>
</body></html>
Prototype.js 關鍵 API 解析
方法 用法 說明
$('id')$('elementId') document.getElementById 簡寫,並擴充 Element 方法
$F('id')$F('elementId') 取得表單元素值 (Form.Element.getValue)
Ajax.Updaternew Ajax.Updater(container, url, options) 自動將回應內容注入 container (innerHTML)
Ajax.Requestnew Ajax.Request(url, options) 底層請求,需手動處理回應
Element.update$('id').update(html) 設定 innerHTML
document.observedocument.observe('dom:loaded', fn) DOMContentLoaded 事件
現代化重寫:Fetch + JSON (推薦新專案)
後端改為 JSON (GetSmallTypeJson.php)
<?php
header("Content-Type: application/json; charset=utf-8");
require_once __DIR__ . '/include/config_base.php';
$fatherId = (int)($_GET['fatherid'] ?? 0);
if ($fatherId <= 0) exit(json_encode([]));
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$stmt = $pdo->prepare("SELECT id, name FROM categories WHERE parent_id = ? ORDER BY id");
$stmt->execute([$fatherId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 加上預設選項
array_unshift($rows, ['id' => 0, 'name' => '-- 不限子分類 --']);
echo json_encode($rows);
前端原生 JS (無依賴)
<script>
async function loadBigType() {
const resp = await fetch('GetBigTypeJson.php');
const data = await resp.json();
const sel = document.getElementById('fatherid');
sel.innerHTML = data.map(d => `${d.name} `).join('');
}
async function loadSmallType() {
const fatherId = document.getElementById('fatherid').value;
const resp = await fetch(`GetSmallTypeJson.php?fatherid=${fatherId}`);
const data = await resp.json();
const sel = document.getElementById('sunid');
sel.innerHTML = data.map(d => `${d.name} `).join('');
}
// 事件綁定
document.addEventListener('DOMContentLoaded', loadBigType);
document.getElementById('fatherid').addEventListener('change', loadSmallType);
</script>
快取策略對比
策略 優點 缺點 適用
檔案快取 (原教程) 簡單、無額外依賴 高併發下鎖競爭、分散式部署需共享儲存 中小流量、單機
Redis 快取 高效能、支援集群、TTL 自動過期 需額外基建 高併發、分散式
APCu (PHP 共享記憶體) 極快、無網路開銷 單機、重啟失效 單機高頻讀取
CDN/Edge 快取 就近訪問、減輕源站 動態參數難快取 靜態/低變化選單
安全性補強
輸入驗證:(int)$_GET['fatherid'] 強制轉型,防 SQL 注入
輸出編碼:htmlspecialchars() 或 JSON 自動處理
CSRF 保護:狀態改變操作需 Token,純查詢可豁免
頻率限制:單 IP 限制請求頻率,防濫用