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

news2024/11/18 13:36:27

自我介绍

  • 做一个简单介绍,年近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.coloradmin.cn/o/1298645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32基于USB串口通信应用开发

✅作者简介&#xff1a;热爱科研的嵌入式开发者&#xff0c;修心和技术同步精进&#xff0c; 代码获取、问题探讨及文章转载可私信。 ☁ 愿你的生命中有够多的云翳,来造就一个美丽的黄昏。 &#x1f34e;获取更多嵌入式资料可点击链接进群领取&#xff0c;谢谢支持&#xff01;…

使用selenium的edge浏览器登录某为

互联网上基本都是某哥的用法&#xff0c;其实edge和某哥的用法是一样的就有一下参数不一样。 一、运行环境 Python&#xff1a;3.7 Selenium&#xff1a;4.11.2 Edge&#xff1a;版本 120.0.2210.61 (正式版本) (64 位) 二、执行代码 from time import sleepfrom selenium…

Course2-Week4-决策树

Course2-Week4-决策树 文章目录 Course2-Week4-决策树1. 决策树的直观理解2. 构建单个决策树2.1 熵和信息增益2.2 构建决策树——二元输入特征2.3 构建决策树——多元输入特征2.4 构建决策树——连续的输入特征2.5 构建回归树——连续的输出结果(选修)2.6 代码实现-递归构建单个…

基于K-means与CNN的遥感影像分类方法

基于K-means与CNN的遥感影像分类 一、引言 1.研究背景 航天遥感技术是一种通过卫星对地观测获取遥感图像信息数据的技术&#xff0c;这些图像数据在各领域都发挥着不可或缺的作用。遥感图像分类主要是根据地面物体电磁波辐射在遥感图像上的特征&#xff0c;判断识别地面物体的属…

BUUCTF pwn rip WriteUp

文件分析 下载附件&#xff0c;分析文件 可以看到是64位ELF文件&#xff0c;elf可以理解为Linux中的可执行文件&#xff0c;就像Windows中的exe文件 用ida打开文件 查看main函数的伪代码&#xff0c;可以看到有一个15位的字符数组&#xff0c;该数组通过gets函数传值 还有一…

Jupyter notebook修改背景主题

打开Anaconda Prompt&#xff0c;输入以下内容 1. pip install --upgrade jupyterthemes 下载对应背景主题包 出现Successfully installed jupyterthemes-0.20.0 lesscpy-0.15.1时&#xff0c;说明已经下载安装完成 2. jt -l 查看背景主题列表 3. jt -t 主题名称&#xff08;…

AI:92-基于深度学习的红外图像人体检测

🚀 本文选自专栏:人工智能领域200例教程专栏 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的核心代码,详细讲解供大家学习,希望可以帮到大家。欢迎订阅支持,正在不断更新…

SQL命令---修改字段的数据类型

介绍 使用sql语句修改字段的数据类型。 命令 alter table 表明 modify 字段名 数据类型;例子 有一张a表&#xff0c;表里有一个id字段&#xff0c;长度为11。使用命令将长度修改为12 下面使用命令进行修改&#xff1a; alter table a modify id int(12) NOT NULL;下面使修…

【Dubbo3云原生微服务开发实战】「Dubbo前奏导学」 RPC服务的底层原理和实现

RPC服务 RPC服务介绍RPC通信模式RPC架构组成RPC技术要点RPC通信技术选项分析RPC实战开发6大基础组件基础组件之Guava基础组件之Hutools基础组件之ReflectionASM基础组件之FastJSON/FastJSON2基础组件之FST相比FastJSON的优势 基础组件之Commons-Codec RPC框架层面选项分析RPC组…

STM32-TIM定时器中断

目录 一、TIM&#xff08;Timer&#xff09;定时器简介 二、定时器类型 2.1基本定时器结构 2.2通用定时器结构 2.3高级定时器结构 三、定时中断基本结构 四、时序图分析 4.1 预分频器时序 4.2 计数器时序 4.3 计数器无预装时序&#xff08;无影子寄存器&#xff09; …

