可取消的异步任务与带类型的服务器错误(使用 SolidJS 和 LazyPromise)
Source: Dev.to
可取消的异步任务
为什么我们需要取消异步任务?
如果你已经发起了服务器请求,请求已经在路上,但在某些情况下在浏览器端中止它是必需的。
示例: OTP 验证码登录流程
- 用户输入邮箱并请求登录验证码。
- 应用展示一个页面,让用户输入收到的验证码。
- 代码发送到服务器;服务器返回
Set‑Cookie头部,完成用户认证并重定向到仪表盘。
如果携带验证码的请求仍在进行中,而用户点击 返回 并尝试使用其他邮箱登录,未完成的请求最终会设置错误的 cookie。此时我们必须在浏览器层面中止该请求。
LazyPromise 与 Solid
前段时间我创建了一个原语叫 LazyPromise。它的 API 看起来像原生 Promise,但它是可取消的。LazyPromise 提供一个 .subscribe(handleValue, handleError) 方法,返回一个处理器用于销毁。
在将一个应用从 React 移植到 Solid 时,我编写了一个粘合函数 useLazyPromise,它把销毁句柄传递给 Solid 的 onCleanup,从而保证一旦 Solid 范围被销毁,异步任务就会被取消:
useLazyPromise(myLazyPromise, handleValue, handleError);
为什么不直接使用 AbortSignal?
你可以用 AbortSignal 包装一个原生 Promise:
useAbortablePromise(async (abortSignal) => {
// fetch things
});
LazyPromise 可以被视为这种包装器。工具函数 lazy 把一个 async 函数转换为 LazyPromise:
useLazyPromise(
lazy(async (abortSignal) => {
// fetch things
}),
handleValue,
handleError
);
当你编写类似 async (abortSignal) => … 的函数时,实际上已经在使用惰性 Promise。使用真正的 LazyPromise 对象还能带来下面讨论的额外好处。
类型化的服务器错误
在我的项目中,类型系统会反映服务器端点可能抛出的错误。服务器处理函数使用普通的 async/await;出错时返回一个对象 { __error: … },因此结果类型是 Data | { __error: Error }(也可以写成 { data: Data } | { error: Error })。
客户端通过 tRPC 通信,包装器会把 tRPC 响应转换为 LazyPromise(gist)。
const lazyPromise = trpcLazyPromise(api.authn.checkOtpCode.mutate)({
/* params */
});
lazyPromise 是 Data 和 Error 的泛型。保持返回类型为 Data | { __error: Error } 可以在不使用自定义 Promise 的情况下捕获错误类型,但这会破坏像 Promise.all 之类的工具,并使代码更难阅读。
像 Effect(React)这样的库也提供类型化错误,但它们需要全新的 API。LazyPromise 保留了熟悉的 Promise‑like API,同时加入了可取消性和类型化错误。
在 Solid 中使用 LazyPromise
惰性、可取消性和类型化错误
我们已经讨论了 LazyPromise 与原生 Promise 的两点不同:
- 惰性 / 可取消性 – 当 Solid 范围被销毁时,你可以中止任务。
- 类型化错误 – 错误类型是 Promise 泛型参数的一部分。
同步解析
第三点区别:LazyPromise 同步(在同一个 tick 中)解析,而不是在微任务中。这与 Solid 的细粒度响应式非常契合。
useLazyPromiseValue
useLazyPromiseValue 是 Solid 的 createResource 的原始版本。它返回一个 accessor,首先产生一个加载符号,然后返回解析后的值。
// `value` 是一个 accessor,首先返回 Symbol loadingSymbol,
// 然后返回解析后的值。
const value = useLazyPromiseValue(() => getLazyPromise(mySignal()));
当惰性 Promise 同步解析时,行为类似于 useMemo:
// `resolved` 类似于 Promise.resolve。
useLazyPromiseValue(() => resolved(mySignal()));
// 表现同:
useMemo(() => mySignal());
结论
如果你对尝试此方案感兴趣,LazyPromise 库已经经过充分测试(包括内存泄漏检测)且相当稳定(source, introductory article)。SolidJS 的绑定仍在实验阶段,但已在这里提供。我很期待在评论区听到你的想法,尤其是关于此方案如何与即将推出的 async signals 特性交互的讨论。