HTML to Elementor
返回教程

为什么写错的设置名会悄悄消失,却不报任何错误

几个月来,这个转换器一直有一个我看不到的 bug:它生成的每个盒阴影都被丢弃了。不是渲染错误——而是被直接抛弃。转换成功,导入成功,元素都到了,唯独阴影就是不存在。

Elementor 组控件如何展开成一个个控件 ID:注册名加上各字段,并与转换器原先写入的七个键对照——其中五个根本不存在。
控件 ID 是运行时由组名加各字段拼出来的,不出现在任何文件里,也就没法用 grep 搜到。这里的七个键有五个从未注册过——而 Elementor 丢弃未知键时不会报任何错。

没有异常。没有控制台警告。没有失败的请求。日志里什么都没有。如果不是你亲手搭建了那个页面,你根本不会知道少了什么;你只会觉得转换效果平平。

这件事值得写下来,因为这个问题的形态是普遍的。Elementor 只是其中一个具体例子。普遍的情况是:你写入的系统会忽略它不认识的键,于是一个拼写错误或一个过时的假设,就会变成永久而无声的数据丢失。

为什么它如此隐蔽

Elementor 将每个元素存储为 JSON——一个元素类型加上一个 settings 对象。渲染时,它会遍历它的已注册控件,并向 settings 对象请求每一个控件。与任何已注册控件都对不上的键根本不会被查询。它不会被拒绝,只是永远不会被读取。

这是一个合理的设计:旧数据能在插件更新后存留下来,新版本产生的未知键也不会破坏旧版本。但这也是为什么错误的键名会产生零反馈。如果 API 会做校验,写错的键在写入时就会报错,下一次跑测试就能发现。如果 API 会忽略未知键,那就只有沉默——用户也许几个月后才发现,也许永远发现不了。

为什么你不能简单地查找名称

显而易见的做法是对照文档检查控件名称。问题在于,其中很多名称在任何地方都没有记录——它们是生成的。

Elementor 有分组控件:像字体排版、边框、背景、盒阴影这样可复用的一组控件。你给一个小部件挂接一个分组并命名,各个控件 ID 在运行时组合而成——分组名、下划线,然后是每个字段名。因此,一个注册为 title 的排版分组会产生控件 title_font_size。没有任何文件里会出现那个字符串。你无法 grep 到它。你必须知道规则并应用它。

在这种情况下,凭记忆、论坛回答和旧博客文章,比什么都不参考还要糟:它们说得具体又笃定,却常常是错的,而 API 也不会纠正你。

这个 bug:阴影是一个控件,不是五个

转换器一直为一个阴影写入七个键——一个用来开启阴影,然后把颜色、水平偏移、垂直偏移、模糊、扩展和位置分别作为单独的值。看起来很周全,实际上几乎全错了。

盒阴影分组恰好注册了两个字段。阴影本身是一个复合控件,其值是一个单独的对象,同时包含水平、垂直、模糊、扩展和颜色;第二个字段是轮廓/内阴影位置。再加上弹出层开关,真正的键集是三个名称,而且都被加了两次前缀,因为这个分组本身名为 box_shadow。

在被写入的七个键中,有五个根本不存在。第六个名称正确,但被赋予了字符串 "yes",而不是它期望的对象。第七个很接近,但缺少一个前缀。

有一个细节是无论怎么猜都猜不出来的:“Outline” 的值是一个空格,不是空字符串。这一点只在 options 数组里,别处都没有。

第二个 bug:名称正确却仍然不起作用

同一轮审查还发现了 flex_grow 和 flex_shrink,它们被写在带有显式宽度的元素上,目的是阻止 flex 父级拉伸或挤压这些元素。又错了——这些控件位于一个以不同名称注册的分组中,因此它们需要一个前缀。

但修复前缀仍然不会起作用,而这正是值得记住的部分。这两个字段都带有一个条件:只有当分组的尺寸控件设置为 “custom” 时,它们才会生效。条件不仅控制面板可见性,也控制 CSS 生成。正确的键名是必要的,但并不充分。

