Caputchin
参考

组件方法与事件

Caputchin 组件交付两个自定义元素,而你的页面以两个方向和它们交互:你在元素上 调用方法 来驱动它,并 监听 它发出的 事件 来作出反应。这一页是这两个面的详尽参考。

每个事件都是一个以 bubbles: truecomposed: true 派发的 CustomEvent,所以它逃出组件的 shadow DOM,你可以直接在元素上(或在任何祖先上)监听它。数据始终在 event.detail 上。

两个元素一览

元素用途方法
<caputchin-widget>只验证(proof-of-work / 复选框)。start()
<caputchin-game>游戏宿主,带可选验证。pass()fail()

复选框组件只有 start();它 暴露 pass() / fail()。游戏宿主没有 start()(它的回合对 inline 在挂载时上线,对 modal / fullscreen 在第一次对话框打开时上线)。两个元素都发出 startpasserrordegraded;游戏宿主额外发出 dialog-showndialog-hidden。覆盖范围在下面的 事件表 里。

方法

start(),仅 <caputchin-widget>

立即开始验证。它的用途是那个 手动触发:当元素有 trigger="manual" 时,你调用 start() 来从你自己的手势(例如,在你表单的提交上)触发 proof-of-work 求解,而非内置复选框。用默认的 auto / click 触发,元素接好它自己的激活,所以你不调用这个。

const widget = document.querySelector("caputchin-widget");
widget.start();

游戏宿主没有 start()。完整的自己驱动它的演练,见 手动模式

pass(payload),仅 <caputchin-game>

释放验证关卡:访客成功了。仅当 trigger="manual" 时有效;在一个非手动游戏上调用它会发出一个带码 invalid-callerror。第一次 pass() 兑现并 锁定 那个裁定;之后的调用被默默忽略(每会话一个回合)。

const widget = document.querySelector("caputchin-game");
widget.pass({ trace: roundRecord });
payload 字段类型含义
tracestring你游戏产出的那条不透明回合记录。在一个设了关卡的回合上,服务器重放它以推导那个权威裁定;在一个未设关卡或纯游戏的密钥上,它被原样接受。如果你的交互没有可回放的记录,就传一个空字符串。

这个方法上没有 score 字段,那个关卡是服务器对 trace 的回放,而非一个客户端声称的分数。(一个 score 确实出现在 pass 事件 上,那是组件向你回报,而非你向组件。)

在回合开始之前(modal / fullscreen,对话框尚未打开)调用 pass() 会发出一个带码 invalid-callerror。在一个纯游戏的密钥上(没有 sitekey)没有关卡可释放,所以 pass() 就只是发出带 token: nullpass 事件。

fail(payload?),仅 <caputchin-game>

中止回合:一次决定性的落败。仅当 trigger="manual" 时有效(否则 invalid-call)。可选,一个未完成的回合无论如何都被当作非通过。它中止在途的验证(proof-of-work 和探测检查),并浮现一个带码 game-error-relayederror 事件。

widget.fail({ code: "out-of-moves", message: "no moves left" });
payload 字段类型含义
codestring(可选)你的诊断码;作为 error 事件的 originalCode 浮现。默认为 game-failed
messagestring(可选)人类可读的原因。

事件

addEventListener(type, handler) 监听。每个载荷都住在 event.detail 上。

事件由谁发出event.detail何时触发
start两者{ gameId: string | null }验证开始(挂载时自动、激活时、或经 start())。gameId 对一个游戏回合被设置,对朴素组件为 null
pass两者{ token: string | null, score: number | null, durationMs: number | null }验证被释放。token 是要发给你后端的那个包装令牌;在一个纯游戏的密钥上(没有 sitekey)为 null
error两者{ code, message, severity, originalCode? }从一个无害的配置警告到一次硬失败的任何东西。见 error 事件
degraded两者{ reason: "timeout" | "network" | "http" | "malformed" }组件没能及时解析你配置的尺寸 / skin / locale,于是用它捆绑的默认值渲染了。这不是一个错误。见 degraded 事件
dialog-shown<caputchin-game>{ layout: "modal" | "fullscreen" }一个覆盖层游戏对话框打开(程序化或第一次点击)。
dialog-hidden<caputchin-game>{ layout: "modal" | "fullscreen" }一个覆盖层游戏对话框关闭(程序化、Escape 或背幕点击)。

