【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.mzph.cn/news/210579.shtml

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

相关文章

GO面试题系列

1.GO有哪些关键字 2.GO有哪些数据类型 3.Go方法与函数的区别 在Go语言中&#xff0c;方法和函数是两个不同的概念&#xff0c;尽管它们在某些方面有相似之处。下面是它们的主要区别&#xff1a; 定义位置&#xff1a; 函数&#xff1a; 函数是独立声明的&#xff0c;它们不…

python数据分析总结(pandas)

目录 前言 df导入数据 df基本增删改查 数据清洗 ​编辑 索引操作 数据统计 行列操作 ​编辑 df->types 数据格式化 ​编辑 日期数据处理 前言 此篇文章为个人python数据分析学习总结&#xff0c;总结内容大都为表格和结构图方式&#xff0c;仅供参考。 df导入数…

Vue3使用vue-baidu-map-3x百度地图

安装vue-baidu-map-3x&#xff1a; // vue3 $ npm install vue-baidu-map-3x --save// vue2 $ npm install vue2-baidu-map --save 全局注册/局部注册&#xff1a; import { createApp } from vue import App from ./App.vue import BaiduMap from vue-baidu-map-3xconst app …

综述 2017-Genome Biology:Alignment-free sequence comparison

Zielezinski, Andrzej, et al. "Alignment-free sequence comparison: benefits, applications, and tools." Genome biology 18 (2017): 1-17. https://genomebiology.biomedcentral.com/articles/10.1186/s13059-017-1319-7 被引次数&#xff1a;476应用问题&…

curl 18 HTTP/2 stream

cd /Users/haijunyan/Desktop/CustomKit/KeepThreadAlive/KeepThreadAlive //Podfile所在文件夹 git config --global https.postBuffer 10485760000 git config --global http.postBuffer 10485760000 pod install https://blog.csdn.net/weixin_41872403/article/details/86…

top K问题(借你五分钟)

目录 前言 top K问题 模拟数据 建堆 验证&#xff08;简单了解即可&#xff09; 最终代码 调试部分 前言 在大小堆的实现&#xff08;C语言&#xff09;中我们讨论了堆的实际意义&#xff0c;在看了就会的堆排序&#xff08;C语言&#xff09;中我们完成了堆排序&#…

银河麒麟本地软件源配置方法

软件源介绍 软件源可以理解为软件仓库&#xff0c;当需要安装软件时则会根据源配置去相应的软件源下载软件包&#xff0c;此方法的优点是可以自动解决软件包的依赖关系。常见的软件源有光盘源、硬盘源、FTP源、HTTP源&#xff0c;本文档主要介绍本地软件源的配置方法&#xff…

功能强大的屏幕录制和剪辑工具Camtasia Studio 2024 中文版

Camtasia Studio 2024 是一款功能强大的屏幕录像工具&#xff0c;集视频录制、剪辑、编辑和播放于一体的多功能屏幕录制软件&#xff0c;Camtasia Studio 2024操作简单&#xff0c;它能够轻松为您将屏幕上的所有声音、影音、鼠标移动的轨迹和麦克风声音全部录制下来&#xff0c…

分布式架构原理与实践读书笔记

分布式架构原理与实践读书笔记 IT 软件架构的更迭&#xff1a;从单体架构&#xff0c;到集群架构&#xff0c;到现在的分布式和微服务架构。 分布式架构具有分布性、自治性、并行性、全局性等特点。 为了应对请求的高并发和业务的复杂性&#xff0c;需要对应用服务进行合理拆…

使用Jmeter做性能测试的注意点

一、性能测试注意点 1. 用jmeter测试时使用BeanShell脚本获取随机参数值&#xff0c;会导致请求时间过长&#xff0c;TPS过低。应改为使用csv读取参数值&#xff0c;记录的TPS会更加准确。 注&#xff1a;进行性能测试时&#xff0c;应注意会影响请求时间的操作&#xff0c;尽量…

1-4、JDK目录结构

语雀原文链接 文章目录 1、目录结构2、JDK中rt.jar、tools.jar和dt.jar作用3、bin目录部分说明&#xff08;基本工具&#xff09; 1、目录结构 bin目录&#xff1a;包含一些用于开发Java程序的工具&#xff0c;例如&#xff1a;编译工具(javac.exe)、运行工具 (java.exe) 、打…

菜鸟学习日记(python)——循环语句

python中的循环语句包括for循环语句和while循环语句&#xff0c;但是python中是没有do...while循环语句的。 while循环语句 while循环语句的一般格式为; while condition:loop body condition是循环判断条件&#xff0c;loop body是循环体。 当循环条件成立时&#xff0c;…

基于ssm的彩妆小样售卖商城的设计与实现论文

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于彩妆小样售卖商城当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了彩妆小样售卖商城&#xff0c;它彻底改变了过…

Leetcode—231.2的幂【简单】

2023每日刷题&#xff08;五十四&#xff09; Leetcode—231.2的幂 实现代码 class Solution { public:bool isPowerOfTwo(int n) {if(n < 0) {return false;}long long ans 1;while(ans < n) {ans * 2;}if(ans n) {return true;}return false;} };运行结果 之后我会…

时间序列预测专栏介绍 — 算法原理、源码解析、项目实战

专栏链接&#xff1a;https://blog.csdn.net/qq_41921826/category_12495091.html 专栏内容 所有文章提供源代码、数据集、效果可视化 文章多次上热搜榜单 时间序列预测存在的问题 现有的大量方法没有真正的预测未来值&#xff0c;只是用历史数据做验证 利用时间序列分解算法存…

【Vue第3章】使用Vue脚手架_Vue2

目录 3.1 初始化脚手架 3.1.1 说明 3.1.2 具体步骤 3.1.3 模板项目的结构 3.1.4 笔记与代码 3.1.4.1 笔记 3.1.4.2 01_src_分析脚手架 3.2 ref与props 3.2.1 ref 3.2.2 props 3.2.3 笔记与代码 3.2.3.1 笔记 3.2.3.2 02_src_ref属性 3.2.3.3 03_src_props配置 3…

根据应聘者的姓名和所学专业判断是否需要这样的程序设计人员

一、程序分析 导入Scanner函数&#xff0c;分别输入应聘者的姓名和应聘者所学的程序设计语言。 二、具体代码 import java.util.Scanner; public class Recruitment {public static void main(String[] args){try (Scanner scan new Scanner(System.in)) {System.out.prin…

Spring Boot 3 整合 Mybatis-Plus 实现动态数据源切换实战

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

m_map导入本地地形数据

m_map绘制地形图时&#xff0c;虽然自带有1的地形图以及从NOAA下载的1分的地形图&#xff08;详见&#xff1a;Matlab下地形图绘图包m_map安装与使用&#xff09;&#xff0c;但有时需要对地形图分辨率的要求更高&#xff0c;便无法满足。 此时&#xff0c;需要导入本地地形数…

算法Day22 星南二楼(最长升序子序列)

星南二楼&#xff08;最长升序子序列&#xff09; Description Input Output Sample 代码 import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int[] grid new int[n];for(int j0;j&l…