首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 靜態頁面生成類進階版:靈活目錄策略、多模板支持、緩存失效機制

     編輯:PHP 相關知識

從基礎到生產級:靜態化類的演進

上一篇介紹了基礎的 CreateHtml 類。生產環境需解決:多種目錄組織策略 (按年/月/日/分類/ID)、多模板渲染、文件命名規範、併發安全、緩存失效與更新。本文呈現一個功能完整的 HtmlGenerator 類。

核心類設計

<?php
/**
 * 專業級靜態頁面生成器
 * 特性:
 * - 多種目錄策略:year, month, day, category, id, flat
 * - 模板引擎無關 (支持原生 PHP、Blade、Twig、Smarty 等)
 * - 併發安全寫入 (文件鎖 + 原子重命名)
 * - 緩存元數據記錄 (便於失效/更新)
 * - 靈活的文件命名規則
 */
class HtmlGenerator {
    // 配置屬性
    private $rootDir = 'html';           // 靜態文件根目錄
    private $dirStrategy = 'day';        // 目錄策略
    private $nameStrategy = 'id';        // 命名策略
    private $templateEngine = null;      // 模板引擎實例
    private $templatePath = '';          // 模板目錄
    private $metadataFile = '.metadata.json'; // 元數據文件
    
    // 運行時狀態
    private $currentData = [];
    private $currentTemplate = '';
    private $generatedPath = '';
    
    /** 目錄策略常量 */
    const DIR_YEAR = 'year';      // html/2024/
    const DIR_MONTH = 'month';    // html/2024/05/
    const DIR_DAY = 'day';        // html/2024/05/20/
    const DIR_CATEGORY = 'category'; // html/news/
    const DIR_ID = 'id';          // html/12345/ (按ID分目錄)
    const DIR_FLAT = 'flat';      // html/ (平鋪)
    
    /** 命名策略常量 */
    const NAME_ID = 'id';           // 12345.html
    const NAME_TIME_ID = 'time_id'; // 20240520_12345.html
    const NAME_SLUG = 'slug';       // article-title.html
    const NAME_MD5 = 'md5';         // a1b2c3d4.html
    const NAME_CUSTOM = 'custom';   // 自定義回調
    
    public function __construct(array $config = []) {
        $this->rootDir = $config['root_dir'] ?? 'html';
        $this->dirStrategy = $config['dir_strategy'] ?? self::DIR_DAY;
        $this->nameStrategy = $config['name_strategy'] ?? self::NAME_ID;
        $this->templatePath = $config['template_path'] ?? '';
        $this->templateEngine = $config['template_engine'] ?? null;
        $this->metadataFile = $config['metadata_file'] ?? '.metadata.json';
    }
    
    /** 設置模板引擎 (需實現 render(string $template, array $data): string 接口) */
    public function setTemplateEngine($engine) {
        $this->templateEngine = $engine;
        return $this;
    }
    
    /** 設置數據並生成 */
    public function generate(array $data, string $template = '', string $customName = ''): string {
        $this->currentData = $data;
        $this->currentTemplate = $template;
        
        // 1. 渲染 HTML
        $html = $this->render($template, $data);
        
        // 2. 確定保存路徑
        $dir = $this->buildDirectory($data);
        $fileName = $this->buildFileName($data, $customName);
        $fullPath = $dir . DIRECTORY_SEPARATOR . $fileName;
        
        // 3. 原子寫入
        $this->atomicWrite($fullPath, $html);
        
        // 4. 記錄元數據
        $this->recordMetadata($fullPath, $data);
        
        $this->generatedPath = $fullPath;
        return $fullPath;
    }
    
    /** 渲染模板 */
    private function render(string $template, array $data): string {
        if ($this->templateEngine) {
            // 支持任意實現 render() 的引擎
            return $this->templateEngine->render($template, $data);
        }
        
        // 原生 PHP 模板 (extract + include)
        if ($template && is_file($this->templatePath . DIRECTORY_SEPARATOR . $template)) {
            extract($data, EXTR_SKIP); // 變量解壓到當前作用域
            ob_start();
            include $this->templatePath . DIRECTORY_SEPARATOR . $template;
            return ob_get_clean();
        }
        
        // 無模板:直接序列化數據 (調試用)
        return '<pre>' . htmlspecialchars(var_export($data, true)) . '</pre>';
    }
    
