長文顯示需分頁,核心策略有二:字數切割(程式自動)與分頁符(編輯手動)。本文深度剖析兩者優劣、HTML 標籤閉合難題、分頁類別架構與前端呈現模式。
| 維度 | 字數切割 | 分頁符 (如 <hr class="page">) |
|---|---|---|
| 實作難度 | 低(字串截取) | 中(需編輯器支援) |
| 閱讀體驗 | 差(可能斷句、斷標籤) | 優(人工控制斷點) |
| HTML 安全性 | 極難保證標籤閉合 | 天然避開標籤斷裂 |
| 維護成本 | 零(自動) | 需編輯時插入標記 |
| 適用場景 | 純文字、自動化匯入 | 富文本、CMS 文章、技術文檔 |
-- 文章內容欄位存儲含分頁符的 HTML
<p>第一段內容...</p>
<hr class="page" />
<p>第二段內容...</p>
<hr class="page" />
<p>第三段內容...</p>
<?php
class Pager {
private int $totalPages;
private int $currentPage;
private int $subPages = 10; // 顯示頁碼數量
private string $linkTemplate; // 分頁連結模板,含 {page} 佔位符
private int $type = 2; // 1=簡易模式, 2=經典模式
public function __construct(int $totalPages, int $currentPage = 1,
int $subPages = 10, string $link = '', int $type = 2) {
$this->totalPages = max(1, $totalPages);
$this->currentPage = max(1, min($currentPage, $this->totalPages));
$this->subPages = $subPages;
$this->type = $type;
// 建立連結模板
if ($link) {
$this->linkTemplate = strpos($link, '?') === false ? $link . '?page={page}'
: $link . '&page={page}';
} else {
$this->linkTemplate = $_SERVER['PHP_SELF'] . '?page={page}';
}
}
public function render(): string {
return $this->type === 1 ? $this->simpleMode() : $this->classicMode();
}
// 簡易模式:首頁 上頁 下頁 尾頁
private function simpleMode(): string {
$html = "第 {$this->currentPage} / {$this->totalPages} 頁 ";
if ($this->currentPage > 1) {
$html .= $this->link(1, '首頁') . ' ';
$html .= $this->link($this->currentPage - 1, '上頁') . ' ';
} else {
$html .= '首頁 上頁 ';
}
if ($this->currentPage < $this->totalPages) {
$html .= $this->link($this->currentPage + 1, '下頁') . ' ';
$html .= $this->link($this->totalPages, '尾頁') . ' ';
} else {
$html .= '下頁 尾頁 ';
}
return $html;
}
// 經典模式:頁碼列表 + 首/尾/上/下
private function classicMode(): string {
$html = "第 {$this->currentPage} / {$this->totalPages} 頁 ";
if ($this->currentPage > 1) {
$html .= $this->link(1, '首頁') . ' ';
$html .= $this->link($this->currentPage - 1, '上頁') . ' ';
} else {
$html .= '首頁 上頁 ';
}
// 計算顯示頁碼範圍
$pages = $this->calculatePageRange();
foreach ($pages as $p) {
if ($p == $this->currentPage) {
$html .= "[$p] ";
} else {
$html .= $this->link($p, "[$p]") . ' ';
}
}
if ($this->currentPage < $this->totalPages) {
$html .= $this->link($this->currentPage + 1, '下頁') . ' ';
$html .= $this->link($this->totalPages, '尾頁') . ' ';
} else {
$html .= '下頁 尾頁 ';
}
return $html;
}
private function calculatePageRange(): array {
if ($this->totalPages <= $this->subPages) {
return range(1, $this->totalPages);
}
$half = floor($this->subPages / 2);
$start = max(1, $this->currentPage - $half);
$end = min($this->totalPages, $start + $this->subPages - 1);
if ($end - $start + 1 < $this->subPages) {
$start = max(1, $end - $this->subPages + 1);
}
return range($start, $end);
}
private function link(int $page, string $text): string {
$url = str_replace('{page}', $page, $this->linkTemplate);
return "$text";
}
}
?>
<?php
function getArticlePage(string $content, int $page = 1, string $delimiter = '<hr class="page" />'): array {
$parts = explode($delimiter, $content);
$total = count($parts);
$page = max(1, min($page, $total));
return [
'content' => $parts[$page - 1] ?? '',
'currentPage' => $page,
'totalPages' => $total,
'hasPrev' => $page > 1,
'hasNext' => $page < $total,
];
}
// 控制器使用範例
$articleContent = $db->query("SELECT content FROM articles WHERE id=1")->fetchColumn();
$page = (int)($_GET['page'] ?? 1);
$paged = getArticlePage($articleContent, $page);
$pager = new Pager($paged['totalPages'], $page, 10, 'article.php?id=1');
?>
<article>
<div class="content"><?= $paged['content'] ?></div>
<nav class="pagination"><?= $pager->render() ?></nav>
</article>
若必須自動切割,請使用 DOMDocument 而非字串函式:
<?php
function truncateHtml(string $html, int $maxChars, string $suffix = '...'): string {
$dom = new DOMDocument();
$dom->loadHTML('<meta charset="UTF-8">' . $html, LIBHTML_HTML5 | LIBHTML_NOERROR);
$body = $dom->getElementsByTagName('body')->item(0);
$text = '';
$iterator = new RecursiveIteratorIterator(new RecursiveDOMIterator($body));
foreach ($iterator as $node) {
if ($node->nodeType === XML_TEXT_NODE) {
$text .= $node->textContent;
if (mb_strlen($text) >= $maxChars) {
// 截斷邏輯:移除後續節點
$excess = mb_strlen($text) - $maxChars;
$node->textContent = mb_substr($node->textContent, 0, -$excess) . $suffix;
// 移除該節點之後的所有兄弟節點
while ($next = $node->nextSibling) {
$next->parentNode->removeChild($next);
}
break 2; // 跳出雙層循環
}
}
}
return $dom->saveHTML($body);
}
?>
| 類型 | 資料來源 | 分頁依據 | SQL 模式 | 快取策略 |
|---|---|---|---|---|
| 列表分頁 | 資料庫查詢結果集 | 固定筆數 | LIMIT offset, size | 查詢結果快取、總數快取 |
| 內容分頁 | 單一文章長文本 | 分頁符/字數 | 單次查詢完整內容 | 分頁後 HTML 片段快取 |
採用分頁符策略搭配 Pager 類別,能同時滿足編輯靈活性、HTML 安全性、SEO 友善(單一 URL 含 rel=prev/next)三大需求,是成熟 CMS 系統的標準選擇。