【Angular 开发】Angular 信号的应用状态管理

自我介绍

  • 做一个简单介绍,年近48 ,有20多年IT工作经历,目前在一家500强做企业架构.因为工作需要,另外也因为兴趣涉猎比较广,为了自己学习建立了三个博客,分别是【全球IT瞭望】,【架构师酒馆】和【开发者开聊】.
  • 企业架构师需要比较广泛的知识面,了解一个企业的整体的业务,应用,技术,数据,治理和合规。之前4年主要负责企业整体的技术规划,标准的建立和项目治理。最近一年主要负责数据,涉及到数据平台,数据战略,数据分析,数据建模,数据治理,还涉及到数据主权,隐私保护和数据经济。 因为需要,最近在学习财务,金融和法律。打算先备考CPA,然后CFA,如果可能可以学习法律,备战律考。
  • 欢迎按学习的同学朋友关注,也欢迎大家交流。微信小号【ca_cea】

在本文中,我将演示如何仅使用Angular Signals和一个小函数来管理应用程序的状态。

不仅仅是“与主题一起服务”

让我们从解释为什么在服务中使用一堆BehaviorSubject对象不足以管理异步事件引起的状态修改开始。

在下面的代码中,我们有一个方法saveItems(),它将调用API服务,以异步更新项列表:

saveItems(items: Item[]) {
  this.apiService.saveItems(items).pipe(
    takeUntilDestroyed(this.destroyRef)
  ).subscribe((items) => this.items$.next(items));
}

每次我们调用这种方法,都是在冒险。

例如:假设我们有两个请求,A和B。

请求A在0s 0ms开始,请求B在0s 250ms开始。然而,由于某些问题,API在500ms后对A做出响应,在150ms后对B做出响应。

结果,a在0s 500ms时完成,B在0s 400ms时完成。

这可能会导致保存错误的项目集。

它也适用于GET请求——有时,对搜索请求应用什么过滤器非常重要。

我们可以添加一些支票,如下所示:

saveItems(items: Item[]) {
  if (this.isSaving) {
    return;
  }
  this.isSaving = true;
  this.apiService.saveItems(items).pipe(
    finalize(() => this.isSaving = false),
    takeUntilDestroyed(this.destroyRef)
  ).subscribe((items) => this.items$.next(items));
}

但是,正确的项目集将根本没有机会保存。

这就是为什么我们的Store需要效果。

使用NgRx ComponentStore,我们可以这样写:

 readonly saveItems = this.effect<Item[]>(_ => _.pipe(
   concatMap((items) => this.apiService.saveItems(items)),
   tapResponse(
     (items)=> this.items$.next(items),
     (err) => this.notify.error(err)
   )
));

在这里,您可以确保请求将一个接一个地执行,无论每个请求运行多长时间。

在这里,您可以很容易地为请求排队选择一种策略:switchMap()、concatMap(),exhautMap()或mergeMap()。

基于信号的存储

什么是应用程序状态?应用程序状态是定义应用程序外观和行为的变量集合。

应用程序总是有一些状态,而“Angular 信号”总是有一个值。这是一个完美的匹配,所以让我们使用信号来保持应用程序和组件的状态。

class App {
   $users = signal<User[]>([]);
   $loadingUsers = signal<boolean>(false);
   $darkMode = signal<boolean|undefined>(undefined);
}

这是一个简单的概念,但有一个问题:任何人都可以写信给$loadingUsers。让我们将状态设为只读,以避免全局可写变量可能带来的无限微调器和其他错误:

class App {
   private readonly state = {
     $users: signal<User[]>([]),
     $loadingUsers: signal<boolean>(false),
     $darkMode: signal<boolean|undefined>(undefined),
   } as const;

   readonly $users = this.state.$users.asReadonly();
   readonly $loadingUsers = this.state.$loadingUsers.asReadonly();
   readonly $darkMode = this.state.$darkMode.asReadonly();

   setDarkMode(dark: boolean) {
     this.state.$darkMode.set(!!dark);
   }
}

是的,我们写了更多的行;否则,我们将不得不使用getter和setter,这甚至是更多的行。不,我们不能让它们都是可写的,并添加一些评论“不要写!!”😉