    /** 構建目錄路徑 */
    private function buildDirectory(array $data): string {
        $base = rtrim($this->rootDir, '/\\');
        $subDir = '';
        
        switch ($this->dirStrategy) {
            case self::DIR_YEAR:
                $subDir = date('Y');
                break;
            case self::DIR_MONTH:
                $subDir = date('Y/m');
                break;
            case self::DIR_DAY:
                $subDir = date('Y/m/d');
                break;
            case self::DIR_CATEGORY:
                $cat = $data['category'] ?? $data['cat_id'] ?? 'uncategorized';
                $subDir = strtolower(preg_replace('/[^a-z0-9_-]/i', '_', $cat));
                break;
            case self::DIR_ID:
                $id = $data['id'] ?? $data['article_id'] ?? 'unknown';
                $subDir = (string)$id;
                break;
            case self::DIR_FLAT:
            default:
                $subDir = '';
        }
        
        $fullDir = $base . ($subDir ? DIRECTORY_SEPARATOR . $subDir : '');
        
        // 遞歸創建 (權限 0755)
        if (!is_dir($fullDir)) {
            if (!mkdir($fullDir, 0755, true)) {
                throw new RuntimeException("無法創建目錄: $fullDir");
            }
        }
        
        return $fullDir;
    }
    
    /** 構建文件名 */
    private function buildFileName(array $data, string $customName = ''): string {
        $ext = '.html';
        
        switch ($this->nameStrategy) {
            case self::NAME_ID:
                $name = $data['id'] ?? $data['article_id'] ?? 'index';
                break;
            case self::NAME_TIME_ID:
                $time = isset($data['created_at']) ? $data['created_at'] : time();
                $id = $data['id'] ?? $data['article_id'] ?? 'index';
                $name = date('YmdHis', $time) . '_' . $id;
                break;
            case self::NAME_SLUG:
                $title = $data['title'] ?? $data['slug'] ?? 'untitled';
                $name = $this->slugify($title);
                break;
            case self::NAME_MD5:
                $content = json_encode($data, JSON_UNESCAPED_UNICODE);
                $name = substr(md5($content), 0, 12);
                break;
            case self::NAME_CUSTOM:
                $name = $customName ?: 'custom';
                break;
            default:
                $name = 'index';
        }
        
        // 文件名安全過濾
        $name = preg_replace('/[^a-zA-Z0-9_-]/', '', $name);
        return $name . $ext;
    }
    
    /** 字符串轉 slug */
    private function slugify(string $text): string {
        // 簡易版,生產建議用 cocur/slugify 库
        $text = mb_strtolower($text, 'UTF-8');
        $text = preg_replace('/[^a-z0-9\u4e00-\u9fa5]+/u', '-', $text);
        $text = trim($text, '-');
        return substr($text, 0, 100); // 限制長度
    }
    
    /** 原子寫入:寫入臨時文件 -> 重命名 (避免讀取到半寫入文件) */
    private function atomicWrite(string $fullPath, string $content): void {
        $dir = dirname($fullPath);
        $tmpName = $dir . DIRECTORY_SEPARATOR . '.' . basename($fullPath) . '.tmp.' . uniqid();
        
        $fp = fopen($tmpName, 'w');
        if (!$fp) {
            throw new RuntimeException("無法創建臨時文件: $tmpName");
        }
        
        // 獨佔鎖
        if (flock($fp, LOCK_EX)) {
            fwrite($fp, $content);
            flock($fp, LOCK_UN);
        }
        fclose($fp);
        
        // 原子重命名 (同文件系統下保證原子性)
        if (!rename($tmpName, $fullPath)) {
            @unlink($tmpName);
            throw new RuntimeException("文件重命名失敗: $tmpName -> $fullPath");
        }
        
        // 設置文件權限
        @chmod($fullPath, 0644);
    }
    
    /** 記錄元數據 (用於緩存失效查找) */
    private function recordMetadata(string $path, array $data): void {
        $metaFile = dirname($path) . DIRECTORY_SEPARATOR . $this->metadataFile;
        $meta = [];
        
        if (is_file($metaFile)) {
            $json = file_get_contents($metaFile);
            $meta = json_decode($json, true) ?? [];
        }
        
        $key = basename($path);
        $meta[$key] = [
            'id' => $data['id'] ?? $data['article_id'] ?? null,
            'category' => $data['category'] ?? $data['cat_id'] ?? null,
            'updated_at' => $data['updated_at'] ?? time(),
            'generated_at' => time(),
            'template' => $this->currentTemplate,
        ];
        
        // 限制元數據條數,防止文件過大
        if (count($meta) > 10000) {
            // 簡單策略:保留最新 5000
            krsort($meta);
            $meta = array_slice($meta, 0, 5000, true);
        }
        
        file_put_contents($metaFile, json_encode($meta, JSON_UNESCAPED_UNICODE), LOCK_EX);
    }
    
