暗色模式

Typecho 1.3 插件踩坑:details 折叠块、自动更新与钩子重建

技术教程
2026-08-11
5
0
本文要点
  • 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>

<!-- 期望 -->
&lt;details&gt;&lt;summary&gt;本文要点&lt;/summary&gt;&lt;ul&gt;&lt;li&gt;...&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;

<!-- 实际渲染 -->
&lt;details&gt;&lt;br&gt;&lt;summary&gt;本文要点&lt;/summary&gt;&lt;/p&gt;&lt;ul&gt;...&lt;/ul&gt;&lt;/details&gt;

<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.phpPlugin.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()、用标记机制处理更新、改资源记得换版本号,能少踩很多坑。

发表评论

暂无评论,快来抢沙发吧!