实际的修复比任何一次尝试都简单,而且它一直就在源码里:尺寸控件有一个 “none” 选项,意思正是“既不增长,也不收缩”——一个无条件控件,替代三个有条件控件。读源码不仅修好了这个 bug,还得出了一个比原本打算的方案更好的实现。

审计其余部分

通过阅读发现了两个 bug。还有九十多个其他键没人读过。所以下一步是机械化的:解析官方插件源码,为每个小部件构建权威的控件名称集合,然后将其与转换器可能生成的每个键进行 diff。

审计脚本在给出正确答案之前,先给出了四个错误答案。每一次失败都有可以推而广之的教训:

  • 名称常常在下一行。控件注册经常把名称放在调用之后的那一行。单行模式会漏掉它们,于是每个小部件看起来都生成了五个无效键——整个“高级”选项卡都是如此。一个将标准控件标记为未知的检查器,不是报告 bug,而是在报告它自己的盲区。
  • 同一事物有两种写法。一个分组用某个变量名构建字段,另一个则用不同的变量名。只匹配第一个会静默地丢掉整个分组——颇具讽刺的是,其中正好包括正在调查的那个分组。
  • 参数数组会嵌套。在第一个右括号处停止匹配,会在第一个嵌套数组处截断,并完全跳过该注册。这就是为什么好几个完全有效的布局键最初显示为“无效”。
  • 过宽的捕获窗口会吞掉内容。为了解决上一点而抓取一个大窗口,意味着每个匹配都会吞掉它内部的任何注册。一个小部件的已知控件数量从 281 掉到 60,而 diff 仍然报告成功。

还有一个问题,专属于这个代码库:Button 小部件的文件里包含零个控件注册。它们全都放在一个 trait 里。第一次运行时,报告 Button 完全没有控件。

请注意这些错误的方向。其中三个产生了误报——很烦人,但能自我纠正:你去调查,发现是工具错了。有一个产生了误通过,这才是危险的那种。已知正确的名称越少,报告的问题就越少;这种失败模式是沉默且令人安心的。

让其他检查值得信赖的那项检查

这就引出了整个过程中唯一值得保留的想法。

一个报告“全部干净”的检查器,在你证明它能够报告“不干净”之前,一文不值。

因此,审计以阴性对照结束:它将五个已知错误的键名输入回去,如果其中任何一个被接受,就让整个运行失败。如果提取器之后坏了——模式变宽、文件被跳过、缓存损坏——这条断言会在“全部通过”那一行有机会撒谎之前就先报红。

与它配套的还有两个锚点:一份确定存在的控件名称列表必须全部被找到,而总数绝不能无声地减少。正是第二条,本可以抓住从 281 骤降到 60 的那次问题。

让修复持续有效

diff 结果全部干净后,最终状态是:在九种小部件类型上检查了 205 个键,没有一个是未知的。然后再加一个持续集成任务,让它一直保持这样——它会转换一个带阴影的页面,并对输出做断言:复合键必须存在,旧的那些分开的键名一个都不能再出现。

不是对纯函数的单元测试。而是对实际交付制品的断言,检查那个已经发生过一次的确切错误。

一般化版本

如果你写入的对象会忽略未知键——页面构建器、CMS 的元数据表、统计分析的上报数据、功能开关的配置数据,以及世上一半的配置格式——那么:

  • 从权威来源推导有效键集,而不是从记忆或文档。文档描述的是意图;生成的名称只存在于运行时。
  • 用程序把你输出的键与它逐一 diff。九十个键靠肉眼是审不过来的,而且错误的键看起来和正确的一模一样。
  • 首先怀疑你的检查器。当一次运行标记出你很确信的东西时,工具出错的概率远高于代码出错。
  • 加入阴性对照。证明检查器能够失败,否则它的通过毫无意义。
  • 在 CI 中对交付制品进行断言,因为一旦有人重构生成代码,这类 bug 就会卷土重来。

阴影现在正常了。更有价值的收获是意识到:几个月来,我们一直把“没有报错”当作结果正确的证据,而这个 API 从结构上就根本不可能报错。

作者:Leo

九年 WordPress 与 Elementor 经验,迁移过 189 个站点。这个转换器里的每条规则都来自真实项目。 关于这个工具