    /** 根據 ID 刪除靜態文件 (更新/刪除文章時調用) */
    public function deleteById($id, $category = null): int {
        $deleted = 0;
        // 掃描元數據文件查找 (實際項目建議用數據庫記錄路徑)
        // 這裡演示掃描根目錄下所有元數據文件
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->rootDir, FilesystemIterator::SKIP_DOTS)
        );
        
        foreach ($iterator as $file) {
            if ($file->getBasename() === $this->metadataFile) {
                $meta = json_decode(file_get_contents($file->getPathname()), true) ?? [];
                foreach ($meta as $fileName => $info) {
                    if (($info['id'] ?? null) == $id && 
                        ($category === null || ($info['category'] ?? null) == $category)) {
                        $targetPath = $file->getPath() . DIRECTORY_SEPARATOR . $fileName;
                        if (is_file($targetPath) && unlink($targetPath)) {
                            $deleted++;
                            unset($meta[$fileName]);
                        }
                    }
                }
                // 更新元數據文件
                file_put_contents($file->getPathname(), json_encode($meta), LOCK_EX);
            }
        }
        
        return $deleted;
    }
    
    /** 獲取最後生成的路徑 */
    public function getGeneratedPath(): string {
        return $this->generatedPath;
    }
}
?>

使用範例:整合 Blade 模板引擎

<?php
// composer require illuminate/view
use Illuminate\View\Factory;
use Illuminate\Filesystem\Filesystem;

// 初始化 Blade (Laravel 組件獨立使用)
$blade = new Factory(new Filesystem(), __DIR__ . '/templates/cache');
$blade->addLocation(__DIR__ . '/templates');

// 配置生成器
$generator = new HtmlGenerator([
    'root_dir' => 'public/static',
    'dir_strategy' => HtmlGenerator::DIR_CATEGORY,
    'name_strategy' => HtmlGenerator::NAME_SLUG,
    'template_path' => __DIR__ . '/templates',
    'template_engine' => $blade,
]);

// 文章數據 (來自數據庫)
$article = [
    'id' => 1001,
    'title' => 'PHP 靜態化最佳實踐',
    'slug' => 'php-static-best-practice',
    'category' => 'tech',
    'content' => '<p>內容...</p>',
    'author' => '張三',
    'created_at' => strtotime('2024-05-20 10:00:00'),
    'updated_at' => time(),
];

// 生成靜態頁
try {
    $path = $generator->generate($article, 'article.blade.php');
    echo "✅ 生成成功: $path";
} catch (Exception $e) {
    echo "❌ 生成失敗: " . $e->getMessage();
}
?>
<!-- templates/article.blade.php -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>{{ $title }} - 技術博客</title>
    <meta name="description" content="{{ Str::limit(strip_tags($content), 160) }}">
</head>
<body>
    <article>
        <header>
            <h1>{{ $title }}</h1>
            <div class="meta">
                作者: {{ $author }} | 
                分類: {{ $category }} | 
                時間: {{ date("Y-m-d H:i", $created_at) }}
            </div>
        </header>
        <div class="content">
            {!! $content !!}
        </div>
    </article>
</body>
</html>

緩存失效策略:三種主流模式

1. 主動刪除 (更新/刪除時觸發)

<?php
// 文章更新後
function updateArticle($id, $data) {
    global $db, $generator;
    $db->update('articles', $data, ['id' => $id]);
    
    // 刪除舊靜態文件
    $generator->deleteById($id);
    
    // 異步重新生成 (推入隊列)
    Queue::push('regenerate_static', ['id' => $id]);
}
?>

2. Nginx try_files 回源 (首次訪問生成)

```nginx location /static/ { try_files $uri $uri/ @generate; } location @generate { # 轉發給 PHP 入口,參數中帶上原始路徑 fastcgi_pass php-fpm; fastcgi_param SCRIPT_FILENAME /var/www/generate.php; include fastcgi_params; } ``` ```php // generate.php $requestUri = $_SERVER['REQUEST_URI']; // 解析路徑獲取文章 ID/slug $id = parseIdFromUri($requestUri); $article = $db->find($id); $generator->generate($article, 'article.blade.php'); // 重定向回原 URL 或直接輸出 header("Location: $requestUri"); ```

3. 定時全量/增量重建

<?php
// crontab: 0 3 * * * /usr/bin/php /var/www/rebuild_static.php
// 每天凌晨 3 點重建過去 24 小時更新的文章
$updatedArticles = $db->query("SELECT * FROM articles WHERE updated_at > ?", time() - 86400);
foreach ($updatedArticles as $article) {
    $generator->generate($article, 'article.blade.php');
}
?>

性能優化清單

  • 模板編譯緩存:Blade/Twig/Smarty 都有編譯緩存,確保開啟且路徑可寫。
  • 批量生成:單次腳本生成多頁,減少初始化開銷。
  • 並行生成:使用 pcntl_fork 或 Swoole/Workerman 多進程並行。
  • CDN 推送:生成後自動推送到 CDN (阿里云/騰訊云/Cloudflare API 刷新/預熱)。
  • 壓縮輸出:生成前啟用 ob_start('ob_gzhandler') 或事後用 gzip 壓縮存儲,Nginx 直接發送 .gz。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved