从静态 HTML 到 PHP SSR 的重构之路
Think Card 是一套为个人站长、独立开发者、小型工作室打造的轻量级建站系统。 它同时兼顾了「内容发布」和「商品变现」两条主线:用 Copy 卡片 + 文章 + 排行 + 链接卡片组织内容, 用商品 + 订单 + 提货码完成虚拟商品的售卖与自动交付,用全站配色 + 栏目 + 底部自定义 + SEO 设置完成站点个性化。
整个系统没有使用任何后端框架、没有 Composer 依赖、没有前端打包流程, 只需要一台支持 PHP 的服务器即可运行。数据存储采用 SQLite 单文件数据库,备份只需复制一个文件。
PHP 7.4+ SQLite 单文件 零依赖 · 零构建 主题配色系统 内容 + 商城一体 全响应式
Think Card 最初的架构是「静态 HTML + 前端 JS 渲染」:页面本身是空壳,浏览器加载后由 JS 请求 api.php 获取数据,
再拼接到页面中。这种架构开发简单,但在实际运行中暴露了三个越来越明显的问题。
每次刷新页面,标题、导航、内容都是异步加载的。用户会先看到一片空白,然后内容“跳”出来。 即使后来加了 localStorage 缓存方案,依然无法彻底消除闪烁——尤其是用户清空浏览器缓存或换了设备之后。
Google 虽然能执行 JS,但百度、微信分享预览、QQ 空间等场景对 JS 渲染几乎完全忽略。 页面源代码里是空的,搜录效果很差。服务端渲染(SSR)可以解决这个问题,让爬虫直接索引到完整的 HTML 内容[reference:0]。
每个前台页面都需要一个独立的 JS 文件来获取设置、渲染导航、渲染内容、处理分页。
applySiteSettings、renderNavMenu 这些函数在十几个文件里反复出现。
改一处需求,要同步修改十几个地方。
既然服务器上已经有 PHP 和 SQLite,为什么不直接在 PHP 里把数据拼进 HTML 再返回呢? 这就是 PHP SSR 的核心思想——服务器直接输出完整的 HTML 页面,浏览器只负责展示和交互[reference:1]。
这也是 SSR 最朴素的价值:更快的首次有效绘制时间、更好的 SEO、更简单的架构[reference:2]。
articles.htmlarticles.php| 指标 | 改造前 | 改造后 |
|---|---|---|
| 首页请求数 | 4 次(HTML + 3 API) | 1 次 |
| 首屏白屏时间 | 约 300–800ms | < 50ms |
| 页面闪烁 | 明显 | 无 |
| 百度/微信收录 | ❌ 抓不到内容 | ✅ 完整收录 |
| 无 JS 环境 | 页面空白 | ✅ 正常显示 |
| 前台 JS 文件数量 | 13 个 | 1 个(app.js) |
| 缓存一致性 | 需手动清 localStorage | 不需要 |
app.js,负责搜索浮层、Toast 提示、复制等纯交互功能。
所有数据渲染都交给了 PHP。
改造后,每个页面的结构都非常统一,只需要关注「内容区」:
<?php
require_once __DIR__ . '/inc/bootstrap.php';
// 1. 查数据
$items = $pdo->query("SELECT * FROM ...")->fetchAll();
// 2. 分页
list($page_items, $page, $total_pages) = paginate($items, 20);
?>
<!DOCTYPE html>
<html>
<head>
<?php $page_title = '页面标题'; include 'inc/head.php'; ?>
</head>
<body>
<div class="container">
<?php $active_nav = 'articles.html'; include 'inc/header.php'; ?>
<!-- 内容区:PHP 直接循环输出 -->
<?php foreach ($page_items as $it): ?>
<div class="list-item"><?= h($it['title']) ?></div>
<?php endforeach; ?>
<?php render_pagination($page, $total_pages, 'articles.html'); ?>
</div>
<?php include 'inc/footer.php'; ?>
</body>
</html>
inc/bootstrap.php 里集中定义了所有页面都会用到的工具函数:
| 函数 | 用途 |
|---|---|
h($s) | HTML 转义(所有输出到页面的数据都必须用它) |
setting($key, $default) | 读取全站设置 |
sanitize_html($html) | 富文本净化,保留排版标签,去掉危险内容 |
render_breadcrumb($items) | 面包屑导航渲染 |
render_pagination($cur, $total, $base) | 服务端分页器 |
paginate($items, $size) | 数组分页切片 |
url_article($id) 等 | 详情页 URL 生成(统一入口,方便以后改) |
为了让旧的 articles.html 地址继续可用,加了重写规则
RewriteRule ^([a-z0-9_-]+)\.html$ $1.php [L]。
结果 pay.html 也被映射成了 pay.php(不存在),直接 404。
修复:在通用规则之前加一条排除:
RewriteRule ^(pay|pickup|admin|edit_article|edit_ranking)\.html$ - [L]
list($a, $b, $c) = paginate(...) 这种写法在 PHP 7.0 及以下会报语法错误。
如果你还在用旧版 PHP,把所有的 [$a, $b, $c] = ... 改成 list($a, $b, $c) = ... 即可。
这两个页面保持静态没做 SSR,但它们的 JS 依赖 theme.js 和 breadcrumb.js。
清理旧文件时容易误删。保留这两个文件即可,它们只被这两个页面引用,不影响 SSR 页面的性能。
栏目表里存的是 articles.php,但页面里写的是 articles.html,严格字符串比较匹配失败。
改成去掉 .html / .php 后缀再比较就兼容了。
SSR 负责数据渲染,但页面的交互性还需要 JS。改造后前台只保留一个 app.js,包含三类功能:
search.php?q=xxx。
此外,每个页面底部可能还有少量内联脚本,比如首页的轮播控制、排行详情的折叠展开、商品详情的副图切换。 这些都是几十行的纯交互代码,不需要单独抽成文件。
| 模块 | 说明 |
|---|---|
首页 index.php | 瀑布流展示全部内容(卡片 + 文章混排),支持分页、置顶、角标 |
| 顶部轮播 | 两栏布局:左侧图片轮播,右侧「冷门推荐」列表 |
| Copy 卡片详情 | 封面 + 富文本内容介绍 + 提示词只读文本框,一键复制 |
| 下载详情 | 支持「普通链接 / 下载链接 / 本地文件仓库」三种类型 |
| 文章列表 / 详情 | 分类筛选、分页、置顶、角标,富文本渲染并做 HTML 净化 |
| 链接汇总 | 链接卡片 + Copy 卡片混排,支持按分类筛选 |
| 排行列表 / 详情 | 条目可折叠展开,标题支持自定义颜色 / 粗细 / 字号 |
| 全部内容 | 卡片 + 文章混排的完整归档页 |
| 商城 / 商品详情 | 商品网格(每行数量可配置)、多副图切换、库存、优惠价划线 |
| 支付页 | 多收款二维码切换、下单后弹出提货码弹窗 |
| 提货中心 | 输入 16 位提货码自动核销:订单码直接出结果,活动码进入多选界面 |
| 搜索 | 全站搜索(文章 / 排行 / 商品 / 卡片),关键词高亮 |
| 面包屑 | 全站统一面包屑导航,栏目名自动从「栏目管理」读取 |
| 主题配色 | 8 个 CSS 变量由后台配置,PHP 直出到 <style> 中,无闪烁 |
| 模块 | 说明 |
|---|---|
| 网站信息 | 站点标题、版本号、文章数 / Copy 卡片数 / 链接卡片数统计 |
| 内容管理 | 四个子标签:Copy 卡片、链接卡片、文章管理、排行管理 |
| 栏目管理 | 导航栏栏目自定义(名称、地址、排序、是否新窗口打开) |
| 文件仓库 | 文件分类 + 文件上传(白名单 + MIME 检测) |
| 商品管理 | 商品、订单、货物、提货码、数据统计 |
| 全站设置 | 基本设置 / 首页设置 / 顶部轮播 / URL 伪静态 / 网站配色 / 底部设置 |
| 账号管理 | 修改密码、修改用户名、多管理员管理 |
data.sqlite 所在目录可写admin.html,使用默认账号 admin / admin123 登录如果希望旧的 .html 地址继续可用,添加以下重写规则:
Apache(.htaccess)
RewriteEngine On
# 真实文件/目录直接放行
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# 保留 .html 的页面
RewriteRule ^(pay|pickup|admin|edit_article|edit_ranking)\.html$ - [L]
# 详情页伪静态
RewriteRule ^article-([0-9A-Za-z_\-]+)\.html$ article.php?id=$1 [L,QSA]
RewriteRule ^copy-([0-9A-Za-z_\-]+)\.html$ copy.php?id=$1 [L,QSA]
RewriteRule ^download-([0-9A-Za-z_\-]+)\.html$ download.php?id=$1 [L,QSA]
RewriteRule ^ranking-([0-9A-Za-z_\-]+)\.html$ rankingview.php?id=$1 [L,QSA]
RewriteRule ^product-([0-9A-Za-z_\-]+)\.html$ productpage.php?id=$1 [L,QSA]
# 列表页 .html → .php
RewriteRule ^([a-z0-9_-]+)\.html$ $1.php [L,QSA]
Nginx
location / {
try_files $uri $uri/ @php;
}
location @php {
rewrite ^/article-([0-9A-Za-z_-]+)\.html$ /article.php?id=$1 last;
rewrite ^/copy-([0-9A-Za-z_-]+)\.html$ /copy.php?id=$1 last;
rewrite ^/download-([0-9A-Za-z_-]+)\.html$ /download.php?id=$1 last;
rewrite ^/ranking-([0-9A-Za-z_-]+)\.html$ /rankingview.php?id=$1 last;
rewrite ^/product-([0-9A-Za-z_-]+)\.html$ /productpage.php?id=$1 last;
rewrite ^/([a-z0-9_-]+)\.html$ /$1.php last;
return 404;
}
try_files $uri $uri/ @php; 配置,
pay.html 等静态文件会直接返回,不需要额外排除规则。上面的 Apache 配置才需要排除。
从静态 HTML + JS 渲染改到 PHP SSR,核心就一句话: 让服务器把数据拼进 HTML,浏览器只负责展示和交互。
改造后带来的收益:
对于那些「内容型」页面(首页、列表、详情),SSR 是更自然的选择。 而对于强交互页面(支付、提货、后台),保持静态 HTML + AJAX 反而更简单。 两者结合,才是最适合个人站长和小型项目的技术方案。