当主题模板没有过滤器:把「字数+阅读时间」并进它的 meta 行

当主题模板没有过滤器:把「字数+阅读时间」并进它的 meta 行-知集
当主题模板没有过滤器:把「字数+阅读时间」并进它的 meta 行
此内容为付费阅读,请付费后查看
¥1
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费阅读
已售 1
阅读约 41 分钟12,201 字

摘要:「给文章加一行字数统计」听起来是个 10 分钟的小功能,真正的难点不在统计,而在放哪里:主题文章头部那一行 meta 是模板里硬编码的,没有 apply_filters,你没法把自己的项插进去。改模板会在主题升级时丢,用 JS 整行重绘会把原样式全弄没。本文给出第三条路 —— 渐进增强:服务端照常渲染、客户端把子项逐个搬进去,搬不动就原地保留。
适合阅读:做子主题 / 插件、需要往父主题 UI 里”塞东西”的开发者。文末附零依赖交付代码与三层测试(44 项纯函数断言 + 11 项 DOM 断言 + 线上端到端)。

一、需求很小,坑很大

需求一句话:在文章里显示「阅读约 X 分钟 · 全文 N 字」。第一版很自然地做成了一颗胶囊,丢在正文开头 —— 能用,但看起来像是贴上去的:主题头部那一行已经有「评论 / 浏览 / 点赞」,我们的胶囊孤零零躺在正文里,风格不搭。

于是想把这两项并进头部那一行。然后撞上了第一堵墙。

二、卡点:那一行是硬编码的,没有过滤器

先做一件最该做的事 —— 去读父主题源码,确认到底有没有过滤器可用:

grep -n "function zib_get_posts_meta" inc/functions/zib-theme.php
# 结果:函数存在,但整段实现里**没有任何 apply_filters**,
#       meta 项是一个个字符串直接拼出来的 —— 无法从外部插入。

这时候只有三条路:

方案做法代价
改父主题模板直接编辑主题文件,把新项拼进那一行红线:升级即丢,且不允许改父主题
JS 整行重绘用脚本把整行 innerHTML 重新拼一遍原样式全丢(每个子项的 class 都带着主题的独立样式)、还会闪
渐进增强(本方案)服务端渲染自己的块;客户端把它的子项逐个搬进目标行几乎为零 —— 搬不进去就原地保留

第二条路的坑值得多说一句:那一行里的每个子项都带自己的 class,主题对它们有独立样式(字号、间距、图标尺寸)。你整块塞一个新元素进去,得到的是一个「块级陌生元素」,样式全失效 —— 位置对了,观感更差。

而逐个 appendChild 已有的子项,目标行对 item 的样式会自动生效,等于「借用」了主题已经调好的那一份样式。

三、方案:服务端渲染 + 客户端并入

3.1 服务端只做自己那部分,保持可独立使用

服务端把信息行渲染成一颗自带样式的胶囊,附一个标记属性:

<div class="zdw-reading-meta" data-zdw-move="1">
  <item class="meta-time">阅读约 33 分钟</item>
  <item class="meta-words">9,727 字</item>
</div>

注意两点:① 样式是自带的(不依赖主题),所以就算搬运失败也能独立显示;② 子项用的是 item 元素 —— 和父主题 meta 行里既有的子项同构,搬进去以后天然享受同一套样式。

3.2 客户端:搬子项,不搬外壳

var w = document.querySelector('.zdw-reading-meta[data-zdw-move]');
var t = document.querySelector('.post-metas');   // 目标行(可配置)
if (!w || !t) return;                            // 🧨 找不到目标 → 原地保留,功能不丢
while (w.firstChild) {
    if (w.firstChild.nodeType === 1) t.appendChild(w.firstChild);   // 只搬元素子项
    else w.removeChild(w.firstChild);            // 空白文本节点直接丢弃
}
w.parentNode.removeChild(w);                     // 最后移除外壳

细节:appendChild 会移动节点(不是复制),所以搬完原位置自然就空了;只搬 nodeType === 1 的元素、丢掉空白文本节点,避免往目标行里塞进一堆无意义的换行符。

3.3 为什么不会闪烁

这是很多人会质疑的一点:先用 CSS 藏起来再搬,不是必然闪一下吗?

这里根本不需要藏。关键在于 DOM 顺序:头部的 meta 行在 HTML 里早于正文输出,而这段脚本是随正文一起到达的 —— 脚本执行时,目标行早已存在。所以整段逻辑是同步执行的:没有 DOMContentLoaded 等待、没有 setTimeout、没有重排窗口。浏览器把这一帧画出来的时候,子项已经在目标行里了。

如果目标行是 AJAX 异步渲染的(有些主题的用户中心是这样),上面这个前提就不成立了 —— 那种场景需要 MutationObserver 等待,本方案的降级行为是「胶囊留在正文里」,功能不受影响。