pass 事件与令牌

pass 事件是你验证成功了的信号,但它 不是 那个信任决定。那个信任决定是你的后端确认 detail.token。始终 在你的后端核验令牌;绝不要仅凭前端事件就授予访问权。在一个表单里,组件也把令牌作为一个 caputchin-token 字段注入,好让一次普通的表单 POST 带上它。这个事件上的 scoredurationMs 是客户端报告的分析数据,可能为 null;绝不要把它们当作一个信任信号。

error 事件

error 事件携带一个稳定的 code、一个人类的 message、一个 severity,有时还有一个 originalCode。每个码都有一个固定的默认严重度,好让你 不必 硬编码一张码到严重度的表,就能把“仍在运行”的警告和“真的坏了”的失败筛开,只读 detail.severity。完整的码集:

codeseverity含义
invalid-configwarn一个组件拒绝的属性或组合;它优雅地降级并继续运行。
invalid-callwarn一个在它无效时被调用的方法(例如在一个非手动游戏上、或在回合开始之前的 pass())。
verification-failederrorproof-of-work 或探测检查(或令牌兑换)失败;没有令牌被签发。
game-load-failederror游戏包无法被解析、加载或注册。
gate-unavailableerror服务器在引导时返回一个权威拒绝(一个设了关卡的密钥无法提供一个有效的游戏)。
game-error-relayederror一个从游戏内部浮现的错误(从 iframe 转达,或从一次手动 fail())。

detail.originalCode 在公开的 code 是一个更具体内部原因的概括时存在(例如,一次作为 game-load-failed 转达的 iframe 加载失败,在 originalCode 里携带原始的 iframe-load-failed / iframe-script-blocked / game-not-registered)。用 code 来分支,只用 originalCode 来诊断。

degraded 事件

在挂载时,组件向 Caputchin 做一次简短的调用,来解析你配置的尺寸、skin 和 locale(对一个市场游戏,还有它偏好的占位)。若那次调用失败或太慢,组件不会阻塞也不会崩坏:它用烘焙进捆绑包里的值渲染,并发出 degraded,好让这种回退绝不悄无声息。在一次 degraded 事件之后,组件仍完全可用,只是解析出来的呈现可能跟你配置的不同(一个游戏可能以它的默认尺寸出现,或复选框以它的默认主题出现)。组件在回退之前会重试一次慢的解析,所以 degraded 意味着重试已耗尽,而非一次尝试慢了。

detail.reason 告诉你解析为何回退,好让你能在你自己的遥测里把它呈现出来:

reason含义
timeout解析没在它的时间预算内作答(服务慢或不可达)。
network请求本身失败了(离线、DNS、被拦、或连接被重置)。
http服务以一个错误状态作答。
malformed服务作答了,但主体不合法。

degraded 是一则通知,而非一次失败,所以它被刻意地 设为一个 error 事件:一次慢的解析不该触发你为真正故障接好的那些处理器。只有当你想观察降级渲染时才监听它,例如在你的访客正看到回退呈现时报警。

监听,实践中

const widget = document.querySelector("caputchin-game");

const onPass = (e) => {
  // send e.detail.token to your backend to verify
};
const onError = (e) => {
  if (e.detail.severity === "error") {
    console.error(e.detail.code, e.detail.message);
  }
};

widget.addEventListener("pass", onPass);
widget.addEventListener("error", onError);

// Clean up when you tear down your view:
widget.removeEventListener("pass", onPass);
widget.removeEventListener("error", onError);

这些是原生的 DOM CustomEvent,所以在 React 里你用一个 refaddEventListener 附上它们(不是 JSX 的 onPass props);前端示例 展示框架专属的接线。因为每个事件都冒泡且被 composed,你也可以从一个容器元素委托,而不是逐个绑定每个组件。

另见

本页内容