LVGL_V8.3入门二---实时时钟(模仿华为watch-UI)

系列文章目录 文章目录 系列文章目录前言一、实现效果二、代码解析 前言 在这个博客中&#xff0c;我们将深入探讨LVGL&#xff08;Light and Versatile Graphics Library&#xff09;版本8.3的实时时钟应用&#xff0c;以模仿华为 Watch UI 为例。LVGL是一款专为嵌入式系统和…

Jenkins简单介绍

学习目标 知道jenkins应用场景能够安装部署jenkins服务器能够实现gitgithubjenkins手动构建能够实现gitgitlabjenkins自动发布系统 认识jenkins Jenkins是一个可扩展的持续集成引擎&#xff0c;是一个开源软件项目&#xff0c;旨在提供一个开放易用的软件平台&#xff0c;使软…

Kafka集成springboot

安装kafka&#xff0c;直接到官网下载bin文件&#xff0c;本文使用windows进行使用kafka。 下载之后&#xff0c;第一步&#xff0c;启动zookeeper&#xff1a; zookeeper-server-start.bat ..\..\config\zookeeper.properties 第二步&#xff0c;启动kafka&#xff1a; kafka…

oops-framework框架 之 多语言设置文本、精灵和骨骼动画

引擎&#xff1a; CocosCreator 3.8.0 环境&#xff1a; Mac Gitee: oops-plugin-excel-to-json 注&#xff1a; 作者dgflash的oops-framework框架QQ群&#xff1a; 628575875 简介 作者dgflash在oops-framework的框架中提供了多语言&#xff0c;主要用于对文本、图片、骨骼动…

Verilog基础:寄存器输出的两种风格

相关文章 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 Verilog中的寄存器操作一般指的是那些对时钟沿敏感而且使用非阻塞赋值的操作。例如状态机中的状态转移&#xff0c;实际上就是一种寄存器操作&#xff0c;因为这相…

【docker 】centOS 安装docker

官网 docker官网 github源码 卸载旧版本 sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine 安装软件包 yum install -y yum-utils \device-mapper-persistent-data…

自下而上-存储全栈(TiDB/RockDB/SPDK/fuse/ceph/NVMe/ext4)存储技术专家成长路线

数字化时代的到来带来了大规模数据的产生&#xff0c;各行各业都面临着数据爆炸的挑战。 随着云计算、物联网、人工智能等新兴技术的发展&#xff0c;对存储技术的需求也越来越多样化。不同应用场景对存储的容量、性能、可靠性和成本等方面都有不同的要求。具备存储技术知识和技…

基于 Gin 的 HTTP 中间人代理 Demo

前面实现的代理对于 HTTPS 流量是进行盲转的&#xff0c;也就是说直接在 TCP 连接上传输 TLS 流量&#xff0c;但是我们无法查看或者修改它的内容。当然了&#xff0c;通常来说这也是不必要的。不过对于某些场景下还是有必要的&#xff0c;例如使用 Fiddler 进行抓包或者监控其…

Flink 本地单机/Standalone集群/YARN模式集群搭建

准备工作 本文简述Flink在Linux中安装步骤&#xff0c;和示例程序的运行。需要安装JDK1.8及以上版本。 下载地址&#xff1a;下载Flink的二进制包 点进去后&#xff0c;选择如下链接&#xff1a; 解压flink-1.10.1-bin-scala_2.12.tgz&#xff0c;我这里解压到soft目录 [ro…

redis之缓存穿透,击透,雪崩~

以下为一个我们正常的缓存流程&#xff1a; 缓存雪崩&#xff1a; 在双十一的时候&#xff0c;淘宝的首页访问量是非常大的&#xff0c;所以它的很多数据是放在redis缓存里面&#xff0c;对应redis中的key&#xff0c;假设设置了缓存失效的时间为3小时&#xff0c;超过这三个小…