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 上关注我**。👋😁