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** 配置对象作为输入,允许您指定以下属性:

  • request:一个反应函数,确定用于对异步资源执行请求的参数;
  • loader:一个加载函数,返回资源值的 Promise,可选地基于提供的请求参数。这是 ResourceOptions 的唯一必需属性;
  • equal:用于比较加载器的返回值是否相等的函数;
  • 注入器:覆盖资源实例使用的注入器,当父组件或服务被销毁时,注入器自身也会被销毁。
  • 通过这些配置,我们可以轻松定义一个**异步依赖项**,它将始终被高效使用并保持最新。

    资源生命周期

    一旦创建了**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 为其状态提供了几个信号属性,您可以轻松地在组件或服务中直接使用它们:

  • value:包含资源的当前值,如果没有可用值,则为 undefined。作为 WritableSignal,可以手动更新;
  • status:包含资源的当前状态,指示资源正在做什么以及可以从其值中期待什么;
  • 错误:如果处于错误状态,它包含资源加载期间引发的最近错误;
  • isLoading:指示资源是否正在加载新值或重新加载现有值。
  • 下面是如何在组件中使用**资源**的示例:

    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** 枚举定义。以下是这些状态及其对应值的摘要:

  • Idle = 0:资源没有有效请求,不会执行任何加载。value()未定义;
  • 错误 = 1:加载失败,出现错误。value() 未定义;
  • 正在加载 = 2:资源当前正在加载新值,这是其请求发生变化的结果。value() 未定义;
  • Reloading = 3:资源当前正在为同一请求重新加载新值。value() 将继续返回之前获取的值,直到重新加载操作完成;
  • Resolved = 4:加载完成。value()包含从加载器数据获取过程返回的值;
  • Local = 5:该值是通过 set() 或 update() 本地设置的。value() 包含手动分配的值。
  • 这些状态有助于跟踪**资源**的进度并有助于更好地处理应用程序中的异步操作。

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