在这个存储中,我们的只读信号(包括使用computed()创建的信号)是状态和选择器的替代品。

剩下的只有:我们需要效果,改变我们的状态。

Angular Signals中有一个名为effect()的函数,但它只对信号的变化做出反应,通常我们应该在向API发出一些请求后修改状态,或者作为对某些异步发出的事件的反应。虽然我们可以使用toSignal()创建额外的字段,然后在Angular的effect()中观察这些信号,但它仍然不能像我们想要的那样对异步代码进行控制(没有switchMap()、没有concatMap(),没有debounceTime()和许多其他东西)。

但是,让我们使用一个著名的、经过充分测试的函数,使用一个强大的API:ComponentStore.effect(),并使其独立!

createEffect()

使用此链接,您可以获得修改后的函数的代码。它很短,但如果你不能理解它是如何在引擎盖下工作的,请不要担心(这需要一些时间):你可以在这里阅读关于如何使用原始effect()方法的文档:NgRx Docs,并以同样的方式使用createEffect()。

如果不键入注释,它非常小:

function createEffect(generator) {
  const destroyRef = inject(DestroyRef);
  const origin$ = new Subject();
  generator(origin$).pipe(
    retry(),
    takeUntilDestroyed(destroyRef)
  ).subscribe();

  return ((observableOrValue) => {
    const observable$ = isObservable(observableOrValue)
      ? observableOrValue.pipe(retry())
      : of(observableOrValue);
    return observable$.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => {
      origin$.next(value);
    });
  });
}

它被命名为createEffect(),以不干扰Angular的effect()函数。

修改:

  1. createEffect() is a standalone function. Under the hood, it subscribes to an observable, and because of that createEffect() can only be called in an injection context. That’s exactly how we were using the original effect() method;
  2. createEffect() function will resubscribe on errors, which means that it will not break if you forget to add catchError() to your API request.

当然,您可以随意添加您的修改:)

把这个函数放在项目的某个地方,现在就可以管理应用程序状态,而不需要任何额外的库:Angular Signals+createEffect()。

Store类型

有三种类型的Store:

  • 全局存储(应用程序级)--应用程序中的每个组件和服务都可以访问;
  • 功能存储(“功能”级别)——某些特定功能的后代可以访问;
  • 本地存储(也称为“组件存储”)--不共享,每个组件都会创建一个新实例,当组件被销毁时,该实例将被销毁。

我编写了一个示例应用程序,向您展示如何使用Angular Signals和createEffect()实现每种类型的存储。我将使用该应用程序中的存储和组件(不带模板),让您看到本文中的代码示例。你可以在这里找到这个应用程序的全部代码:GitHub链接。

Global Store

@Injectable({ providedIn: 'root' })
export class AppStore {
  private readonly state = {
    $planes: signal<Item[]>([]),
    $ships: signal<Item[]>([]),
    $loadingPlanes: signal<boolean>(false),
    $loadingShips: signal<boolean>(false),
  } as const;

  public readonly $planes = this.state.$planes.asReadonly();
  public readonly $ships = this.state.$ships.asReadonly();
  public readonly $loadingPlanes = this.state.$loadingPlanes.asReadonly();
  public readonly $loadingShips = this.state.$loadingShips.asReadonly();
  public readonly $loading = computed(() => this.$loadingPlanes() || this.$loadingShips());

  constructor() {
    this.generateAll();
  }

  generateAll() {
    this.generatePlanes();
    this.generateShips();
  }

  private generatePlanes = createEffect(_ => _.pipe(
    concatMap(() => {
      this.state.$loadingPlanes.set(true);
      return timer(3000).pipe(
        finalize(() => this.state.$loadingPlanes.set(false)),
        tap(() => this.state.$planes.set(getRandomItems()))
      )
    })
  ));

  private generateShips = createEffect(_ => _.pipe(
    exhaustMap(() => {
      this.state.$loadingShips.set(true);
      return timer(3000).pipe(
        finalize(() => this.state.$loadingShips.set(false)),
        tap(() => this.state.$ships.set(getRandomItems()))
      )
    })
  ));
}

要创建全局存储,请添加以下装饰器:
@Injectable({ providedIn: ‘root’ })

