方法事件
SnailMethod 上有五个事件订阅方法:onSuccess、onError、onCodeError、onFinish、 onHitCache。它们观察同一次 send() 的不同侧面,每个都返回一个取消订阅函数。
const method = userApi.getUser("42");
const off = method.onSuccess((result) => {
console.log(result.data);
});
await method.send();
off(); // 不再需要时取消订阅五个事件一览
| 方法 | 底层事件 | 触发条件 | 载荷 |
|---|---|---|---|
onSuccess(listener) | success | 请求成功、SnailResult 已经组装好 | SnailResult |
onError(listener) | error | 非业务码的任何失败,包括取消 | unknown |
onCodeError(listener) | codeError | 失败原因是 SnailResponseError(业务码被 validateCode 拒绝) | { code, payload, error } |
onFinish(listener) | finish | 总是触发,成功、失败、取消皆然(在 finally 里) | undefined |
onHitCache(listener) | cache | 成功路径上、响应由缓存提供(插件调用了 ctx.markCacheHit()) | undefined |
onError 与 onCodeError 互斥:一次失败只会有其中一个触发。五个事件的名字与载荷形状由 SnailMethodEventMap 固定。
相对请求生命周期的位置
send()
│
├─ beforeCreate ← 插件钩子
├─ beforeRequest 链
├─ 网络步骤(缓存命中时跳过)
├─ afterResponse 链 ← 网络响应与缓存命中都跑
│
├─ 成功路径 ├─ 失败路径
│ ├─ 写 meta.data / code / message │ ├─ 写 meta.error
│ ├─ 命中时 emit "cache" │ ├─ onError 插件钩子(反向)
│ ├─ emit "success" │ └─ emit "codeError" 或 "error"
│ └─ return SnailResult │ (业务码失败 → codeError,
│ │ 其它失败 → error)
│ ─────────────────────────────────────┘
│
├─ afterRequest 插件钩子(在 finally 里)
├─ 写 meta.loading = false
└─ emit "finish"三条值得单独记住的规则:
cache早于success:缓存命中也是一次成功,onHitCache只是比onSuccess先说一句。success早于finish:成功路径上监听器先拿到结果,再收到「结束了」。finish一定最后:它在finally里,也是meta.loading已经回到false之后 —— 这正是 UI 用它来关闭加载态的原因。
onSuccess
method.onSuccess((result) => {
// result 与 await method.send() 拿到的是同一个对象
result.data;
result.code;
result.fromCache; // 缓存命中时为 true
});句柄已经被写入:监听器读 method.meta.data 时看到的就是这一次响应的值,而不是上一次的。
onError
method.onError((error) => console.error(error));SnailCancelledError 也走这里 —— 取消不是业务失败,但它确实是一次「没有成功」。策略层会自己 把它识别为预期控制流而忽略;裸用 SnailMethod 时请自行区分:
import { SnailCancelledError } from "@snail-js/api";
method.onError((error) => {
if (error instanceof SnailCancelledError) return; // 组件卸载导致的取消
toast("请求失败");
});onCodeError —— 只能观察,不能恢复
method.onCodeError(({ code, payload, error }) => {
toast(String((payload as { message?: string })?.message ?? `业务失败:${code}`));
});回调跑完之后,send() 仍然会以 SnailResponseError reject。这个事件是给「弹一个提示」 用的,不是给「接住失败」用的:
try {
await method.send();
} catch (error) {
// 即使 onCodeError 已经跑过,这里依然会进 catch
}想真正拦住一个请求,唯一的位置是插件钩子 beforeRequest(写入 ctx.response 并 ctx.interrupt())—— 这也是缓存命中做的事。细节见错误处理。
onFinish
method.onFinish(() => {
spinner.hidden = true; // 成功、失败、取消,三条路都会到这里
});onFinish 是关闭加载态的唯一可靠位置。在 onSuccess 与 onError 里各写一遍,会漏掉 取消;只依赖 meta.loading 也要注意它是在 finish 之前被复位的。
onHitCache
method.onHitCache(() => console.log("这次走的是缓存"));它只在缓存命中时触发,所以「命中与未命中」可以在这里分流,而不必读 result.fromCache。 标记缓存命中的是缓存插件(ctx.markCacheHit()),核心没有别的地方会发出这个事件。
监听器挂在一个 method 实例上
事件由 SnailMethod 实例持有,不是全局广播。这带来两个直接后果:
// 每个 method 各自持有自己的监听器
const a = userApi.getUser("1");
const b = userApi.getUser("2");
a.onSuccess(() => console.log("a"));
await b.send(); // 不会触发 a 的监听器- 重新构造 method 就要重新注册。
userApi.getUser(id)每次调用都返回新的SnailMethod;UI 在重新渲染时重建 method(例如useMemo的依赖变了),上一次注册的监听器 不会跟过来,必须在新实例上重新订阅。策略 hook 正是靠内部持有唯一实例(为什么) 来避免这个问题。 - 取消订阅是显式的。五个方法都返回
() => void;不调用它,监听器就一直留在那个实例上, 直到实例被回收。
插件钩子与事件的关系
方法事件是调用方可见的表面;插件看到的是另一套钩子。两者容易混淆,这里说清楚:
| 关注的时机 | 用什么 |
|---|---|
| 真实网络响应 和 缓存命中 | 插件的 afterResponse(每个响应恰好跑一次) |
| 成功、失败、取消之后 | 插件的 afterRequest(在 finish 事件之前执行) |
| 失败 | 插件的 onError(反向顺序,无法恢复) |
| UI 里的成功 / 失败 / 结束 / 缓存命中 | 本页的五个方法事件 |
afterResponse是响应钩子,不是网络钩子:缓存命中虽然跳过了传输,但依然会产生一次afterResponse,所以响应校验与转换在命中时照常工作。afterRequest跑在finish事件之前,这样调用方的onFinish看到的meta.loading已经 是false。- 没有
afterSuccess插件钩子,而且是刻意的:需要「只在成功时做点什么」的插件应当用afterResponse(成功与缓存命中都会跑,并且早于信封校验)或onError,而不是新增一个钩子 与事件重复。完整的钩子清单见插件生命周期。