四、字数到底怎么算(这里也有一个真 bug)

4.1 两种口径,差别很大

「字数」至少有两种常见口径:

口径算法一篇 800 词英文文章显示为
非空白字符数去掉空白后的字符长度(中文站点常用)约 6,000 字(明显不对)
中文按字 + 英文按词汉字与拉丁词分别计数再相加约 800 字

中英混排站点用第一种口径会闹笑话。所以交付代码默认用第二种,同时保留一个开关可以切回字符口径(老站点口径迁移时用得上)。

4.2 单测抓出来的真 bug:先删空白,再按词切

第一版实现是这样的(当时想的是「先把空白都去掉,好统计」):

$text = preg_replace('/s/', '', $text);      // 先删掉所有空白
$words = preg_match_all('/[A-Za-z0-9]+/', $text, $m);   // 再数英文词

纯函数单测直接把这个实现打回来了:"中文 abc def" 期望 4,实际得到 3;"a b cntd" 期望 4 个词,实际只数出 1 个。

根因很简单:删掉空白后,abc def 变成了 abcdef,三个词被粘成了一个。「先删空白」这个动作只在字符口径下是正确的,词口径下必须保留空白作为分隔。

修正后的管线是:「剥标签 → 解码实体 → 中文按字(直接全文匹配)→ 把中文替换成空格 → 剩下按词切」。

顺带一个必须记住的坑:preg_replace('/s/', ...) 里的 s 写成 /s/ 的话,删掉的是字母 s而不是空白符 —— 文本里所有 s 会被悄悄抹掉,字数统计无声变少。这个坑 v1 真踩过。

五、三层实测

交付代码用三层测试覆盖(纯计算 → 浏览器 DOM → 线上真实页面):

层覆盖内容规模结果
① 纯函数单测(PHP CLI,无需 WP)中英混排计数 / 空白符处理 / HTML 与实体 / 参数边界 / 千分位 / 输出结构 / 样式不写死背景 / 目标为空时不输出脚本22 项 × 2 口径44/44 通过
② 浏览器 DOM 测试(真实生成片段 + headless)未配目标→原地保留 / 命中目标→子项搬入且外壳移除 / 目标未命中→原地保留 / 重复执行幂等3 场景 11 断言全绿
③ 线上端到端(真实站点)文章页输出、首页与归档不输出;真实浏览器里子项确实落进 .post-metas、外壳残留 0;卸载后无残留3 页 + 1 次 DOM 抓取全绿

第 ② 层的做法值得一提:不手抄 JS,而是先用本地 PHP 生成真实输出片段,再注入浏览器。这样测的就是交付代码的真实产物,而不是我抄错一版。

六、踩坑记录

#坑现象根因 / 解法
1重复给核心类加同名方法Fatal:Cannot redeclare Zhiji_Adapter::svg()辅助方法已在别处定义过。加方法/函数前先 grep -n "function xxx"
2用 in_the_loop() 判断”是否正文”正常文章被整体挡掉,信息行不显示某些主题用模板直接渲染正文,该值为恒 false。只判 is_singular('post'),再排除 feed/preview
3搬运脚本重复执行子项被搬两次(出现两组字数)脚本要自带执行标记(window.__zdwRmMoved),保证幂等
4只删空白导致英文词粘连4 个英文词被数成 1 个词口径下不能先删空白;字符口径下才需要
5样式里写死背景色深色主题下胶囊变成一块白底用 currentColor / color-mix 派生,不写死颜色

另外提醒一句:如果主题本身已经有「字数 + 阅读时间」,同时开启会显示两组。接入前先确认,二选一。

附:测试方法

这套功能有两块逻辑,要用两种测试手段,别指望一种覆盖:

① 纯计算(字数/阅读时间)—— 用 PHP CLI 单测,不需要 WordPress
   把计算写成纯函数(输入文本 / 输出数组),再 define 几个桩函数(add_filter 等)就能跑
   ⚠️ 关键:把边界值都写进断言(空文本 / 速度 0 / 中英混排 / HTML 实体 / 只有符号)

② DOM 搬运 —— 用无头浏览器,且**不要手抄 JS**
   先用本地 PHP 生成真实输出片段(HTML + 脚本)→ 写进测试页 → chrome --dump-dom 读回断言
   断言要点:搬之前的目标行状态、搬之后的子项数、外壳是否移除、原有哪些子项还在不在

第 ② 层最容易犯的错是脚本执行顺序:真实页面里搬运脚本随正文先到、断言脚本必须在它之后跑,顺序反过来就会得到「没搬成功」的假失败(我第一版就栽在这里)。


📚 zibll 子主题开发实录(共 22 篇) · 系列总目录与阅读路径 →

① 前端性能与加载

② 排版与样式系统

③ 功能实现与边界

④ 系统与运维

⑤ 成就与活动系统

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容