开发错误记录
00 - 开发错误记录
每次新开发任务前先读本文档,避免重复踩坑;按需去README.md索引过往开发文档。
每次开发收尾阶段,都应主动维护本文档及开发文档,用于项目的可持续性。
E01 · Widget 调用必须在 header.php 之后
错误现象:页面导航栏缺少部分菜单项(如"我的动态""网站维护"消失,只剩"首页"和最后一个页面)。
根因:Widget\Contents\Page\Rows::alloc() 是共享实例,迭代器一旦消耗不会自动重置。在 header.php 之前调用并遍历该 widget,会导致 header.php 拿到已消耗的实例,无法正确构建导航。
规则:模板文件顶部只做文件系统操作(glob、file_get_contents 等),所有 Widget::alloc() 调用必须放在 $this->need('header.php') 之后。
E03 · <nav> 标签会被 pico.css 附加导航样式
错误现象:面包屑导航渲染后看起来像一个完整的导航栏(字体、间距与顶部 site-nav 相同)。
根因:pico.css 对 <nav> 标签有全局样式规则,凡是 <nav> 都会被当作导航组件处理。
规则:非导航功能的元素(面包屑、提示文字等)使用 <p> 或 <div>,不要用 <nav>。
E04 · 服务端环境问题不要在代码层面绕行
错误现象:时区显示 UTC 而非 CST,在 PHP 代码中用 DateTimeZone('Asia/Shanghai') 修复,方向错误,后被回滚。
根因:问题根因是服务器 php.ini 未配置 date.timezone,属于环境问题,不应在业务代码中用 DateTimeZone 绕行。
规则:遇到时区/编码/扩展缺失等问题,先确认是环境配置问题还是代码问题,环境问题直接改配置文件(如 php.ini),不要在代码里打补丁。
E05 · Typecho 模板名来自 docblock 第一行,非 Template Name 注释
错误现象:误以为 Typecho 读取 Template Name: xxx 作为后台下拉显示的模板名。
根因:Plugin::parseInfo() 只解析 docblock 中第一段非 @ 标签文字作为 description(即模板名),Template Name: 行位于 @package 之后,实际上被忽略。
规则:自定义模板的显示名称 = docblock 第一行文字(@package custom 之前的描述行)。Template Name: 注释无实际作用,可省略或仅作说明用途保留。
E06 · pico.css <ul> 默认样式导致自定义列表出现 bullet point
错误现象:自定义下拉菜单(<ul> + <li>)每项前面出现黑色方块或圆点,即使已设 list-style: none。
根因:pico.css 对 <ul> 默认施加 padding-left,且部分情况下 list-style 的继承不够彻底。只在 <ul> 上设 list-style: none 不足以完全清除标记,还需同时清零 padding: 0,并在 <li> 上也显式设 list-style: none。
规则:自定义 <ul> 组件必须同时设置:
ul.custom { list-style: none; padding: 0; margin: 0; }
ul.custom li { list-style: none; padding: 0; margin: 0; }E07 · 下拉浮层不要用 CSS :hover 或 <details>
错误现象:
:hover方案:鼠标从触发按钮向下移动时面板闪烁消失(经过 1–2px 缝隙即触发 hover 解除)<details>方案:点击展开后导航栏整体高度暴增——浏览器计算<details>高度时包含了position: absolute子内容的自然高度,不遵循"绝对定位不贡献父高"的 CSS 规则
规则:浮层/下拉菜单一律用 click 切换 + 点击外部关闭 方案:
btn.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('open');
});
document.addEventListener('click', () => dropdown.classList.remove('open'));子菜单默认 display: none,JS 切换 .open class 展开,配合 position: absolute 浮层,完全脱离文档流。
E08 · theme.css 仅在 customize 配色方案下加载
错误现象:在 theme.css 中添加的全局样式规则(如 body > main { padding-top } 间距调整)在深色模式或其他配色方案下完全不生效。
根因:header.php 中 theme.css 的加载受条件保护:
<?php if ($this->options->colorSchema == 'customize'): ?>
<link rel="stylesheet" href="...theme.css">
<?php endif; ?>非 customize 方案下该文件根本不请求,所有规则对用户不可见。
规则:全局通用的样式覆盖(body 级别、layout 调整、spacing 修改等)必须写在 header.php 内联 <style> 块中,该块对所有配色方案始终加载。theme.css 只放与 customize 主题色直接相关的变量覆盖(--pico-primary 等)。
E09 · pico.css 对 <a> 颜色处理:--pico-color 在 <a> 作用域内被重写为主色
错误现象:
- 浮层下拉内
<a>设color: inherit→ 继承到导航栏的--pico-primary-inverse(白色),深/亮色模式均不可见 - 改为
color: var(--pico-color) !important后文字变蓝(主色),仍非预期的正文灰
根因:pico 全局规则 :where(a:not([role=button])) { --pico-color: var(--pico-primary); } 将 --pico-color 在每个 <a> 作用域内重写为主色。color: var(--pico-color) 写在 <a> 上 = 蓝色;color: inherit 则顺着 DOM 继承到导航栏白色。
规则:在 <a> 的父容器(<ul>/<div>)上设 color: var(--pico-color)(此时解析为正确文本色),子 <a> 用 color: inherit !important 继承,绕过 pico 的变量重写:
.nav-sub-menu { color: var(--pico-color); }
.nav-sub-menu li a { color: inherit !important; }E12 · iOS Safari 点击输入框自动缩放页面
错误现象:在 iOS Safari 中点击 <input>、<select> 或 <textarea> 后,页面自动放大,导致布局溢出或内容显示不全。
根因:iOS Safari 内置行为:当表单元素的 font-size 小于 16px 时,浏览器认为文字过小,点击聚焦时自动缩放视口以提升可读性。只要任意表单元素的计算字号(含继承)低于 16px 即会触发,与 viewport meta 设置无关(除非使用 maximum-scale=1,但该方式会完全禁止用户手动缩放,损害可访问性,不推荐)。
规则:在 theme.css 中已添加全局媒体查询,移动端强制表单元素字号 ≥ 16px:
@media (max-width: 768px) {
input, select, textarea {
font-size: 1rem !important;
}
}此规则已写入 theme.css,所有页面自动受益。新增页面无需单独处理。
E13 · dirname(__DIR__, N) 回溯层级算错导致共享模块路径偏移
错误现象:封面缓存重构为共享模块 usr/themes/classic-22/inc/cover-cache.php 后,轻小说查询页所有封面变为 📚 占位符(novel-cover.php 返回 500);EPUB 转换器每次转换都重新下载封面,缓存始终未命中(静默性能退化)。
根因:在 cover-cache.php 内用 dirname(__DIR__, 3) 计算项目根目录,少回了一级:
__DIR__ = {ROOT}/usr/themes/classic-22/inc
dirname(…, 3) = {ROOT}/usr ❌
dirname(…, 4) = {ROOT} ✅规则:被 require 的共享模块不要自己用 __DIR__ 推算项目根——更稳的做法是让调用方(本身就在根目录的文件)显式传入路径常量;非要用时必须逐级数清楚并在注释中写明推导(inc → classic-22 → themes → usr → {ROOT},共 4 级)。
E14 · 删除 inline JS 函数时误删相邻函数的闭合花括号
错误现象:轻小说查询页加载后,点击「查询」按钮完全无反应(Network 无任何请求发出),页面其他交互也全部失灵。后端 API 经 curl 验证完全正常。
根因:移除 toggleIntro() 函数时,edit_file 的 old_string 范围不精确,误将相邻 loadRating() 函数的闭合 } 一并吞掉,导致整个 <script> 块 parse error,所有顶层函数和事件绑定全部失效。PHP php -l 无法发现此问题,因为错误在 inline JS 内。
规则:
- 删除 JS 函数时,
old_string必须从function xxx(一直精确包到结尾},不要切半个函数边界。 - 改完 inline JS 后提取
<script>块做一次语法校验(node --check),或用浏览器 DevTools Console 实测——这是唯一零误报的检查手段。
E15 · 多接口聚合缓存的"部分成功即写入"陷阱
错误现象:个人动态页 Steam 模块某天上午有内容,当天稍后变为完全空白(「无法获取 Steam 状态,请稍后刷新重试」)。刷新无效,且会持续 6 小时(短缓存有效期);短缓存过期后仍然空白(fallback 文件也已被污染)。
根因:getSteamData() 聚合三个子接口的数据后,用 OR 判断是否有数据:
$player = fetchSteamPlayerSummary(); // 渲染硬依赖,为 null 则直接 return 错误提示
$games = fetchSteamRecentGames();
$badges = fetchSteamBadges();
$hasData = $player !== null || !empty($games); // ❌ OR 聚合
if ($hasData) {
_activity_write_cache($cacheFile, $data); // 污染短缓存
_activity_write_cache($fallbackFile, $data); // 连兜底也污染
}当 fetchSteamPlayerSummary() 临时失败而 GetRecentlyPlayedGames 成功时,$hasData = true 但 player = null,写入的坏数据会同时污染短缓存和 fallback。
规则:
- 写缓存的判据必须是"渲染层硬依赖字段":
$hasData = $player !== null才写,可选字段缺失可降级但不阻止写入。 - 短缓存和 fallback 用不同写入条件:短缓存接受主字段成功的半成品(避免重复请求);fallback 要求所有非可选字段均成功。Steam 示例:
$hasData = $player !== null(写短缓存)、$hasGoodFallback = $player !== null && $badges !== null(写 fallback)。 - 修完代码后必须手动删除已被污染的缓存和 fallback 文件,否则修复不会立即生效。
E16 · 根目录自定义 API 端点调 Typecho 登录态,必须先 \Widget\Init::alloc()
错误现象:已登录的管理员访问自建 API(如 /novel-rating-override-api.php)时,\Widget\User::alloc()->hasLogin() 恒为 false,接口返回 401。
根因:Typecho 的登录态以 Cookie 形式存在,Cookie 名带前缀 md5(siteUrl)(见 var/Typecho/Cookie.php::setPrefix)。这个前缀的设置由 \Widget\Init::alloc() 在初始化阶段完成(读 Widget_Options 的 siteUrl 后调 Cookie::setPrefix)。自定义 API 若只 require config.inc.php,只加载了常量 + autoload,没执行 Init,Cookie::$prefix 是空的 → Cookie::get('__typecho_uid') 找不到浏览器里那个带前缀的 Cookie → hasLogin() 直接返回 false。
规则:任何需要读 Typecho 登录态、配置、插件状态的根目录 PHP 端点,require config.inc.php 之后必须紧跟 \Widget\Init::alloc();只读 SQLite 或代理外部 API 的端点无需初始化 Init。
require_once __DIR__ . '/config.inc.php';
\Widget\Init::alloc(); // ← 关键
$user = \Widget\User::alloc();
if (!$user->hasLogin()) { /* 401 */ }E17 · pico.css 对 input/select/textarea 的 margin-bottom 不会应用于 div 自定义控件
错误现象:搜索表单在手机端,"连载状态"选择器与"标签"选择器之间的间距,明显小于其他表单字段之间的间距(目测约 0.4rem vs 1.4rem)。
根因:pico.css 有全局规则:
input:not([type=checkbox],[type=radio]), select, textarea {
margin-bottom: var(--pico-spacing); /* 1rem */
}这条规则只覆盖原生表单元素。.status-toggle(连载状态切换器)是 <div>,不在选择器范围内,margin-bottom = 0。结果:
- 其他字段(书名/作者
<input>、文库<select>):1rem margin-bottom + 0.4rem flex/grid gap = 1.4rem 视觉间距 - 连载状态(
.status-toggle <div>):0 + 0.4rem gap = 0.4rem 视觉间距,差了整整 1rem
注意:同一文件早已有同类补丁(margin-top),因为 label > :where(input,select,textarea) { margin-top: 0.25rem } 也不覆盖 div,已手动补齐。但 margin-bottom 那条规则被遗漏。
规则:用 <div> 仿造表单控件时(toggle、自定义 select 等),检查 pico 对原生 input/select/textarea 施加的所有 margin/padding 规则,逐一手动补到 div 上:
.custom-control {
margin-top: calc(var(--pico-spacing) * 0.25); /* 对应 label > :where(input,select) */
margin-bottom: var(--pico-spacing); /* 对应 input:not(...),select,textarea */
}