編譯型模板引擎工作流程
模板文件 (.html)
↓ 讀取
模板源碼 (字符串)
↓ 正則替換 (詞法分析 + 語法轉換)
PHP 代碼字符串
↓ 寫入緩存文件 (.php)
編譯後的 PHP 文件
↓ include 執行 (運行時)
輸出 HTML
核心類結構 JTemplate.class.php
<?php
class JTemplate {
private $templateVar = []; // assign() 變量存儲
private $templateDir = ''; // 模板目錄
private $compileDir = ''; // 編譯緩存目錄
private $fileName = ''; // 當前模板文件名
public function __construct($templateDir, $compileDir) {
$this->templateDir = rtrim($templateDir, '/');
$this->compileDir = rtrim($compileDir, '/');
}
// 入口:顯示模板
public function display($fileName) {
$this->fileName = $fileName;
$templatePath = "{$this->templateDir}/{$fileName}";
if (!file_exists($templatePath)) {
$this->error("模板文件不存在: {$fileName}");
}
$compileFile = $this->getCompileFilePath();
// 編譯判斷:緩存不存在 或 模板文件更新過
if (!file_exists($compileFile) || filemtime($compileFile) < filemtime($templatePath)) {
$this->deleteOldCompiles();
$this->compile();
}
// 變量解析到當前作用域
extract($this->templateVar, EXTR_OVERWRITE);
include $compileFile;
}
// 變量賦值
public function assign($key, $value) {
$this->templateVar[$key] = $value;
}
// 核心編譯邏輯
private function compile() {
$content = file_get_contents("{$this->templateDir}/{$this->fileName}");
$phpCode = $this->parse($content);
$compileFile = $this->getCompileFilePath();
if (file_put_contents($compileFile, $phpCode) === false) {
$this->error("編譯目錄不可寫: {$this->compileDir}");
}
}
// 詞法/語法分析:模板標籤 → PHP 代碼
private function parse($content) {
$patterns = [
// 循環: ...
'//s' =>
'',
// 帶鍵名循環:
'//s' =>
' $$3) { ?>',
// else 分支: ...
'/(.+?)/s' =>
'$1',
// 結束循環
'//s' => '',
// 條件判斷
'//s' => '',
'//s' => '',
'//s' => '',
'//s' => '',
// 變量輸出:{$var}
'/\{([a-zA-Z0-9_\[\]\'\"\$\.\x7f-\xff]+)\}/s' => '',
// 常量輸出:{C:CONST_NAME}
'/\{C:(\w+)\}/s' => '',
// 包含子模板:{F:sub.html}
'/\{F:(.+?)\}/s' => 'templateDir.'/$1"; ?>',
// PHP 代碼塊:{P:code}
'/\{P:(.+?)\}/s' => '',
];
return preg_replace(array_keys($patterns), array_values($patterns), $content);
}
// 生成緩存文件名(含修改時間戳防止衝突)
private function getCompileFilePath() {
$baseName = pathinfo($this->fileName, PATHINFO_FILENAME);
$mtime = filemtime("{$this->templateDir}/{$this->fileName}");
return "{$this->compileDir}/{$baseName}_{$mtime}.php";
}
// 刪除同模板的舊緩存
private function deleteOldCompiles() {
$baseName = pathinfo($this->fileName, PATHINFO_FILENAME);
$files = glob("{$this->compileDir}/{$baseName}_*.php");
foreach ($files as $file) @unlink($file);
}
private function error($msg) {
exit("Error: {$msg}
");
}
}
模板文件示例 template/index.html
<!DOCTYPE html>
<html>
<head><title>{title}</title></head>
<body>
<h1>歡迎, {$user.name}</h1>
<!--if ($user.is_vip)-->
<span class="vip">VIP 會員</span>
<!--else-->
<span>普通用戶</span>
<!--endif-->
<ul>
<!--loop $items $item-->
<li>{$item.name} - ¥{$item.price}</li>
<!--endloop-->
</ul>
<!-- 包含底部模板 -->
{F:footer.html}
</body>
</html>
入口文件 index.php
<?php
require_once 'JTemplate.class.php';
$tpl = new JTemplate('template', 'compile');
$tpl->assign('title', '商品列表');
$tpl->assign('user', ['name' => '張三', 'is_vip' => true]);
$tpl->assign('items', [
['name' => 'iPhone 15', 'price' => 5999],
['name' => 'MacBook Pro', 'price' => 14999],
]);
$tpl->display('index.html');
編譯後生成的 PHP 緩存文件
<!DOCTYPE html>
<html>
<head><title><?php echo $title; ?></title></head>
<body>
<h1>歡迎, <?php echo $user['name']; ?></h1>
<?php if ($user['is_vip']) { ?>
<span class="vip">VIP 會員</span>
<?php } else { ?>
<span>普通用戶</span>
<?php } ?>
<ul>
<?php if (!empty($items) && is_array($items)) { foreach ($items as $item) { ?>
<li><?php echo $item['name']; ?> - ¥<?php echo $item['price']; ?></li>
<?php } } ?>
</ul>
<?php include_once "template/footer.html"; ?>
</body>
</html>
關鍵設計要點解析
| 模塊 | 關鍵技術 | 注意事項 |
| 詞法分析 | 正則替換 preg_replace | 順序敏感:優先匹配長標籤;s 修飾符跨行匹配 |
| 語法轉換 | 標籤映射為 PHP 控制結構 | 變量作用域:extract() 導入模板變量 |
| 緩存策略 | 文件修改時間比對 + 時間戳文件名 | 高併發下需加鎖防止重複編譯 |
| 安全性 | 模板中禁止任意 PHP 執行 | 僅允許白名單標籤;{P:} 慎用 |
| 性能 | 編譯後直接 include,無運行時解析 | OPcache 加速字節碼執行 |
進階擴展方向
- 繼承/佈局:
{extends:layout.html} + {block:content} 實現模板繼承
- 過濾器/管道:
{$name|upper|escape} 鏈式處理輸出
- 沙箱模式:禁用危險函數、限制類訪問、控制執行時間/內存
- 異步渲染:
{async:widgetName} 併發渲染片段,最後組裝
- AST 解析器:替換正則為詞法/語法分析器(如
nikic/PHP-Parser),支持更複雜語法、更好錯誤提示
現代替代方案對比
| 引擎 | 類型 | 特點 |
| Twig | 編譯型 | Symfony 標準,沙箱、繼承、擴展豐富,文檔完善 |
| Blade | 編譯型 | Laravel 內建,語法優雅,編譯為原生 PHP,零依賴 |
| Smarty | 編譯型 | 老牌,功能全,插件機制,學習曲線平緩 |
| Plates | 原生 PHP | 無編譯,直接 PHP 語法,極簡輕量 |
| Lathe | 編譯型 | 高性能,類 Twig 語法,C 擴展加速 |
總結
編譯型模板引擎通過「模板 → PHP 代碼 → 緩存文件 → include 執行」實現高性能渲染。核心難點在於:標籤設計的表達力與安全性平衡、緩存失效策略的正確性、錯誤回溯到原始模板行號。自建引擎適合學習原理或極簡場景,生產環境強烈建議使用成熟的 Twig/Blade,享受生態紅利與安全審計。