【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.pswp.cn/news/210579.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/210579.shtml
英文地址,請注明出處:http://en.pswp.cn/news/210579.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

智能機器人在新材料方面遇到的挑戰

智能機器人在新材料方面面臨的挑戰包括但不限于以下幾點&#xff1a; 新材料的研發&#xff1a;機器人需要使用新材料來提高其性能和功能。然而&#xff0c;新材料的研發需要大量的時間和資金&#xff0c;同時還需要具備高超的技術和專業知識. 材料的可靠性&#xff1a;機器人…

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…

linux命令積累

1.查找指定目錄下第二層目錄&#xff0c;一年前的文件 find $dir -maxdepth 1 -type d -mtime 365 2./data/att/dir1軟連接到/data1/att/dir1 硬連接和軟連接的區別 硬連接 ln file1 file2 1.硬連接不能對目錄進行鏈接。 2.硬連接修改一個文件&#xff08;不論修改哪方文件&…

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;需要對應用服務進行合理拆…

springboot(ssm暢游游戲銷售平臺 游戲電商系統Java系統

springboot(ssm暢游游戲銷售平臺 游戲電商系統Java系統 開發語言&#xff1a;Java 框架&#xff1a;ssm/springboot vue JDK版本&#xff1a;JDK1.8&#xff08;或11&#xff09; 服務器&#xff1a;tomcat 數據庫&#xff1a;mysql 5.7&#xff08;或8.0&#xff09; 數…

使用Jmeter做性能測試的注意點

一、性能測試注意點 1. 用jmeter測試時使用BeanShell腳本獲取隨機參數值&#xff0c;會導致請求時間過長&#xff0c;TPS過低。應改為使用csv讀取參數值&#xff0c;記錄的TPS會更加準確。 注&#xff1a;進行性能測試時&#xff0c;應注意會影響請求時間的操作&#xff0c;盡量…

[JVM 基礎 - Java 類加載機制]

這篇文章將帶你深入理解Java 類加載機制。 JVM 基礎 - Java 類加載機制 類的生命周期 類的加載: 查找并加載類的二進制數據連接 驗證: 確保被加載的類的正確性準備: 為類的靜態變量分配內存&#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;它徹底改變了過…

RUST博客帖子編輯示例

狀態模式&#xff08;state pattern&#xff09;是一種面向對象的設計&#xff0c;它的關鍵點在于&#xff1a;一個值擁有的內部狀態由數個狀態對象&#xff08;state object&#xff09;表的而成&#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;只是用歷史數據做驗證 利用時間序列分解算法存…