Angular 中基于 Signal 的异步数据管理:使用 Resource API 构建响应式用户资料加载器
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
在 Angular 中,异步数据(如 HTTP 请求返回的结果)的状态管理一直是开发中的高频场景:数据是否加载中、是否出错、如何随参数变化自动重新请求、请求如何被取消……针对这些问题,Angular 的 Resource API 给出了一个完全以 Signal 为中心的声明式答案。本文以官方交互式教程《Managing async data with signals using the Resources API》为核心,在 Angular 仓库的实战环境中,通过一个用户资料加载器示例,逐步讲解resource()函数的用法,并结合 resource 源码 与 Resource API 类型定义 剖析其底层工作方式。学完本文,你将能独立使用resource()加载异步数据、驱动参数变化自动重载、处理加载/错误/成功三种界面状态,并理解status()、value()、error()、hasValue()与reload()背后的设计原理。
本教程的完整代码以练习与答案两种形态存放在仓库中:练习起点见 src/app/app.ts,完整实现见 answer/src/app/app.ts,配套的模拟 API 位于 src/app/user-api.ts。
为什么需要 Resource API
在 Angular Signal 生态中,signal()用于存放同步状态,computed()用于派生只读状态,linkedSignal()用于描述"跟随另一个信号变化、但可被局部覆写"的状态。然而真正的业务数据往往来自远端,是一次异步的、会失败、需要取消、且需要随参数变化重新发起的操作。
Resource API 正是为此设计:它把"一次异步读取操作"抽象为一个响应式资源,让数据加载过程拥有内建的加载状态、错误处理和请求管理能力。在引入 Resource 之前,开发者通常需要手写一组signal+effect+ 手动 cleanup 的样板代码;引入后,这份样板被封装进resource()工厂函数与底层的ResourceImpl类中。
值得强调的是,源码注释明确了它的适用边界:resource是为读操作(read operations)设计的,而非变更(mutation)操作——因为资源被销毁或请求对象变化时,会通过AbortSignal取消进行中的加载,这可能会过早中断一次变更(见 resource.ts)。在实践中,这意味资源加载适合承载 GET 类请求,写操作应使用传统的事件处理器或 RxJS 流程。
前提:mock API 与信号生态
在动手前,先理解教程使用的模拟数据源 src/app/user-api.ts:
// Mock API function for loading user data export async function getUserData(id: number): Promise<{name: string; email: string}> { // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate potential errors if (id === 999) { throw new Error('User not found'); } return { name: `User ${id}`, email: `user${id}@example.com`, }; }这个函数刻意模拟了两个真实网络场景:1000ms 的网络延迟,以及"用户不存在"(id 为 999)时抛出的异常。它返回一个Promise<{name, email}>,正是后续resource()的loader需要承载的载荷形态。在整个教程中,它充当远程 API 的角色,而 Resource 负责将它的"进行中 / 成功 / 失败"过程映射为可被模板读取的信号状态。
Step 1:导入resource与 mock API
一切从导入开始。需要把resource加入既有的@angular/core导入列表,同时引入模拟 API 函数:
// Add resource to existing imports import {Component, signal, computed, resource, ChangeDetectionStrategy} from '@angular/core'; // Import mock API function import {getUserData} from './user-api';组件同时使用了signal(承载 userId)、computed(派生加载/错误状态)与resource(承载异步数据),这清晰体现了 Resource API 与 Signal 体系的无缝衔接——它不取代 Signal,而是构建在 Signal 之上。
Step 2:创建加载用户数据的 resource
在组件类中新增一个属性,基于一个用户 ID 信号创建资源。当userId信号变化时,资源会自动携带新参数重新执行加载:
userId = signal(1); userResource = resource({ params: () => ({id: this.userId()}), loader: (params) => getUserData(params.params.id), });逐项拆解resource()的入参对象:
params(也支持旧名称request):一个响应式函数,返回本次请求的描述对象(这里包裹了userId)。它的返回值参与响应式依赖追踪;每当读取过的信号发生变化,Resource 就认为"请求内容变了",自动触发新一轮加载。如果完全不提供params,loader 将不会自动重跑,除非显式调用reload()(详见 api.ts 中的 BaseResourceOptions)。loader:接收ResourceLoaderParams,其中包含params(即上面params函数的返回值)、abortSignal与previous.status(见 ResourceLoaderParams)。loader 返回Promise时即 Promise 型资源;返回信号或"信号的 Promise"时即流式(streaming)资源,二者不可同时指定(见 PromiseResourceOptions / StreamingResourceOptions)。
从源码层面看,resource()内部首先做注入上下文校验,然后把params与loader组装进new ResourceImpl(...)(见 resource.ts)。ResourceImpl的状态机由两个linkedSignal驱动:一个外部请求信号extRequest(把 params 求值结果与 reload 计数器绑定),一个内部状态信号state,两者协作实现"参数一变即瞬时切换状态、再异步推进到结果"的行为(见 resource.ts)。这正是教程反复强调的"Resources are reactive"的底层来源。
Step 3:与资源交互——改参数与手动重载
加载器并非只能被动响应参数变化,它同样接受指令式操作:
loadUser(id: number) { this.userId.set(id); } reloadUser() { this.userResource.reload(); }这里呈现了两种触发重载的途径,二者对应完全不同的底层语义:
- 改参数触发:
loadUser()通过userId.set(id)修改信号,进而让params重新求值。参数变化时extRequest会携带一个新请求对象,statelinked signal 看到请求已变化,就判定这属于"新请求",进入loading(首载)状态并执行 loader。 - 手动重载:
reloadUser()调用userResource.reload()。查看 reload() 实现 可以发现它不做请求体变化,而是把请求对象里的reload计数器加一,使资源进入reloading状态——此时value()仍会保留上一次已取回的数据,只有状态信号变为reloading。同时reload()在资源处于idle或loading时返回false(不重启进行中的加载),成功发起时才返回true,这是调用方可以判别的返回值契约。
选择哪种方式取决于语义:参数本质不同(如翻页、切换用户 ID)应当驱动params;而"刷新同一份数据"应当走reload()。教程中的界面把两种方式都暴露给了用户:Load User 1/2按钮演示前者,Reload按钮演示后者。
Step 4:用 computed 信号派生资源状态
模板需要根据加载状态渲染不同内容,因此用computed把资源状态折叠成布尔开关:
isLoading = computed(() => this.userResource.status() === 'loading'); hasError = computed(() => this.userResource.status() === 'error');status()是资源暴露的核心信号。值得留意的是:教程正文用 'loading'、'success'、'error' 三个词帮助学生建立直观概念,但当前仓库中ResourceStatus的完整取值比这更精细。依据 api.ts 的类型注释,实际状态共六种:
| 状态 | 含义 | value() 的行为 |
|---|---|---|
idle | 没有有效请求,不会执行加载 | 返回undefined |
loading | 因响应式依赖变化而加载新值 | 返回undefined |
reloading | 针对同一请求重新拉取新值 | 继续返回上一次取回的值 |
resolved | 加载完成,持有 loader 返回的值 | 返回加载结果 |
error | 加载抛出错误 | 返回undefined,错误存于error() |
local | 值被.set()/.update()就地覆写 | 返回本地设置的值 |
可以看到教程代码里的两个判断各司其职:status() === 'loading'只覆盖首载过程(刷新期间显示旧值,不闪烁"Loading"文案),status() === 'error'则精确命中出错场景。
资源还提供一系列开箱即用的成员(见 Resource 接口):
value():只读信号,当前已加载的数据;出错状态下读取它会抛出错误。status():上面列举的六态状态信号。error():出错时返回最后一次错误。isLoading:信号形式的快捷标志。查看 BaseWritableResource 构造 可发现它由status() === 'loading' || status() === 'reloading'计算而来,覆盖面比单独比对'loading'更广——教程为了概念拆解使用了computed自建标志,而库本身已内置isLoading。hasValue():响应式函数,安全地判断是否存在有效数据;实现上会先检查错误态(出错时返回false),再判断value()是否为undefined(见 resource.ts)。snapshot():把status与value/error打包成不可变快照,便于整体传递。
Step 5:连接按钮并把状态渲染进模板
模板骨架由教程预先给出,最终答案(见 answer/src/app/app.ts)把交互与展示完整接好。
Part 1:为按钮绑定点击处理器
<button (click)="loadUser(1)">Load User 1</button> <button (click)="loadUser(2)">Load User 2</button> <button (click)="loadUser(999)">Load Invalid User</button> <button (click)="reloadUser()">Reload</button>前三个按钮向loadUser传入不同 id——尤其是 999,它会在user-api中触发throw new Error('User not found'),用于验证错误分支;Reload按钮则演示对同一用户的强制刷新。
Part 2:用 @if 处理加载、错误、成功三分支
@if (isLoading()) { <p>Loading user...</p> } @else if (hasError()) { <p class="error">Error: {{ userResource.error()?.message }}</p> } @else if (userResource.hasValue()) { <div class="user-info"> <h3>{{ userResource.value().name }}</h3> <p>{{ userResource.value().email }}</p> </div> }这是一段典型的资源状态渲染结构,值得注意的工程细节:
- 分支顺序很重要:先判断 isLoading(首载遮罩),再判断错误,最后才信任
hasValue()取数据,避免在错误态去读取value()(那会抛出异常,参见前文对value语义的说明)。 userResource.error()?.message使用了可选链,因为error()在未出错时是undefined;且 loader 抛出的非 Error 值会被源码中的encapsulateResourceError包装成Error实例,保证.message始终可读(见 resource.ts)。hasValue()作为最后的守卫,确保仅在"有值可用"时才渲染数据区域。
教程把资源的状态检查方式汇总如下,它们是模板与资源交互的完整工具箱:
isLoading()—— 拉取数据期间为真;hasError()—— 发生错误时为真;userResource.hasValue()—— 有可用数据时为真;userResource.value()—— 访问已加载数据;userResource.error()—— 访问错误信息。
配套样式文件 src/app/app.css 用.error的红色(#d32f2f)与.user-info的绿色(#2e7d32)区分错误与成功状态的视觉反馈,button与.status区给出了常规的间距与边框样式,确保示例开箱即用即可观察。
状态机如何跑起来:一次加载的源码级旅程
要真正理解 Resource,值得把"点击 Load User 2"到"页面渲染出 User 2"之间发生的事在源码层面串一遍。核心实现全部位于 packages/core/src/resource/resource.ts:
loadUser(2)执行userId.set(2),params函数里对userId()的读取使其失效。extRequest(linked signal)重算,携带新的请求对象;statelinked signal 检测到请求变化,把状态置为loading(resource.ts)。statuscomputed 将内部状态投影为公开六态;此时模板中的isLoading()变真,渲染"Loading user..."。- 内部
effect(loadEffect)被触发:它会先abortInProgressLoad()取消上一次未完成的请求(pendingController.abort(),resource.ts),并注册一个PendingTasks任务用于阻塞应用稳定性判定,然后在 untracked 上下文中调用 loader(resource.ts)。loader 只依赖params侧的重活性,返回值则刻意不参与信号追踪,避免跨await的追踪歧义。 - loader 返回的 Promise 完成后,进入"是否丢弃本次结果"的裁决:若
abortSignal.aborted或当前请求已不是发起时那个(extRequest已被再次更新),则静默丢弃旧响应,不污染新状态(resource.ts)——这正是教程所说"自动取消与清理"的落地机制。 - 状态写入
resolved(或失败则写入错误流,状态投影为error),value 信号更新,模板渲染数据区;最后 resolve 掉 PendingTasks 注册的任务,应用据此恢复稳定。 - 当组件被销毁时,
DestroyRef回调调用destroy(),它会销毁 effect、中止进行中的请求并把状态归位为idle(resource.ts),杜绝了传统异步组件常见的"销毁后仍更新状态"的内存泄漏与异常。
值得注意的一点:整个加载循环中,取消是针对"最近一次"请求的。任何新请求出现,都会立刻中止旧请求——也就是说,当用户快速依次点击 Load User 1、Load User 2 时,前一次请求即使慢也会被放弃,界面上永远只呈现最后一次操作的结果,这正是reload()会返回false、避免重复请求这一设计要守护的一致性。
掌握四组核心概念
教程收尾给出了四组必须记住的关键点,结合本文的源码分析,它们可以映射为具体实现依据:
- Resources are reactive(资源是响应式的):
params读取的信号一变化,linked signal 驱动自动重载;手动场景则通过reload()的计数器机制刷新同一请求。 - Built-in state management(内建状态管理):
status()、value()、error()三个信号覆盖六态状态机(idle/loading/reloading/resolved/error/local),外加isLoading、hasValue()、snapshot()等便捷成员。 - Automatic cleanup(自动清理):
AbortController取消过期请求、DestroyRef在销毁时清理资源、过期响应的"丢弃裁决"保证不写入脏数据。 - Manual control(手动控制):
reload()触发刷新、.set()/.update()写入本地值并进入local态、destroy()手动销毁资源并取消进行中的请求。
这套能力并非教学玩具——ResourceStatus、Resource、WritableResource、ResourceRef等类型均标注为@publicApi 22.0的公开 API(见 api.ts),ResourceImpl还支持通过options.id+TransferState在服务端渲染时缓存并在客户端复用数据(见 resource.ts),因此可直接用于真实业务。将 官方 Signals 教程目录 中的这个示例跑通,也就掌握了把任意 Promise/流式数据源接入 Angular 响应式渲染管线的基本范式。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考