可取消的异步任务与带类型的服务器错误(使用 SolidJS 和 LazyPromise)

发布: (2025年12月4日 GMT+8 10:20)
5 min read
原文: Dev.to

Source: Dev.to

可取消的异步任务

为什么我们需要取消异步任务?
如果你已经发起了服务器请求,请求已经在路上,但在某些情况下在浏览器端中止它是必需的。

示例: OTP 验证码登录流程

  1. 用户输入邮箱并请求登录验证码。
  2. 应用展示一个页面,让用户输入收到的验证码。
  3. 代码发送到服务器;服务器返回 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 响应转换为 LazyPromisegist)。

const lazyPromise = trpcLazyPromise(api.authn.checkOtpCode.mutate)({
  /* params */
});

lazyPromiseDataError 的泛型。保持返回类型为 Data | { __error: Error } 可以在不使用自定义 Promise 的情况下捕获错误类型,但这会破坏像 Promise.all 之类的工具,并使代码更难阅读。

Effect(React)这样的库也提供类型化错误,但它们需要全新的 API。LazyPromise 保留了熟悉的 Promise‑like API,同时加入了可取消性和类型化错误。

在 Solid 中使用 LazyPromise

惰性、可取消性和类型化错误

我们已经讨论了 LazyPromise 与原生 Promise 的两点不同:

  1. 惰性 / 可取消性 – 当 Solid 范围被销毁时,你可以中止任务。
  2. 类型化错误 – 错误类型是 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 特性交互的讨论。

Back to Blog

相关文章

阅读更多 »

Angular pipes:重新思考

Forem 社区 Forem !Forem 徽标 https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3...