静态化生成
本文详细介绍 CarefreeCMS 的静态化生成功能。
什么是静态化
静态化是将动态网页(需要 PHP 和数据库)转换为静态 HTML 文件的过程。
优势
- 性能提升:无需 PHP 解析,访问速度快 10-100 倍
- 降低负载:减少数据库查询,服务器压力小
- 更好的 SEO:搜索引擎更容易抓取和索引
- CDN 友好:静态文件更适合 CDN 分发
- 高并发支持:轻松应对大流量访问
原理
动态访问流程:
用户请求 → Nginx → PHP-FPM → 查询数据库 → 渲染模板 → 返回 HTML
静态访问流程:
用户请求 → Nginx → 直接返回 HTML 文件
配置静态化
基础配置
编辑 config/static.php:
return [
// 静态文件存储路径
'path' => root_path() . 'html/',
// 生成规则
'rules' => [
// 首页
'index' => [
'template' => 'index',
'file' => 'index.html',
],
// 文章列表
'article_list' => [
'template' => 'article/list',
'file' => 'article/index.html',
'paginate' => true,
],
// 文章详情
'article_detail' => [
'template' => 'article/detail',
'file' => 'article/{id}.html',
],
// 分类页
'category' => [
'template' => 'category/index',
'file' => 'category/{id}.html',
],
],
// 排除规则
'exclude' => [
'/admin/*',
'/api/*',
'/search',
],
];
Nginx 配置
配置静态优先访问:
server {
listen 80;
server_name example.com;
root /var/www/cms/html;
# 优先访问静态文件
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP 处理
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
# 静态资源
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
生成静态页
后台生成
进入后台 系统管理 → 静态化管理:
- 生成首页:点击"生成首页"按钮
- 生成文章列表:选择要生成的分类,点击"生成列表"
- 生成文章详情:选择文章,点击"生成详情"
- 全站生成:点击"全站生成",生成所有页面
命令行生成
# 生成首页
php think static:index
# 生成文章列表
php think static:article-list
# 生成指定文章
php think static:article 123
# 生成分类页
php think static:category 1
# 全站生成
php think static:all
# 指定数量
php think static:article-list --limit=100
自动生成
文章发布时自动生成
class ArticleController
{
public function save()
{
$article = Article::create(request()->post());
// 自动生成静态页
event('ArticleCreated', $article);
return json(['code' => 200, 'msg' => '保存成功']);
}
}
// 事件监听器
class ArticleCreatedListener
{
public function handle($article)
{
$static = new StaticService();
// 生成文章详情页
$static->generateArticle($article->id);
// 更新首页
$static->generateIndex();
// 更新列表页
$static->generateArticleList();
}
}
定时生成
// 定时任务
class CronJob
{
public function daily()
{
// 每天凌晨生成全站
(new StaticService())->generateAll();
}
}
Crontab 配置:
# 每天凌晨2点生成全站
0 2 * * * cd /var/www/cms && php think static:all >> /var/log/static.log 2>&1
生成逻辑
StaticService 实现
<?php
namespace app\service;
use think\facade\View;
use app\model\Article;
use app\model\Category;
class StaticService
{
protected $basePath;
public function __construct()
{
$this->basePath = config('static.path');
}
// 生成首页
public function generateIndex()
{
$data = [
'articles' => Article::where('status', 'published')
->order('create_time', 'desc')
->limit(10)
->select(),
'categories' => Category::select(),
];
$html = View::fetch('index', $data);
$this->saveFile('index.html', $html);
return true;
}
// 生成文章详情
public function generateArticle($id)
{
$article = Article::find($id);
if (!$article || $article->status !== 'published') {
return false;
}
$data = ['article' => $article];
$html = View::fetch('article/detail', $data);
$file = "article/{$id}.html";
$this->saveFile($file, $html);
return true;
}
// 生成文章列表
public function generateArticleList($page = 1, $pageSize = 20)
{
$articles = Article::where('status', 'published')
->order('create_time', 'desc')
->page($page, $pageSize)
->select();
$total = Article::where('status', 'published')->count();
$totalPages = ceil($total / $pageSize);
$data = [
'articles' => $articles,
'page' => $page,
'total_pages' => $totalPages,
];
$html = View::fetch('article/list', $data);
$file = $page === 1 ? 'article/index.html' : "article/page_{$page}.html";
$this->saveFile($file, $html);
// 递归生成其他页
if ($page < $totalPages) {
$this->generateArticleList($page + 1, $pageSize);
}
return true;
}
// 生成分类页
public function generateCategory($id, $page = 1)
{
$category = Category::find($id);
$articles = Article::where('category_id', $id)
->where('status', 'published')
->order('create_time', 'desc')
->page($page, 20)
->select();
$data = [
'category' => $category,
'articles' => $articles,
];
$html = View::fetch('category/index', $data);
$file = "category/{$id}.html";
if ($page > 1) {
$file = "category/{$id}/page_{$page}.html";
}
$this->saveFile($file, $html);
return true;
}
// 全站生成
public function generateAll()
{
// 生成首页
$this->generateIndex();
// 生成所有文章
$articles = Article::where('status', 'published')->column('id');
foreach ($articles as $id) {
$this->generateArticle($id);
}
// 生成文章列表
$this->generateArticleList();
// 生成所有分类
$categories = Category::column('id');
foreach ($categories as $id) {
$this->generateCategory($id);
}
return true;
}
// 保存文件
protected function saveFile($file, $content)
{
$filepath = $this->basePath . $file;
$dir = dirname($filepath);
// 创建目录
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 写入文件
file_put_contents($filepath, $content);
Log::info('生成静态页', ['file' => $file]);
}
// 删除静态文件
public function deleteFile($file)
{
$filepath = $this->basePath . $file;
if (file_exists($filepath)) {
unlink($filepath);
}
}
// 删除文章静态页
public function deleteArticle($id)
{
$this->deleteFile("article/{$id}.html");
}
// 清空所有静态文件
public function clear()
{
$this->deleteDirectory($this->basePath);
}
protected function deleteDirectory($dir)
{
if (!is_dir($dir)) {
return;
}
$files = glob($dir . '/*');
foreach ($files as $file) {
if (is_dir($file)) {
$this->deleteDirectory($file);
} else {
unlink($file);
}
}
rmdir($dir);
}
}
增量更新
智能更新
只更新变化的内容:
class IncrementalStatic
{
// 记录更新时间
protected function markUpdated($type, $id)
{
StaticLog::create([
'type' => $type,
'item_id' => $id,
'update_time' => time(),
]);
}
// 检查是否需要更新
protected function needUpdate($type, $id)
{
$log = StaticLog::where('type', $type)
->where('item_id', $id)
->find();
if (!$log) {
return true;
}
// 检查内容是否有更新
if ($type === 'article') {
$article = Article::find($id);
return $article->update_time > $log->update_time;
}
return false;
}
// 增量生成
public function incrementalGenerate()
{
// 查找需要更新的文章
$articles = Article::where('status', 'published')
->where('update_time', '>', $this->getLastGenerateTime())
->select();
foreach ($articles as $article) {
if ($this->needUpdate('article', $article->id)) {
(new StaticService())->generateArticle($article->id);
$this->markUpdated('article', $article->id);
}
}
}
protected function getLastGenerateTime()
{
return StaticLog::max('update_time') ?: 0;
}
}
更新策略
内容更新时
// 文章更新后
$article->save();
// 删除旧静态文件
(new StaticService())->deleteArticle($article->id);
// 生成新静态文件
(new StaticService())->generateArticle($article->id);
批量更新
# 只更新最近7天的文章
php think static:incremental --days=7
# 更新指定分类
php think static:category 1 --recursive
高级功能
多语言静态化
public function generateMultiLanguage($id)
{
$languages = ['zh-CN', 'en-US', 'ja-JP'];
foreach ($languages as $lang) {
app()->lang->setLangSet($lang);
$article = Article::find($id);
$html = View::fetch('article/detail', ['article' => $article]);
$file = "{$lang}/article/{$id}.html";
$this->saveFile($file, $html);
}
}
移动端适配
public function generateResponsive($id)
{
$article = Article::find($id);
// PC 版本
$html = View::fetch('article/detail', ['article' => $article]);
$this->saveFile("article/{$id}.html", $html);
// 移动版本
$mobileHtml = View::fetch('mobile/article/detail', ['article' => $article]);
$this->saveFile("m/article/{$id}.html", $mobileHtml);
}
预加载优化
在 HTML 中添加预加载:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{$article.title}</title>
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="/css/style.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<!-- 预连接 -->
<link rel="preconnect" href="https://api.example.com">
</head>
<body>
<!-- 内容 -->
</body>
</html>
缓存策略
HTTP 缓存头
protected function saveFile($file, $content)
{
// 添加 meta 标签
$cacheHeaders = <<<HTML
<meta http-equiv="Cache-Control" content="max-age=3600">
<meta http-equiv="Expires" content="{date('r', time() + 3600)}">
HTML;
$content = str_replace('</head>', $cacheHeaders . '</head>', $content);
file_put_contents($this->basePath . $file, $content);
}
ETag 支持
public function generateWithEtag($file, $content)
{
$etag = md5($content);
// 保存 ETag 信息
$meta = [
'etag' => $etag,
'size' => strlen($content),
'time' => time(),
];
file_put_contents($this->basePath . $file, $content);
file_put_contents($this->basePath . $file . '.meta', json_encode($meta));
}
监控与维护
生成日志
class StaticLog extends Model
{
public static function record($action, $data)
{
self::create([
'action' => $action,
'data' => json_encode($data),
'create_time' => time(),
]);
}
}
// 使用
StaticLog::record('generate_article', [
'id' => $id,
'file' => $file,
'size' => filesize($filepath),
'time' => microtime(true) - $startTime,
]);
完整性检查
# 检查静态文件完整性
php think static:check
# 修复缺失文件
php think static:repair
class StaticChecker
{
public function check()
{
$missing = [];
// 检查所有已发布文章
$articles = Article::where('status', 'published')->select();
foreach ($articles as $article) {
$file = config('static.path') . "article/{$article->id}.html";
if (!file_exists($file)) {
$missing[] = $article->id;
}
}
return $missing;
}
public function repair()
{
$missing = $this->check();
foreach ($missing as $id) {
(new StaticService())->generateArticle($id);
}
}
}
性能优化
并发生成
use Workerman\Worker;
class ParallelStatic
{
public function generateAll()
{
$articles = Article::where('status', 'published')->column('id');
// 分批处理
$chunks = array_chunk($articles, 100);
foreach ($chunks as $chunk) {
// 使用多进程
$this->processChunk($chunk);
}
}
protected function processChunk($ids)
{
$worker = new Worker();
$worker->count = 4; // 4个进程
$worker->onWorkerStart = function($worker) use ($ids) {
foreach ($ids as $id) {
(new StaticService())->generateArticle($id);
}
};
Worker::runAll();
}
}
压缩优化
public function saveFile($file, $content)
{
// HTML 压缩
$content = $this->compressHtml($content);
// Gzip 压缩
$gzContent = gzencode($content, 9);
// 同时保存普通版和压缩版
file_put_contents($this->basePath . $file, $content);
file_put_contents($this->basePath . $file . '.gz', $gzContent);
}
protected function compressHtml($html)
{
// 删除注释
$html = preg_replace('/<!--.*?-->/s', '', $html);
// 删除多余空白
$html = preg_replace('/\s+/', ' ', $html);
return trim($html);
}
最佳实践
生成时机
- 发布时生成:文章发布后立即生成
- 定时生成:每天凌晨全量生成
- 手动触发:重要更新时手动生成
目录结构
html/
├── index.html # 首页
├── article/
│ ├── index.html # 列表首页
│ ├── page_2.html # 列表第2页
│ ├── 1.html # 文章1
│ └── 2.html # 文章2
├── category/
│ ├── 1.html # 分类1
│ └── 2.html # 分类2
└── tag/
└── 1.html # 标签1
备份策略
生成前备份旧文件:
public function backup()
{
$backupDir = runtime_path() . 'static_backup/' . date('YmdHis') . '/';
// 复制整个目录
$this->copyDirectory($this->basePath, $backupDir);
}
故障排查
常见问题
问题:生成的文件无法访问
解决:
# 检查文件权限
chmod -R 755 /var/www/cms/html
# 检查所有者
chown -R www-data:www-data /var/www/cms/html
问题:静态文件未更新
解决:
# 清空所有静态文件
php think static:clear
# 重新生成
php think static:all
问题:生成速度慢
解决:
- 使用并发生成
- 优化模板渲染
- 减少数据库查询