在这里,你可以看到,每次你点击紫色的大按钮“Reload”,“飞机”和“飞船”这两个列表都会被重新加载。不同之处在于,“平面”将被连续加载,与您单击按钮的次数一样多。“Ships”将只加载一次,所有连续的点击都将被忽略,直到上一次请求完成。

字段$loading被称为“派生的”——它的值是使用compute()从其他信号的值中创建的。它是角信号中最强大的部分。与基于可观察的存储中的派生选择器相比,computed()具有一些优势:

  • 动态依赖项跟踪:在上面的代码中,当$loadingPlanes()返回true时,$loadingShips()将从依赖项列表中删除。对于非平凡的派生字段,它可能会节省内存;
  • 无毛刺,无脱落;
  • 懒惰的计算:派生值不会在它所依赖的信号的每次变化时重新计算,而是只有在读取该值时(或者如果生成的信号在effect()函数内部或在模板中使用)。

还有一个缺点:你无法控制依赖关系,它们都是自动跟踪的。

Feature Store

@Injectable()
export class PlanesStore {
  private readonly appStore = inject(AppStore);
  private readonly state = {
    $page: signal<number>(0),
    $pageSize: signal<number>(10),
    $displayDescriptions: signal<boolean>(false),
  } as const;

  public readonly $items = this.appStore.$planes;
  public readonly $loading = this.appStore.$loadingPlanes;
  public readonly $page = this.state.$page.asReadonly();
  public readonly $pageSize = this.state.$pageSize.asReadonly();
  public readonly $displayDescriptions = this.state.$displayDescriptions.asReadonly();

  public readonly paginated = createEffect<PageEvent>(_ => _.pipe(
    debounceTime(200),
    tap((event) => {
      this.state.$page.set(event.pageIndex);
      this.state.$pageSize.set(event.pageSize);
    })
  ));

  setDisplayDescriptions(display: boolean) {
    this.state.$displayDescriptions.set(display);
  }
}

该功能的根组件(或路由)应“提供”此存储:

@Component({
  // ...
  providers: [
    PlanesStore
  ]
})
export class PlanesComponent { ... }

不要将此存储添加到子代组件的提供程序中,否则,它们将创建自己的本地功能存储实例,这将导致令人不快的错误。

Local Store

@Injectable()
export class ItemsListStore {
  public readonly $allItems = signal<Item[]>([]);

  public readonly $page = signal<number>(0);

  public readonly $pageSize = signal<number>(10);

  public readonly $items: Signal<Item[]> = computed(() => {
    const pageSize = this.$pageSize();
    const offset = this.$page() * pageSize;
    return this.$allItems().slice(offset, offset + pageSize);
  });

  public readonly $total: Signal<number> = computed(() => this.$allItems().length);

  public readonly $selectedItem = signal<Item | undefined>(undefined);

  public readonly setSelected = createEffect<{
    item: Item,
    selected: boolean
  }>(_ => _.pipe(
    tap(({ item, selected }) => {
      if (selected) {
        this.$selectedItem.set(item);
      } else {
        if (this.$selectedItem() === item) {
          this.$selectedItem.set(undefined);
        }
      }
    })
  ));
}

与功能存储非常相似,组件应该为自己提供此存储:

@Component({
  selector: 'items-list',
  // ...
  providers: [
    ItemsListStore
  ]
})
export class ItemsListComponent { ... }

Component as a Store

如果我们的组件没有那么大,我们确信它不会那么大,而且我们只是不想为这个小组件创建一个存储区,该怎么办?

我有一个组件的例子,是这样写的:

@Component({
  selector: 'list-progress',
  // ...
})
export class ListProgressComponent {
  protected readonly $total = signal<number>(0);
  protected readonly $page = signal<number>(0);
  protected readonly $pageSize = signal<number>(10);

  protected readonly $progress: Signal<number> = computed(() => {
    if (this.$pageSize() < 1 && this.$total() < 1) {
      return 0;
    }
    return 100 * (this.$page() / (this.$total() / this.$pageSize()));
  });


  @Input({ required: true })
  set total(total: number) {
    this.$total.set(total);
  }

