通过信号管理RXJS流量并悬挂
#javascript #网络开发人员 #angular #rxjs

信号会替换RXJS吗?

tl; dr:不!并非完全。

虽然Angular不是实现信号的第一个框架,但它是开发人员最依赖RXJ的框架。这引发了有关角生态系统中RXJ的未来的许多询问,以及信号是部分还是完全替换RXJ。

我们在这里面临的是我想称之为 spoon and fork情况,我们在软件世界中曾经用过。

考虑到汤匙可能出现在叉子前,叉子出现了,有些人可能把它们视为汤匙的替代品。当他们在土豆泥上尝试它们时...直到他们被汤汤一天。

每次解决某个特定问题的两个解决方案都可以匆匆而错误地视为较老的问题的最后一个问题。

rxjs提供了无与伦比的方法来管理基于推动数据流的时间相关的操纵,例如缓冲,节流,重试,指数向退缩或通过平坦的策略进行编排。
另一方面,它不一定是描述我们更改的值与使用它的角度视图之间的反应图的最佳拟合。

这是信号发挥作用并简化视图变化的传播。

如果您想了解有关信号的更多信息,我强烈推荐this blog post by my friend Tomas Trajanmy own blog post 😅 if you want a quick deep dive in some internals

rxjs具有一个非常有趣的时间维度,但是它带有成本,使用RXJS运算符(即Map)和Angular Pipes (即asyncpush将更改推向视图。

另一方面的信号没有任何溪流意识或时间意识,但它们非常擅长构建反应性图,并通过从价值变化到视图的变化以及通过由视图进行传播的变化“计算”信号。

- 让信号发光!

虽然信号和RXJ之间显然存在一些重叠,但让每个人都在适合最适合的地方发光。
除非您不关心下载/上传进度,重试功能以及与时间有关的任何内容,否则您可能不需要RXJ,但是大多数时候我们都想关心这些事情...级别。

在组件/视图级别上最重要的是当前状态。一个组件生活在当前,信号是当前的代表。

让我们只弥合差距,然后“投射”观察到的现状使用toSignal()功能,然后出去喝啤酒,对吧?

但是等等!

如果有错误会发生什么?

另外,我们如何知道可观察到的值是否尚未发出值?

以及一个可观察到多个值的可观察到的,我们怎么知道它是否完成了?

»spinner Vertigo

假设我们正在从远程服务中获取数据。让我们的convert the Observable to a Signalforward it to a child component

@Component({
  
  template: `<mc-recipe [recipe]=”recipe()”/>`
})
class MyCmp {
  recipe = toSignal(this.getRecipe());

  getRecipe() {
    return of('🍔').pipe(delay(1000));
  }
}

那是我们可能遇到一个常见的打字问题时:

error TS2322: Type 'string | undefined' is not assignable to type 'string'

儿童组件期望string是输入,但是toSignal()的当前默认行为不要求可观察到的值在读取信号之前发出值。它落在默认的初始值(undefined)上,可以使用initialValue选项自定义。

所以,我们可以迫使人们吃披萨和空弦:

recipe = toSignal(this.getRecipe(), {initialValue: '🍕'});

或坚持默认行为

recipe = toSignal(this.getRecipe());

- 然后在模板中处理此操作并显示旋转器直到数据准备就绪。

<mc-spinner *ngIf="!recipe()"/>
<mc-recipe *ngIf="recipe() as recipeValue" [recipe]="recipeValue" />

现在,如果出于某种混乱的原因,getRecipe()返回的观察者会发出nullundefined值或一个空字符串?

在这种情况下,旋转器只会继续旋转,因为我们可以在待处理状态和与初始值匹配的发射值之间有任何区别。

这也提出了,使用*ngIf + as来缩小类型并承受布尔强制为副作用的问题。

ð¥oups!您的有效数据无效。

错误发生了,我们可能想处理它们ð

实际上,如果我们可观察到的错误,则信号(以及从中计算出的那些) 将在阅读时简单地丢弃错误。
由于信号将主要在模板中读取,因此没有方便的方式处理错误。

人们可能想到的第一个解决方案是使用rxjs catchError()setting the error in another signal as a side effect

@Component({
  
  template: `
    <div *ngIf="error()">Oups! Something went wrong.</div>
    <mc-recipe *ngIf="recipe() as recipeValue" [recipe]="recipeValue" />
  `,
})
export class AppComponent {
  error = signal(null);
  recipe = toSignal(
    this.getRecipe().pipe(
      catchError((error) => {
        this.error.set(error);
        return of(null);
      })
    )
  );

  
}

这种方法的问题是副作用通常会导致不一致。当我们手动设置错误信号时,我们还必须在回到正轨时手动将其设置为null,否则,这可能发生:

RxJS to Signal Error

即使我们移至下一个食谱后,错误也会粘在那里。

为了解决此问题,我们可以添加一些其他意大利面条代码或等一下。对不起,悬念!

我们还没有做到!

请记住,可观察物可以发出多个值!假设我们的getRecipe()方法仍然从远程服务获取配方,但它可能会从某些缓存中散发出第一个值,而它从远程服务获取了食谱的新鲜版本。

即使我们收到第一个缓存值,我们可能仍希望显示一个进度栏,直到流完成为止,以便让用户知道我们仍在尝试加载一些其他或更新鲜的数据。

当前状态既不待定,也不是完成。

之间的某个地方。

我们在这里遇到的问题是,信号以某种方式吞没了通知,我们没有内置的方式知道该流是否完成。
我们必须像combining the koude14 operator with another Signal更好地弄清楚一种更好的方法,就像我们对catchError()所做的那样。否则,我们将遇到上述相同的不一致问题。

@Component({
  
  template: `
    <mc-progress-bar *ngIf="!finalized()"/>
    <app-recipe *ngIf="recipe() as recipeValue" [recipe]="recipeValue" />
  `,
})
export class AppComponent {
  finalized = signal(false);
  recipe = toSignal(
    this.getRecipe().pipe(
      finalize(() => this.finalized.set(true))
    )
  );

  
}

ð暂停

我们需要找出一种将可观察物映射到更丰富类型的信号的方法,该信号包含一个一致且当前的投影。

materialize操作员听起来像是自然的选择,但由于以下原因,它并没有解决我们的问题:

  • 在第一个值,错误或完整通知之前,它没有发出任何内容,因此我们必须在初始通知中发射
  • 完整的通知不包含最后一个发射值,因此我们必须以某种方式记住它(例如,使用scan操作员应用还原器)。

这个可观察到的投影问题与信号并不是新事物。当我们尝试在NGRX效果或使用Rxangular s Connect方法中投射异步任务的当前状态时,这也会发生。
这就是我们几年前与我的朋友Edouard Bozon一起创建suspensify()操作员的原因。

该操作员会产生一个可观察的可观察到的,该可观察到始终具有初始值,并且永远不会引发错误。

,它不会发出可观察的值,而是会发出一个Suspense对象,该对象包含不同的属性,该对象将告诉我们可观察到的当前状态。

下面的示例:

this.getRecipe()
  .pipe(suspensify())
  .subscribe(value => console.log(value));

如果可观测值散发值,则可以发出以下值,然后失败。

{pending: true, finalized: false, hasError: false, hasValue: false}
{pending: false, finalized: false, hasError: false, hasValue: true, value: '🍔'}
{pending: false, finalized: true, hasError: true, hasValue: false, error: '💥'}

我们可以将其变成一个始终具有初始值且永不抛弃的信号。

recipe = toSignal(this.getRecipe().pipe(suspensify())); // Signal<Suspense<Recipe> | undefined>

一旦我们尝试在这样的模板中使用它,我们就偶然发现了一个错误

<!-- error TS2532: Object is possibly 'undefined' -->
<mc-progress-bar *ngIf=”!recipe().finalized”/>

实际上,信号的默认初始值是undefined,但是我们可以通过让toSignal()知道我们可观察到的可观察到的初始值来修复键入,因为我们使用的是suspensify()

recipe = toSignal(this.getRecipe().pipe(suspensify()), {requireSync: true}); // Signal<Suspense<Recipe>>

模板中的type-narring

suspensify()的默认行为是返回类型的联合以帮助类型缩小。

这是一个示例:

<div *ngIf="suspense.hasError">
  {{ suspense.error }} // ✅
  {{ suspense.value }} // 💥 template compilation error
</div>

<div *ngIf="suspense.hasValue">
  {{ suspense.error }} // 💥 template compilation error
  {{ suspense.value }} // ✅
</div>

虽然这将避免一些常见的错误并允许我们传播正确的孩子的类型,但目前它的发挥作用不佳,因为编译器不知道recipe()在一个过程中始终返回相同的值更改检测周期。

<div *ngIf="recipe().hasError">
  {{ recipe().error }} // 💥 template compilation error
  {{ recipe().value }} // 💥 template compilation error
</div>

<div *ngIf="recipe().hasValue">
  {{ recipe().error }} // 💥 template compilation error
  {{ recipe().value }} // 💥 template compilation error
</div>

目前在角路线图中,将在以后的某些版本中固定,至少针对基于信号的组件。

同时,解决方法是将suspensifystrict选项设置为false,以便始终可用errorvalue属性,但可能是undefined

recipe = toSignal(this.getRecipe().pipe(suspensify({strict: false})));
<div *ngIf="recipe().hasError">
  {{ recipe().error }} // ✅
  {{ recipe().value }} // ✅
</div>

<div *ngIf="recipe().hasValue">
  {{ recipe().error }} // ✅
  {{ recipe().value }} // ✅
</div>

另一种选择是使用ngIf + as创建局部变量:

<ng-container *ngIf=”recipe() as suspense>
  <div *ngIf="suspense.hasError">
    {{ suspense.error }} // ✅
    {{ suspense.value }} // 💥
  </div>

  <div *ngIf="suspense.hasValue">
    {{ suspense.error }} // 💥
    {{ suspense.value }} // ✅
  </div>
</ng-container>

虽然这种解决方法可提供更好的类型缩小,但它不是很方便,并且根据RFC的信号,这可能在基于信号的组件ð
中可能不起作用。 ……但请记住!基于信号的组件可能会在模板中运送带有类型的缩小。

最后,我们可以在reusable function中迅速将其包裹起来:

@Component({
  
  template: `
<mc-progress-bar *ngIf=”!recipe().finalized”/>

<div *ngIf="recipe().hasError">
  {{ recipe().error }}
</div>

<div *ngIf="recipe().hasValue">
  {{ recipe().value }}
</div>
`
})
class MyCmp {
  recipe = toSuspenseSignal(this.getRecipe());

  getRecipe(): Observable<Recipe> {
    
  }
}

function toSuspenseSignal<T>(source$: Observable<T>) {
  return toSignal(source$.pipe(suspensify({ strict: false })), {
    requireSync: true,
  });
}

ð§`关键要点

  • ðInignals并不是要替换可观察到的。
  • ðª像suspensify()这样的简单操作员可以保持声明性,因此避免了命令性意大利面。
  • ð您可以在ngrx效果或rxstate中使用suspensify()来连接不同的来源。

ð链接和即将举行的研讨会

ð»ð«Workshops

ð»Source Code on Stackblitz

ð°Subscribe to Newsletter

ð-Discuss this on github