導讀:DedeCMS 作為國產 CMS 典範,其「模板標簽 + 靜態生成」架構極具特色。本文從入口
index.php切入,深度還原PartView、DedeTagParse、MakeOneTag核心類協作流程,揭秘模板編譯、標簽解析、緩存機制、HTML 生成全鏈路,附帶目錄結構圖譜。
請求入口 (index.php) │ ├─ 加載核心配置 (common.inc.php) → 定義常量、載入 DB、函數庫 │ ├─ 判斷 upcache 參數 → 是 → 執行靜態化流程 │ │ │ ├─ 實例化 PartView │ │ │ │ │ ├─ SetTemplet() → DedeTagParse::LoadTemplate() │ │ │ │ │ │ │ ├─ 讀取模板文件內容 │ │ │ ├─ 檢查緩存 (tplcache/) → 命中 → 反序列化標簽樹 │ │ │ └─ 未命中 → ParseTemplate() 解析標簽 → 序列化緩存 │ │ │ │ │ └─ ParseTemplet() → MakeOneTag() 綁定數據 │ │ │ │ │ ├─ 掃描 taglib/ 目錄獲取所有標簽類 │ │ ├─ 遍歷標簽樹 (CTags) │ │ ├─ field 標簽 → 直接賦值 Fields 陣列 │ │ ├─ 業務標簽 (arclist/channel 等) → include 標簽文件 → 執行函數 → 返回 HTML │ │ └─ 將結果寫入標簽對象 TagValue │ │ │ └─ SaveToHtml() → DedeTagParse::SaveTo() → 替換標簽位置輸出最終 HTML → 寫入 index.html │ └─ 否 → 301 重定向到 index.html (靜態文件直接服務)
| 類/文件 | 職責 | 關鍵方法 |
|---|---|---|
PartView (arc.partview.class.php) | 視圖控制器,協調模板加載、解析、數據綁定、輸出 | SetTemplet, ParseTemplet, SaveToHtml |
DedeTagParse (dedetag.class.php) | 模板解析引擎核心,負責詞法分析、語法樹構建、緩存管理、最終輸出 | LoadTemplate, ParseTemplate, SaveTo, SaveCache |
DedeTag (dedetag.class.php) | 單個標簽節點對象,保存名稱、屬性、內部文本、位置、解析後值 | 屬性: TagName, InnerText, StartPos, EndPos, TagValue, CAttribute |
DedeAttributeParse (dedetag.class.php) | 標簽屬性字符串解析器 (如 row=10 typeid='1,2') | SetSource, cAttributes |
MakeOneTag (channelunit.func.php) | 標簽數據綁定總入口,動態加載標簽處理文件,執行並回填值 | 遍歷 CTags, include taglib, 調用標簽函數 |
TypeLink (typelink.class.php) | 欄目鏈接生成、面包屑導航、頂級欄目查找 | GetPositionLink, GetOneTypeLink |
ChannelUnit (channelunit.class.php) | 內容模型字段處理、附加表操作 (較少直接使用) | GetFields, MakeField |
輸入:模板字符串 $this->SourceString
輸出:標簽對象陣列 $this->CTags
{dede: (開始)、} (結束)、{/dede:tagname} (塊結束)、/} (自閉合結束)。for ($i=0; $i<$len; $i++) 逐字符掃描,strpos 定位下一個 {dede:。{dede: 後讀取直到空格/}/換行,得到 tagname (如 arclist)。/} (自閉合) 或 {/dede:tagname} (塊標簽),取最近者作為結束位置。} 前為屬性字符串,後為 InnerText (模板片段)。DedeAttributeParse 將 row=10 type='image' 解析為鍵值陣列。CTags 陣列,索引為 TagID。CTags 核心屬性序列化為 PHP 代碼陣列 $z[index]=['tagname', innerText, start, end, [attrs]] 寫入 data/tplcache/xxx.inc,同時寫入修改時間戳 .txt 文件。
// 緩存文件名生成
$ckfile = str_replace($templateDir, '', $templateFile) . substr(md5($templateFile), 0, 16) . '.inc';
// 例如: index.htm → index.htm_a1b2c3d4e5f67890.inc
// 驗證有效性
if (!file_exists($ckfile) || !file_exists($ckfile . '.txt')) return false;
$cachedMtime = file_get_contents($ckfile . '.txt');
if ($cachedMtime != filemtime($templateFile)) return false; // 模板已修改
// 載入緩存 (include 直接執行 PHP 陣列定義)
include $ckfile; // 得到 $z 陣列
// 還原 DedeTag 對象樹
foreach ($z as $k => $v) {
$tag = new DedeTag();
$tag->TagName = $v[0];
$tag->InnerText = $v[1];
$tag->StartPos = $v[2];
$tag->EndPos = $v[3];
$tag->TagID = $k;
if (isset($v[4])) $tag->CAttribute->Items = $v[4];
$this->CTags[$k] = $tag;
}
$this->Count = count($z) - 1;
return true;
function MakeOneTag(&$dtp, &$obj) {
// 1. 掃描所有可用標簽 (taglib/*.lib.php)
$allTags = glob(DEDEINC . '/taglib/*.lib.php');
// 2. 遍歷當前模板解析出的標簽樹
foreach ($dtp->CTags as $tag) {
$tagName = $tag->TagName;
// 3. 特殊標簽: field (直接映射 Fields 陣列)
if ($tagName == 'field') {
$fieldName = $tag->CAttribute->Items['name'] ?? '';
if ($fieldName == 'array') {
$dtp->Assign($obj->Fields); // 整個 Fields 陣列賦值
} else {
$tag->TagValue = $obj->Fields[$fieldName] ?? '';
}
continue;
}
// 4. 別名兼容 (arclist -> list, arclist -> sgpage 等)
$funcName = $tagName;
if (in_array($tagName, ['list', 'sgpage'])) $funcName = 'arclist';
// 5. 業務標簽: 動態加載並執行
$libFile = DEDEINC . '/taglib/' . $funcName . '.lib.php';
if (file_exists($libFile)) {
include $libFile; // 定義函數 lib_$funcName(&$dtp, &$tag)
$func = 'lib_' . $funcName;
if (function_exists($func)) {
$func($dtp, $tag); // 執行標簽邏輯,結果寫入 $tag->TagValue
}
}
}
}
function lib_arclist(&$dtp, &$tag) {
// 1. 解析屬性
$att = $tag->CAttribute->Items;
$row = $att['row'] ?? 10;
$typeid = $att['typeid'] ?? 0;
// ... 更多屬性
// 2. 構建 SQL 查詢
$sql = "SELECT * FROM dede_archives WHERE arcrank > -1";
if ($typeid) $sql .= " AND typeid IN ($typeid)";
$sql .= " ORDER BY pubdate DESC LIMIT $row";
// 3. 執行查詢
$dsql = $GLOBALS['dsql']; // 全局 DB 對象
$dsql->SetQuery($sql);
$dsql->Execute();
// 4. 遍歷結果集,構建輸出 HTML
$innerText = $tag->InnerText; // 模板片段: [field:title/]
$result = '';
while ($row = $dsql->GetArray()) {
// 為每行創建臨時 Fields
$fields = $row;
// 解析 InnerText 中的 [field:xxx/] 標簽
$itemHtml = $dtp->GetResult($innerText, $fields); // 內部調用 MakeOneTag 遞歸解析
$result .= $itemHtml;
}
// 5. 回填最終值
$tag->TagValue = $result;
}
public function SaveTo($filename) {
$content = $this->SourceString; // 原始模板
// 從後向前替換,避免位置偏移
for ($i = $this->Count; $i >= 0; $i--) {
$tag = $this->CTags[$i];
if ($tag->IsReplace) continue; // 已替換跳過
// 替換 [StartPos, EndPos] 區間為 TagValue
$content = substr_replace($content, $tag->TagValue, $tag->StartPos, $tag->EndPos - $tag->StartPos + 1);
}
// 寫入文件
$dir = dirname($filename);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
return file_put_contents($filename, $content);
}
/ ├── index.php # 入口,靜態化觸發 ├── data/ │ ├── common.inc.php # DB 配置 │ ├── config.cache.inc.php # 系統配置緩存 │ ├── tplcache/ # 模板解析緩存 (*.inc, *.txt) │ └── sessions/ # Session 文件 ├── include/ │ ├── dedetag.class.php # 核心解析引擎 │ ├── arc.partview.class.php # 視圖控制器 │ ├── typelink.class.php # 欄目鏈接 │ ├── channelunit.class.php # 模型處理 │ ├── taglib/ # 業務標簽庫 (arclist.lib.php, channel.lib.php ...) │ ├── tpllib/ # 動態標簽 (少用) │ └── FCKeditor/ # 編輯器 ├── dede/ # 後台管理目錄 ├── templets/ │ ├── default/ # 前台模板 │ │ ├── index.htm # 首頁模板 │ │ ├── article_article.htm # 文章頁 │ │ └── list_article.htm # 列表頁 │ ├── plus/ # 插件模板 │ └── system/ # 系統底層模板 ├── plus/ # 插件入口 (搜索、留言板、RSS...) ├── uploads/ # 上傳文件 (allimg, litimg, media, soft) └── special/ # 專題頁面
| 維度 | 優點 | 缺點/風險 | 現代化建議 |
|---|---|---|---|
| 模板語法 | 簡單易學、HTML 友好、非程序員可改 | 邏輯表達弱、無繼承/塊機制、XSS 風險 | 遷移至 Twig/Blade,保留標簽語法兼容層 |
| 靜態生成 | 極致性能、抗高併發、安全無動態執行 | 發布延遲、動態交互難 (評論/用戶態)、文件系統壓力 | CDN + 邊緣緩存 + ESI / SSI / AJAX 動態片段 |
| 標簽機制 | 擴展性強、PHP 直寫、無編譯步驟 | 全局變量污染、無依賴注入、難測試、安全隱患 (include 任意文件) | 服務化標簽 (RPC/HTTP)、沙箱執行、PSR-4 自動加載 |
| 緩存策略 | 文件緩存、無需 Redis、部署簡單 | 無失效策略、併發競爭、無分佈式支援 | Redis/Memcached + 標簽化失效 |
| 安全性 | 後台目錄可重命名 | 歷史漏洞多 (SQL注入、任意文件上傳、模板注入)、停止維護 | 全面審計、WAF、禁用危險函數、升級 PHP 8+ |
DedeCMS 的架構在「靜態化性能」與「模板易用性」上達到了當時 PHP CMS 的極致平衡,但其全局狀態依賴、缺乏現代工程規範 (命名空間/自動加載/依賴注入/中間件)、安全歷史包袱重,已不適合新項目。存量項目建議凍結核心、外掛服務化、逐步剝離業務邏輯至微服務/Laravel,而非繼續在核心代碼上修補。