Angular Resource:基于 Signal 的异步数据流响应式方案实战指南
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
本篇技术指南基于 Angular 官方文档 adev/src/content/guide/signals/resource.md 展开,核心讲解@angular/core中Resource这一将异步数据接入 Signal 体系的能力:如何用resource()创建资源、如何借助params/loader/stream协调加载流程、如何通过status状态机驱动 UI、如何在 SSR 下复用缓存、如何用chain与resourceFromSnapshots完成资源组合。读完本文,你将掌握在 Angular 应用中"以同步方式访问异步结果"的完整套路,并能针对 HTTP 请求、流式数据源与资源依赖链分别写出可靠实现。
所有ResourceSignal API(signal、computed、input等)都是同步的,但真实应用几乎总要面对异步数据。Resource的价值在于:把一次或持续性的异步操作(最常见的场景是从服务器拉取数据)纳入 Signal 体系,让你既可以用同步风格读写结果,又不必手动维护 loading / error / 请求取消等样板状态。从源码结构看,Resource是 Angular 22 引入的@publicApi(见 packages/core/src/resource/api.ts),其实现位于 packages/core/src/resource/resource.ts。
使用resource()创建异步资源
创建Resource最直接的方式是调用resource函数。它接受一个ResourceOptions配置对象,其中两个核心字段是params与loader。
import {computed, resource, Signal} from '@angular/core'; const userId: Signal<string> = getUserId(); const userResource = resource({ // 定义一个响应式计算。 // 每当其中被读取的 signal 变化时,params 的值会重新计算。 params: () => ({id: userId()}), // 定义一个异步 loader 获取数据。 // 每当 `params` 的值变化时,resource 都会调用该函数。 loader: ({params}) => fetchUser(params), }); // 基于 resource loader 的结果创建 computed signal。 const firstName = computed(() => { if (userResource.hasValue()) { // `hasValue` 有两个作用: // - 作为类型守卫,从类型中剥离 `undefined` // - 防止在 resource 处于 error 状态时读取会抛错的 `value` return userResource.value().firstName; } // 当 resource 的 value 为 `undefined` 或处于 error 状态时的回退值 return undefined; });注意两点类型细节:
- 如果不提供
defaultValue,resource<T>返回的value类型是T | undefined,因为加载尚未完成时资源没有值; - 若提供了
defaultValue,则返回类型收紧为T。源码中resource通过两个函数重载区分这两种情况(packages/core/src/resource/resource.ts)。
params:响应式请求描述
params是一个纯响应式计算,行为类似computed:当其中读取的任意 signal 发生变化,resource 就会计算出一个新的参数值。每当params产生新值,resource 都会以新参数重新触发loader。
- 如果
params计算返回undefined,loader 不会运行,resource 进入'idle'状态; params是可选的。源码注释说明:不提供params时,loader 不会自动重跑,除非手动reload(packages/core/src/resource/api.ts)。
ResourceOptions除params/loader外还包含以下配置(见 packages/core/src/resource/api.ts):
| 选项 | 作用 |
|---|---|
loader | 返回Promise<T>的一次性加载函数,与stream互斥 |
stream | 返回 Signal 的流式加载函数,与loader互斥 |
defaultValue | 服务端值不可用(如仍在加载)时 resource 返回的默认值 |
equal | 用于比较 loader 返回值的相等函数 |
injector | 覆盖resource内部使用的Injector |
id | SSR 场景下用于在TransferState中缓存数据,见下文 |
debugName | 响应式节点的调试名,用于 Angular DevTools 中标识该节点 |
一个隐含约束:不传入自定义injector时,resource必须在注入上下文(Injection Context)内调用,开发模式下源码会通过assertInInjectionContext校验这一点(packages/core/src/resource/resource.ts)。
Resource 的信号化属性
Resource对外暴露若干 signal 属性,均定义在 packages/core/src/resource/api.ts 中:
| 属性 | 类型 | 说明 |
|---|---|---|
value | Signal<T> | 最近一次加载的结果;无结果时为undefined(或在 error 状态下读取时抛错) |
hasValue() | 函数 | 是否有有效值,是响应式的,且可作为类型守卫剥离undefined |
error | Signal<Error \| undefined> | loader 最近一次抛出的错误;无错误时为undefined |
isLoading | Signal<boolean> | loader 当前是否在运行。源码用 computed 定义为status === 'loading' \|\| status === 'reloading'(见 packages/core/src/resource/resource.ts) |
status | Signal<ResourceStatus> | 当前精确状态,取值见下表 |
snapshot | Signal<ResourceSnapshot<T>> | 当前状态的快照结构,见"快照组合"一节 |
其中value信号在实现上是把加载结果包装为底层流式 Signal 后的computed结果;处于 error 状态时读取value会抛出携带Error.cause的ResourceValueError(见 packages/core/src/resource/resource.ts),因此文档建议用hasValue()做读取前的守卫——它先检查错误再检查undefined,避免错误向外冒泡(packages/core/src/resource/resource.ts)。
资源加载器(Resource loaders)
创建资源时指定的加载器分为两种形态,类型定义见 packages/core/src/resource/api.ts:
ResourceLoader:接收ResourceLoaderParams,返回PromiseLike<T>,每次请求只 resolve 一次;ResourceStreamingLoader:返回一个Signal<ResourceStreamItem<T>>(或该 Signal 的 Promise),值可随时间不断更新,适用于流式数据源。
ResourceLoaderParams对象包含三个属性:
| 属性 | 说明 |
|---|---|
params | resource 的params计算当前产生的值 |
previous | 含status字段的对象,保存上一次的ResourceStatus |
abortSignal | 一个AbortSignal,用于响应加载被取消,详见"请求取消" |
一次性加载 vs 流式加载
loader适用于"每个请求只产出单个结果"的一次性异步操作,典型代表是 HTTP 端点请求;stream则适用于"随时间不断产出多个值"的持续数据源,例如 WebSocket、Server-Sent Events(SSE)、Firestore 的onSnapshot监听等:
const userUpdates = signal({value: 'Alice'}); const userResource = resource({ stream: () => userUpdates, }); // 之后当新数据到达时: userUpdates.set({value: 'Bob'});二者的选项类型在源码层面被强制互斥:PromiseResourceOptions要求loader并将stream类型置为never,StreamingResourceOptions则相反(packages/core/src/resource/api.ts)。resource内部通过isStreamingResourceOptions判断分支,并把loader的结果统一包装成只含单个{value}(或{error})的 Signal,走同一套内部状态机(见 packages/core/src/resource/resource.ts)。流式项的类型ResourceStreamItem<T> = {value: T} | {error: Error}定义在 packages/core/src/resource/api.ts。
请求取消
如果在资源加载过程中params计算发生了变化(例如用户 ID 切换),resource 会中止上一次尚未完成的加载操作。源码通过AbortController实现:每次加载都会创建新的AbortController并把signal传给 loader,下一次加载前先abort上一次的控制器(packages/core/src/resource/resource.ts)。
你可以在 loader 内利用ResourceLoaderParams.abortSignal响应取消。原生fetch恰好支持AbortSignal:
const userId: Signal<string> = getUserId(); const userResource = resource({ params: () => ({id: userId()}), loader: ({params, abortSignal}): Promise<User> => { // 当给定的 `AbortSignal` 指示请求被中止时, // fetch 会取消任何尚未完成的 HTTP 请求。 return fetch(`users/${params.id}`, {signal: abortSignal}); }, });关于AbortSignal的更多取消语义可查阅 MDN。实现层还有一个与稳定性相关的细节:每次加载都会通过pendingTasks.add()注册一个 pending task(packages/core/src/resource/resource.ts),加载完成或被取消时再 resolve,这保证了异步加载期间应用不会过早判定为"稳定";而当请求被丢弃或资源销毁时,结果不会写回状态,避免"过期响应"污染新请求。
手动重新加载:reload()
调用reload()可编程地重新触发 loader:
const userId: Signal<string> = getUserId(); const userResource = resource({ params: () => ({id: userId()}), loader: ({params}) => fetchUser(params), }); // ... userResource.reload();需要注意的是:
reload()触发的是"对同一组参数重新加载",状态会进入'reloading',且此时value()仍保留上一次解析出的旧值(区别于因params变化引起的'loading',后者会把value置为undefined);- 从源码看
reload()有返回值:当资源处于'idle'或'loading'(已在加载中)时会直接返回false表示没有发起新加载,否则递增内部 reload 计数器并返回true(packages/core/src/resource/resource.ts)。内部实现正是用"reload 计数器是否为 0"来区分对外投影loading与reloading(packages/core/src/resource/resource.ts)。
Resource 状态机:用status驱动 UI
statussignal 会给出字符串常量形式的ResourceStatus。完整类型定义见 packages/core/src/resource/api.ts,各状态语义如下:
| Status | value() | 说明 |
|---|---|---|
'idle' | undefined | 没有有效请求(如params为undefined),loader 未运行 |
'error' | undefined | loader 执行出错 |
'loading' | undefined | 因params值变化,loader 正在运行 |
'reloading' | 上一次的值 | 因调用reload()方法,loader 正在运行 |
'resolved' | 已解析的值 | loader 已成功完成 |
'local' | 本地设置的值 | 值通过.set()或.update()被本地写入 |
文档给出的status是面向使用者的五态(error与reloading从实现上属于投影状态),UI 可以直接依据它来条件渲染加载指示器、错误提示等内容。例如:
import {resource} from '@angular/core'; const userResource = resource({ params: () => ({id: getUserId()}), loader: ({params}) => fetchUser(params), }); // 在模板或组件中按状态渲染 if (userResource.isLoading()) { // 显示加载中 } else if (userResource.status() === 'error') { // 显示 userResource.error() } else if (userResource.hasValue()) { // 渲染 userResource.value() }被当作"可写"资源时,WritableResource/ResourceRef还额外提供set()(直接覆盖值并使状态进入'local')、update()(基于旧值计算新值,等价于set(updateFn(untracked(this.value))))、asReadonly()以及手动释放用的destroy()(packages/core/src/resource/api.ts)。set()会中止当前进行中的任何加载;destroy()则会注销副作用、中止请求并把状态复位为'idle'(packages/core/src/resource/resource.ts)。
SSR:用id缓存resource数据
服务端渲染时,resource loader 会运行一次以产出初始 HTML;在浏览器端水合(hydration)过程中,通常还会再跑一次同样的 loader。为避免重复请求,可以为 resource 提供id:
const userId: Signal<string> = getUserId(); const userResource = resource({ params: () => ({id: userId()}), loader: ({params}) => fetchUser(params), id: 'user-unique-id', });配置id后,Angular 会在服务端把解析后的值存入TransferState,客户端初始化时据此把 resource 直接置于'resolved'状态,从而跳过水合期的第二次加载。其底层逻辑可见 packages/core/src/resource/resource.ts:当缓存激活(CACHE_ACTIVE)且TransferState中已存在该 key 时,直接用缓存值构造流式 Signal;服务端加载成功后则通过saveToTransferState写入TransferState(packages/core/src/resource/resource.ts)。
使用id必须满足两个前提:
id在应用内全局唯一;- 客户端与服务端的
id完全一致,Angular 才能把缓存条目与发出请求的 resource 对应起来。
重要提醒:由于缓存值会被序列化进页面 HTML,不要为"由触发该次服务端渲染的用户决定的个性化数据"设置id——尤其是当渲染出的 HTML 会被缓存或在用户间共享时,否则可能造成用户数据串号。
资源链式依赖:chain与参数传递
当一个 resource 依赖另一个 resource 的结果时,可以用params上下文对象里提供的chain函数表达依赖:
import {resource} from '@angular/core'; const userResource = resource({ params: () => ({id: getUserId()}), loader: ({params}) => fetchUser(params), }); const companyResource = resource({ params: ({chain}) => chain(userResource)?.companyId, loader: ({params: companyId}) => fetchCompany(companyId), });上例中companyResource依赖userResource加载完成后才可知的companyId。chain(userResource)会读取userResource的值并自动把它的状态传播给下游资源:
- 若
userResource处于idle,companyResource也会进入idle; - 若
userResource处于loading / reloading,companyResource进入loading且其 loader 不会运行。注意在reloading期间chain不会返回之前已解析的值; - 若
userResource处于error,companyResource也随之进入error; - 若
userResource处于resolved / local,chain返回其当前值,companyResource把它作为自己的params。
chain传播上游状态(idle / loading / reloading / error)时,params 函数不会继续执行下去;只有上游为resolved或local时才返回其值,而该值本身可能是undefined。上例用chain(userResource)?.companyId兜底:undefined值会让 params 变成undefined,于是companyResource退化为idle。
注意:请把链式得到的值直接作为 params 值,而不要包进对象里。像{companyId: undefined}这样的 params 仍是一个"已定义的值",loader 会带着undefined的companyId运行,而不是让 resource 进入idle。
chain 与直接读取value()的取舍
你也许会想在params里直接读上游 resource 的value():
const companyResource = resource({ params: () => { const user = userResource.value(); // 可能是 undefined return user ? {companyId: user.companyId} : undefined; }, loader: ({params}) => fetchCompany(params.companyId), });虽然它能工作,但params返回undefined只会让下游进入idle,无法如实反映上游真实的loading/error状态。用chain则能正确镜像这些状态,因此是更推荐的做法。
从源码看,chain的实现非常直白:按上游status决定行为——idle抛ResourceParamsStatus.IDLE、loading/reloading抛ResourceParamsStatus.LOADING、error抛带上游错误的ResourceDependencyError,仅对resolved/local返回其value()(见 packages/core/src/resource/resource.ts)。抛出的ResourceParamsStatus会被params求值捕获并转换为对应的资源状态,ResourceDependencyError的cause指向真实的上游错误(packages/core/src/resource/resource.ts)。这恰好印证了"返回undefined进 idle、抛状态码进对应状态"的文档描述。
最后,不要滥用chain:它只适用于"下游需要基于上游值执行自身异步工作"的场景;如果只是要根据 resource 结果同步派生一个值,请用computed。
用快照(Snapshots)组合资源
ResourceSnapshot是资源当前状态的结构化表示,每个 resource 的snapshot属性都会提供其当前状态的 Signal。类型定义见 packages/core/src/resource/api.ts:快照包含一个status,并根据状态携带value或error:
{status: 'idle' | 'loading' | 'reloading' | 'resolved' | 'local', value: T}{status: 'error', error: Error}
const userId: Signal<string> = getUserId(); const userResource = resource({ params: () => ({id: userId()}), loader: ({params}) => fetchUser(params), }); const userSnapshot = userResource.snapshot;resourceFromSnapshots可以从一组快照构建出新的资源(packages/core/src/resource/from_snapshots.ts 中的SnapshotResource会从快照投影出value/status/error/isLoading/hasValue)。配合computed、linkedSignal等 Signal API,就能把资源行为组合变换出新的形态。文档给出的"加载时保留旧值"组合示例很典型:
import {linkedSignal, resourceFromSnapshots, Resource, ResourceSnapshot} from '@angular/core'; function withPreviousValue<T>(input: Resource<T>): Resource<T> { const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({ source: input.snapshot, computation: (snap, previous) => { if (snap.status === 'loading' && previous && previous.value.status !== 'error') { // 当输入资源进入 loading 状态时,如果有旧值则保留之。 return {status: 'loading' as const, value: previous.value.value}; } // 否则直接透传输入资源的状态。 return snap; }, }); return resourceFromSnapshots(derived); } @Component({ /*... */ }) export class AwesomeProfile { userId = input.required<number>(); user = withPreviousValue(httpResource(() => `/user/${this.userId()}`)); // 当 userId 变化时,user.value() 会保留旧用户数据,直到新数据加载完成 }这个模式让下游资源在切换参数时呈现"保留旧值 + loading"的组合状态,避免界面闪空。核心思想是:把对"状态"的变换与对"异步加载"的执行解耦,前者用纯 Signal 组合完成,后者交给Resource。
基于 HttpClient 的httpResource
httpResource是HttpClient之上的信号化封装:它把 HTTP 请求的状态与响应都暴露为 signal,并走 Angular HTTP 栈(包括拦截器 interceptors)。其文档位于 adev/src/content/guide/http/http-resource.md,配套的httpResource实现与测试在 packages/common/http 目录中。
// 简单用法:把响应包装为 resource const user = httpResource(() => `/user/${this.userId()}`);httpResource与resource共享同一套ResourceStatus状态机,因此上文所有关于状态判断、hasValue()守卫、SSR 缓存的讨论对它同样适用。
小结
Resource以"params 响应式计算 + 异步 loader / 流式 stream"为骨架,把异步数据封装成一套可同步读取的信号接口:
params变化 → 自动重新加载;undefinedparams →idle;loader用于一次性请求,stream用于持续更新的数据源,两者互斥;abortSignal支持请求取消,reload()支持手动刷新并区分loading/reloading;value、hasValue、error、isLoading、status五类信号化读口覆盖渲染所需全部状态;- SSR 下
id+TransferState复用服务端结果,但禁止用于用户个性化数据; chain与resourceFromSnapshots分别提供"参数级"与"状态快照级"两种资源组合手段;- 需要走 HTTP 拦截器体系时可直接选用同构的
httpResource。
无论是一般的 API 数据获取、基于 SSE/WebSocket 的推送流,还是资源依赖资源的多级加载,都可以用这套 API 以声明式方式组织,让异步逻辑回归 Signal 的统一心智模型。
扩展阅读
- 信号综合指南:
resource是 Angular Signal 家族的一部分,可对照阅读 adev/src/content/guide/signals 下的signal、computed、input等基础章节; httpResource详解:adev/src/content/guide/http/http-resource.md;- Resource 公共 API 类型与文档注释:packages/core/src/resource/api.ts;
- Resource 核心实现(
resource工厂、状态机、chain、reload、SSR 缓存):packages/core/src/resource/resource.ts; - 快照组合实现:packages/core/src/resource/from_snapshots.ts。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考