Angular resources() 和 rxResource() API:你需要知道的内容
几周前发布的 **Angular v19** 标志着该框架内信号革命的一个重要里程碑,**Input**、**Model**、**Output** 和 **Signal Queries** API 现已正式升级为稳定版本。
但这还不是全部!此主要版本还引入了旨在进一步推动信号革命的强大新工具:新的**资源 API**。
顾名思义,这个新的**资源 API**旨在通过充分利用信号的功能来简化**异步资源**的加载!
**重要提示:**在撰写本文时,新的 **资源 API** 仍处于实验阶段。这意味着它可能会在稳定之前发生变化,因此使用它需要您自担风险。😅
让我们深入了解它是如何工作的以及它如何简化异步资源的处理!
资源 API
大多数信号 API 都是同步的,但在实际应用中,处理异步资源至关重要,例如从服务器获取数据或实时管理用户交互。
这就是新的**Resource** API 发挥作用的地方。
使用**资源**,您可以轻松地通过信号使用异步资源,从而让您轻松管理数据提取、处理加载状态,并在相关信号参数发生变化时触发新的提取。
resource( ) 函数
创建 **Resource** 的更简单方法是使用 **resource()** 函数:
import { resource, signal } from '@angular/core'; const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/'; private id = signal(1); private myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id), });
此函数接受 **ResourceOptions** 配置对象作为输入,允许您指定以下属性:
通过这些配置,我们可以轻松定义一个**异步依赖项**,它将始终被高效使用并保持最新。
资源生命周期
一旦创建了**Resource**,就会执行**loader**函数,然后开始生成的**异步请求**:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading")
每当 **request** 函数所依赖的信号发生变化时,**request** 函数就会再次运行,如果它返回新的参数,就会触发 **loader** 函数来获取更新后的资源的值:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } id.set(2); // Triggers a request, causing the loader function to run again console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 2 , ... }
如果没有提供 **request** 函数,**loader** 函数将只运行一次,除非使用 **reload** 方法重新加载 **Resource**(详见下文)。
最后,一旦父组件或服务被销毁,除非提供了特定的注入器,否则**资源**也会被销毁。
在这种情况下,**资源**将保持活动状态,只有当提供的**注入器**本身被销毁时才会被销毁。
使用 abortSignal 中止请求
为了优化数据获取,如果在先前的值仍在加载时 **request()** 计算发生变化,则 **Resource** 可以中止未完成的请求。
为了管理这一点,**loader()** 函数提供了一个 **abortSignal**,您可以将其传递给正在进行的请求,例如 **fetch**。该请求会监听 **abortSignal**,并在触发时取消操作,从而确保高效的资源管理并防止不必要的网络请求:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request, abortSignal }) => fetch(RESOURCE_URL + request.id, { signal: abortSignal }) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // Triggers a new request, causing the previous fetch to be aborted // Then the loader function to run again generating a new fetch request id.set(2); console.log(myResource.status()); // Prints: 2 (which means "Loading")
基于此,建议主要将 **Resource** API 用于 **GET** 请求,因为它们通常可以安全取消而不会引起问题。
对于 **POST** 或 **UPDATE** 请求,取消可能会导致意外的副作用,例如数据提交或更新不完整。但是,如果您需要针对这些类型的请求实现类似的功能,则可以使用 **effect()** 方法来安全地管理操作。
如何使用资源
**Resource** API 为其状态提供了几个信号属性,您可以轻松地在组件或服务中直接使用它们:
下面是如何在组件中使用**资源**的示例:
import { Component, resource, signal } from '@angular/core'; const BASE_URL = 'https://jsonplaceholder.typicode.com/todos/'; @Component({ selector: 'my-component', template: ` @if (myResource.value()) { {{ myResource.value().title }} } ` }) export class MyComponent { private id = signal(1); protected myResource = resource({ request: () => ({ id: this.id() }), loader: ({ request }) => fetch(BASE_URL + request.id).then((response) => response.json()), }); protected fetchNext(): void { this.id.update((id) => id + 1); } }
在这个例子中,**Resource** 用于根据 **id** 信号的值从 API 中获取数据,该值可以通过单击按钮来增加。
每当用户点击按钮时,**id**信号值就会发生变化,触发**loader**函数从远程 API 中获取新项目。
由于 **Resource** API 公开的信号属性,UI 会使用获取的数据自动更新。
检查资源的状态
如前所述,**status**信号提供有关任何给定时刻资源当前状态的信息。
**status** 信号的可能值由 **ResourceStatus** 枚举定义。以下是这些状态及其对应值的摘要:
这些状态有助于跟踪**资源**的进度并有助于更好地处理应用程序中的异步操作。
hasValue() 函数
考虑到这些状态的复杂性,**Resource API** 提供了 **hasValue()** 方法,该方法根据当前状态返回一个布尔值。
这确保了有关**资源**状态的准确信息,提供了一种更可靠的方式来处理异步操作,而不依赖于在某些状态下可能是**未定义**的值。
hasValue() { return ( this.status() === ResourceStatus.Resolved || this.status() === ResourceStatus.Local || this.status() === ResourceStatus.Reloading ); }
此方法是反应式的,允许您像**信号**一样使用和跟踪它。
isLoading( ) 函数
**Resource** API 还提供了 **isLoading** 信号,返回资源当前是否处于 **Loading** 或 **Reloading** 状态:
readonly isLoading = computed( () => this.status() === ResourceStatus.Loading || this.status() === ResourceStatus.Reloading );
由于 **isLoading** 是一个计算信号,因此可以对其进行反应性跟踪,从而让您可以使用信号 API 实时监控加载状态。
资源值作为 WritableSignal
**Resource** 提供的值信号是 **WritableSignal**,它允许您使用 **set()** 和 **update()** 函数手动更新它:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } myResource.value.set({ id: 2 }); console.log(myResource.value()); // Prints: { id: 2 } console.log(myResource.status()); // Prints: 5 (which means "Local") myResource.value.update((value) => ({ ...value, name: 'Davide' }); console.log(myResource.value()); // Prints: { id: 2, name: 'Davide' } console.log(myResource.status()); // Prints: 5 (which means "Local")
**注意:**如您所见,手动更新信号的**值**也会将状态设置为**5**,这意味着“**本地**”,以表明该值是在本地设置的。
手动设置的值将一直存在,直到设置新值或执行新请求,新请求将用新值覆盖它:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... } myResource.value.set({ id: 2 }); console.log(myResource.value()); // Prints: { id: 2 } console.log(myResource.status()); // Prints: 5 (which means "Local") id.set(3); // Triggers a request, causing the loader function to run again console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 3 , ... }
**注意:**“Resource API”的“value”信号使用与新“LinkedSignal”API相同的模式,但在底层并不使用它。🤓
便捷包装方法
为了简化 **value** 信号的使用,**Resource** API 为 **set**、**update** 和 **asReadonly** 方法提供了便捷的包装器。
**asReadonly** 方法特别有用,因为它返回 **value** 信号的只读实例,只允许读取访问并防止任何意外修改。
您可以使用此方法来创建通过导出**值**的只读实例来管理和跟踪资源值变化的服务:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; export class MyService { const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id }) }); public myValue = myResource.value.asReadonly(); setValue(newValue) { // Wrapper of `myResource.value.set()` myResource.set(newValue); } addToValue(addToValue) { // Wrapper of `myResource.value.update()` myResource.update((value) => ({ ...value, ...addToValue }); } } // Usage of the service in a component or other part of the application const myService = new MyService(); myService.myValue.set(null); // Property 'set' does not exist in type 'Signal' myService.setValue({ id: 2 }); console.log(myService.myValue()); // Prints: { id: 2 } myService.addToValue({ name: 'Davide' }); console.log(myService.myValue()); // Prints: { id: 2, name: 'Davide' }
这将防止消费者修改值,降低意外更改的风险,提高复杂数据管理的一致性。
重新加载或销毁资源
当使用异步资源时,您可能会面临需要刷新数据或销毁**资源**的情况。
为了处理这些情况,**资源 API** 提供了两种专用方法,为管理这些操作提供了有效的解决方案。
reload() 函数
**reload()** 方法指示 **Resource** 重新执行异步请求,确保它获取最新的数据:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") myResource.reload(); // Returns true if a reload was initiated console.log(myResource.status()); // Prints: 3 (which means "Reloading") // After the fetch resolves console.log(myResource.status()); // Prints: 5 (which means "Local")
如果重新加载成功启动,**reload()** 方法将返回 **true**。
如果无法执行重新加载,无论是因为没有必要,例如状态已经为 **Loading** 或 **Reloading**,还是不受支持,例如状态为 **Idle**,该方法都返回 **false**。
destroy() 函数
**destroy()** 方法手动销毁 **Resource**,销毁任何用于跟踪请求变化的 **effect()**,取消任何待处理的请求,并将状态设置为 **Idle**,同时将值重置为 **undefined**:
import { resource, signal } from "@angular/core"; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = resource({ request: () => ({ id: id() }), loader: ({ request }) => fetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") myResource.destroy(); // Returns true if a reload was initiated console.log(myResource.status()); // Prints: 1 (which means "Idle") console.log(myResource.value()); // Prints: undefined
**资源**被销毁后,它将不再响应请求改变或**reload()**操作。
**注意:**此时,虽然 **value** 信号仍然可写,但 **Resource** 将失去其预期用途并且不再发挥其功能,变得毫无用处。🙃
rxResource( ) 函数
与迄今为止推出的几乎所有基于信号的 API 一样,**Resource** API 还提供了互操作性实用程序,可与 **RxJS** 无缝集成。
除了使用 **resource()** 方法创建基于 Promise 的 **Resource** 之外,您还可以使用 **rxResource()** 方法来使用 **Observables**:
import { resource, signal } from "@angular/core"; import { rxResource } from '@angular/core/rxjs-interop'; import { fromFetch } from 'rxjs/fetch'; const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/"; const id = signal(1); const myResource = rxResource({ request: () => ({ id: id() }), loader: ({ request }) => fromFetch(RESOURCE_URL + request.id) }); console.log(myResource.status()); // Prints: 2 (which means "Loading") // After the fetch resolves console.log(myResource.status()); // Prints: 4 (which means "Resolved") console.log(myResource.value()); // Prints: { "id": 1 , ... }
**注意**:**rxResource()** 方法实际上是由 **rxjs-interop** 包公开的。
**loader()** 函数生成的 **Observable** 将只考虑第一个发射的值,而忽略后续发射。
感谢您的阅读🙏
感谢大家在这美好的 2024 年里一直关注我。🫶🏻
这是充满挑战的一年,但也收获颇丰。我对 2025 年有宏伟的计划,迫不及待地想要开始实施它们。🤩
我希望得到您的反馈,因此请留下**评论**、**点赞**或**关注**。👏
然后,如果你真的喜欢它,请与你的社区、技术兄弟和任何你想要的人**分享它**。别忘了**在 LinkedIn 上关注我**。👋😁