  @Input() set page(page: number) {
    this.$page.set(page);
  }

  @Input() set pageSize(pageSize: number) {
    this.$pageSize.set(pageSize);
  }

  @Input() disabled: boolean = false;
}

在Angular的版本17中,将引入input()函数来创建作为信号的输入,从而使此代码变得更短。

此示例应用程序部署在此处: GitHub Pages link.

您可以使用它来查看不同列表的状态是如何独立的,功能状态如何在功能的组件之间共享,以及所有组件如何使用应用程序全局状态中的列表。

在代码中,您可以找到对事件的反应、异步状态修改的排队、派生(计算)状态字段和其他详细信息的示例。

我知道我们可以改进代码,让事情变得更好——但这不是这个示例应用程序的重点。这里的所有代码只有一个目的:说明本文并解释事情是如何工作的。

我已经演示了如何在没有第三方库的情况下管理Angular应用程序状态,只使用Angular Signals和一个附加函数。

感谢您的阅读!

文章链接:

【Angular 开发】Angular 信号的应用状态管理 | 程序员云开发,云时代的程序员.

欢迎收藏  【全球IT瞭望】,【架构师酒馆】和【开发者开聊】.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/233503.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于PaddleOCR银行卡识别实现(四)之uni-app离线插件

目的 在前三篇文章中完成了银行卡识别整个模型训练等工作&#xff0c;通过了解PaddleOCR的端侧部署&#xff0c;我们也可以将银行卡号检测模型和识别模型移植到手机中&#xff0c;做成一款uni-app手机端离线银行卡号识别的应用。 准备工作 为了不占用过多篇幅&#xff0c;这…

内存学习——堆(heap)

目录 一、概念二、自定义malloc函数三、Debug运行四、heap_4简单分析4.1 heap管理链表结构体4.2 堆初始化4.3 malloc使用4.4 free使用 一、概念 内存分为堆和栈两部分&#xff1a; 栈&#xff08;Stack&#xff09;是一种后进先出&#xff08;LIFO&#xff09;的数据结构&…

class072 最长递增子序列问题与扩展【算法】

class072 最长递增子序列问题与扩展【算法】 code1 300. 最长递增子序列 // 最长递增子序列和最长不下降子序列 // 给定一个整数数组nums // 找到其中最长严格递增子序列长度、最长不下降子序列长度 // 测试链接 : https://leetcode.cn/problems/longest-increasing-subsequen…

【Java 基础】29 序列化

文章目录 1.定义2.目的3.使用1&#xff09;序列化2&#xff09;反序列化 3.应用场景4.注意事项总结 1.定义 序列化&#xff08;Serialization&#xff09;是将对象的状态转换为字节流的过程&#xff0c;以便将其存储到文件、数据库或通过网络传输 说简单点&#xff0c;序列化就…

关于DNS服务器地址总是127.0.0.1且无法解析域名地址

问题 笔者尝试nslookup解释域名时&#xff0c;出现服务器变成本地环回口地址&#xff0c;导致无法解析域名 C:\Users\Zsy>nslookup www.baidu.com 服务器: UnKnown Address: 127.0.0.1*** UnKnown 找不到 www.baidu.com: Server failed排查思路 尝试关闭虚拟网卡&#…

SQL语句的执行顺序怎么理解?

SQL语句的执行顺序怎么理解&#xff1f; 我们常常会被SQL其书写顺序和执行顺序之间的差异所迷惑。理解这两者的区别&#xff0c;对于编写高效、可靠的SQL代码至关重要。今天&#xff0c;让我们用一些生动的例子和场景来深入探讨SQL的执行顺序。 一、书写顺序 VS 执行顺序 SQ…

JS生成用户登录图形验证码

生成用户登录图形验证码的过程可以通过几个步骤来实现&#xff0c;包括创建画布&#xff0c;生成随机验证码文本&#xff0c;将验证码文本绘制到画布上&#xff0c;以及添加一些噪点和线条来增加复杂性。 HTML 首先&#xff0c;在HTML文件中创建一个<canvas>元素和一个…

c#生成二维码二维码中间添加定制LoGo

