本文要点
- Typecho 默认的 markdown 解析器(Parsedown)不识别
<details>为块级元素,会往里面插入<br>/</p>,导致折叠块失效; - Inaline 主题正文渲染直接调
Utils\Markdown::convert、绕过了 Typecho 的 filter,插件挂在标准钩子上拦不住,只能靠主题补丁; - 自动更新存在三个连环坑:更新器不更新自身、更新请求内旧类已加载、旧逻辑清空激活状态——最终用"写标记 + 下次加载重建钩子"解决;
- EdgeOne 对静态资源缓存 7 天,改 CSS/图片后必须换文件名或加版本参数,否则用户一直看到旧版。
背景
开发 TeoSeo v1.3.0 的过程中,踩了不少 Typecho 1.3 特有的坑。有些是 Typecho 1.3 新架构带来的,有些是 CDN 和自动更新的隐藏行为。写下来供同样在 Typecho 1.3 上做插件/主题的人参考。
坑一:Parsedown 不认 <details> 是块级元素
要给文章加"本文要点"折叠块,最自然的做法是用原生 <details>/<summary>。但 Typecho 1.3 的 markdown 渲染(Parsedown)不认识 <details>,渲染时会在内部插入 <br> 和 </p>:
<!-- 期望 -->
<details><summary>本文要点</summary><ul><li>...</li></ul></details>
<!-- 实际渲染 -->
<details><br><summary>本文要点</summary></p><ul>...</ul></details><summary> 不再是 <details> 的第一个子元素,折叠功能和样式全部失效。解法是在 markdown 渲染前用占位符保护 <details> 块,渲染后再还原。
坑二:主题绕过 Typecho 的 markdown filter
Typecho 标准渲染走 Contents::markdown(),插件可以挂 Widget_Abstract_Contents:markdown filter 做保护。但 Inaline 主题的正文渲染直接调用 Utils\Markdown::convert,完全绕过了 filter(用探针验证过,钩子根本没被调用)。
所以对 Inaline,插件在标准钩子上保护 details 是无效的,只能在主题层打补丁(MarkdownParser::parseMarkdown() 里做占位符保护)。这也是 v1.3.0 需要"主题适配补丁"的原因。
坑三:EdgeOne 静态缓存 7 天
给折叠块加 CSS 后,怎么改页面都没变化——EdgeOne 对静态资源缓存 7 天,浏览器/用户命中的是 CDN 缓存的旧 CSS。而且本机 curl 走代理/直连命中的可能是不同节点(拿到新版),造成"源站对、用户错"的假象。
解法:改 CSS/图片后换文件名或加版本参数(如 markdown.css?v=20260815),新 URL 无缓存强制回源。
坑四:更新器不更新自身
自动更新逻辑在 Update.php 里,但更新时只覆盖插件核心文件,Update.php 不在覆盖清单里——更新器不会更新自身(避免更新中断)。后果:想通过修改 Update.php 修复自动更新逻辑,永远无法通过自动更新传播;修复必须放在会被覆盖的文件(如 Plugin.php)里。
坑五:更新请求内旧类已加载,钩子重建失败
自动更新在同一个请求里完成:请求开始时 Plugin.php 类已加载(旧版),覆盖文件后调用 activate() 用的还是内存里的旧类——所以"更新后重新激活"这招不可靠。旧版 Update.php 的重启逻辑大概长这样:
// 旧 Update.php: 更新后"重启插件"
$db = \Typecho\Db::get();
$plugins = \Typecho\Plugin::export();
unset($plugins['activated']['TeoSeo']); // ① 清空激活状态
$db->query($db->update('table.options') // ② 写回: 残留旧 handles
->rows(array('value' => json_encode($plugins)))
->where('name = ?', 'plugins'));
\TeoSeo_Plugin::deactivate();
\TeoSeo_Plugin::activate(); // ③ 用的是请求开始就已加载的旧类, 没有新钩子① 清空了激活状态、② 又残留旧钩子、③ 用的是旧类——三者叠加,更新后设置页直接 500。
最终解法是标记机制:更新请求内不碰数据库插件状态,只写一个"待重建钩子"标记:
// 新版 Update.php: 更新完成后写"待重建钩子"标记, 不碰 plugins 表
$db = \Typecho\Db::get();
$pending = $db->fetchRow($db->select('value')->from('table.options')
->where('name = ?', 'teoseo_pending_reactivate'));
if ($pending) {
$db->query($db->update('table.options')->rows(array('value' => '1'))
->where('name = ?', 'teoseo_pending_reactivate'));
} else {
$db->query($db->insert('table.options')
->rows(array('name' => 'teoseo_pending_reactivate', 'user' => 0, 'value' => '1')));
}下一次任何请求加载新版 Plugin.php 时,文件顶层检测到标记,用"正确升级法"重建钩子:
// 新版 Plugin.php 文件末尾: 检测标记 → 重建钩子 → 清除标记
if (defined('__TYPECHO_ROOT_DIR__') && 'cli' !== PHP_SAPI) {
try {
$db = \Typecho\Db::get();
$pending = $db->fetchRow($db->select('value')->from('table.options')
->where('name = ?', 'teoseo_pending_reactivate'));
if ($pending && '1' === (string) $pending['value']) {
\Typecho\Plugin::init(array('handles' => array(), 'activated' => array()));
$db->query($db->update('table.options')
->rows(array('value' => json_encode(array('handles' => array(), 'activated' => array()))))
->where('name = ?', 'plugins'));
\TeoSeo_Plugin::activate(); // 此时用的是新类, 注册全部新钩子
\Typecho\Plugin::activate('TeoSeo');
$db->query($db->update('table.options')
->rows(array('value' => json_encode(\Typecho\Plugin::export())))
->where('name = ?', 'plugins'));
$db->query($db->delete('table.options')->where('name = ?', 'teoseo_pending_reactivate'));
}
} catch (\Throwable $e) {
error_log('[TeoSeo] pending reactivate failed: ' . $e->getMessage());
}
}注意这要求 Update.php 和 Plugin.php 都已经是新版——v1.2.x 的旧 Update.php 没有写标记的能力,所以 v1.2.x 升级到 v1.3.0 只能手动安装(删除后重装)。
坑六:钩子注册必须写进 activate()
GEO 钩子(headerOptions/excerpt/markdown)最早是手动加到 options.plugins 的——结果一重新激活/自动更新重建就丢失。所有钩子必须在 activate() 里通过 \Typecho\Plugin::factory(...) 注册,才能在任何重建流程中被恢复。
小结
Typecho 1.3 的插件开发有几个和传统 PHP 插件不太一样的点:markdown 渲染链路可能被主题绕过、自动更新受"类已加载"和"自身不更新"限制、CDN 缓存会掩盖问题。把钩子全部收进 activate()、用标记机制处理更新、改资源记得换版本号,能少踩很多坑。
评论 (0)
暂无评论,快来抢沙发吧!