&#x1f680;介绍 &#x1f340;QRCoder是一个开源的.NET库&#xff0c;用于生成QR码&#xff08;Quick Response Code&#xff09;。这个库是用C#编写的&#xff0c;并且可以在.NET框架的各种版本上使用&#xff0c;包括.NET Framework, .NET Core, Mono, Xamarin等。QRCode…

深入解析Linux内核网络-拥塞控制系列(二)

上篇文章&#xff1a;深入解析Linux内核网络-拥塞控制系列(一&#xff09;对Linux内核网络中网络拥塞框架的框架进行了分析。本次针对具体的Cubic拥塞控制算法进行简单分析。在进行代码的梳理前&#xff0c;同样还是先来看一下相关概念、原理&#xff1a; 在上一篇文章中也提到…

电脑出现这些现象,说明你的固态硬盘要坏了

与传统机械硬盘&#xff08;HDD&#xff09;相比&#xff0c;固态硬盘&#xff08;SSD&#xff09;速度更快、更稳定、功耗更低。但固态硬盘并不是完美无瑕的&#xff0c;由于颗粒写入机制&#xff0c;可能会在七到十年的预期寿命之前出现故障。所以用户最好为最终故障做好准备…

vue3 自己写一个月的日历

效果图 代码 <template><div class"monthPage"><div class"calendar" v-loading"loading"><!-- 星期 --><div class"weekBox"><div v-for"(item, index) in dayArr" :key"index&q…

认识计算机的设备管理

在计算机系统中&#xff0c;除了处理器和内存之外&#xff0c;其他的大部分硬设备称为外部设备。它包括输入/输出设备&#xff0c;辅存设备及终端设备等。这些设备种类繁多&#xff0c;特性各异&#xff0c;操作方式的差异很大&#xff0c;从而使操作系统的设备管理变得十分繁杂…

数据仓库工具Hive

1. 请解释Hive是什么&#xff0c;它的主要用途是什么&#xff1f; Hive是一个基于Hadoop的数据仓库工具&#xff0c;主要用于处理和分析大规模结构化数据。它可以将结构化的数据文件映射为一张数据库表&#xff0c;并提供类似SQL的查询功能&#xff0c;将SQL语句转换为MapRedu…

使用 iperf 和 iftop 测试网络带宽

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

京东商品详情数据在数据分析行业中的重要性

京东商品详情数据在数据分析行业中具有重要作用。这些数据提供了丰富的信息&#xff0c;可以帮助企业了解市场趋势、消费者需求、产品表现以及运营策略等多个方面。 首先&#xff0c;京东商品详情数据可以为企业提供市场趋势分析的依据。通过观察商品的销售量、销售额、价格等…

Qt 6.5 类库实例大全:QObject

大家好&#xff0c;我是20YC小二&#xff01;福利时间&#xff1a;欢迎(wx)扫码关注&#xff0c;免费领取《C程序员入门必修第一课&#xff1a;C基础课程》在线视频教程&#xff0c;还有更多技术分享&#xff01;#下面进入今天内容# 1. QObject 介绍 QObject 是 Qt 库中最重要…

RocketMq集成SpringBoot(待完善)

环境 jdk1.8, springboot2.7.3 Maven依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from…

C++学习笔记:继承

继承 什么是继承?继承的写法基类和派生类的赋值转换继承中的作用域派生类的默认成员函数单继承,多继承,虚拟继承is-a 和 has-a 什么是继承? 继承是C语言面向对象的三大特性之一&#xff0c;是面向对象程序设计使代码可以复用的最重要的手段,基本都是在一个类的基础上为了增加…

一个简单的可视化的A星自动寻路

一个简单的应用场景&#xff0c;流程图连线 源码&#xff1a; addExample("A星路径查找", function () {return {template: <div><div ref"main"></div></div>,data() { return {}; },computed: {},methods: {},mounted() {var c…

2-3、运算符

语雀原文链接 文章目录 1、算术运算符2、关系运算符3、逻辑运算符4、赋值运算符5、移位运算符6、位运算符(二进制位进行运算)7、条件运算符:三目运算符8、运算符的优先级 1、算术运算符 &#xff1a;加法-&#xff1a;减法*&#xff1a;乘法/&#xff1a;除法取商%&#xff1…