我們都知道 Lodash,它是一個在項目中重用無狀態(tài)邏輯的庫。那么,如果在 Angular 項目中我們有一個類似的工具包來重用有狀態(tài)邏輯呢?
Composables 并不是一個新的概念,它是來自 Vue.js 的一個概念。我在這篇博客中使用的許多示例和想法直接來自 Vue.js Composables 文檔。
在版本 16.0.0-next.0 中,Angular 團隊引入了 Signals 的實現(xiàn),Signals 是一種反應性的基本組件,可以在 Angular 中提供精細的反應性能力。隨著這樣的重大變化,還考慮到 Angular 團隊在最新版本中引入的其他非常有用的功能,比如 inject 函數(shù)或 DestroyRef 的概念,不可避免地會出現(xiàn)新的模式。本文試圖在 Angular 的上下文中探索這個模式。
在 Angular 自身中,我們已經看到了我們可以稱之為“功能型服務(Functional Services)”的過渡。它始于版本 14.2.0 中功能型守衛(wèi)(functional guards)和解析器(resolvers)的引入,繼續(xù)于版本 15.0.0 中功能型攔截器(functional interceptors)的引入。但是什么是 Angular Composable,為什么以及如何在項目中使用它?
什么是 Angular Composable?
在 Angular 應用程序的上下文中,一個 composable 是一個使用 Signals API 封裝有狀態(tài)邏輯的函數(shù)。這些可組合函數(shù)可以在多個組件中重復使用,可以相互嵌套,并且可以幫助我們將組件的有狀態(tài)邏輯組織成小型、靈活和簡單的單元。
與我們創(chuàng)建 util 函數(shù)以在組件之間重用無狀態(tài)邏輯的方式相同,我們創(chuàng)建 composable 以共享有狀態(tài)邏輯。
但是讓我們看看在 Angular 應用程序中如何編寫一個 composable。在下面的示例中,我沒有使用 Angular Signals RFC 中提議的 API。當此 API 的所有功能就位時(例如應用程序渲染生命周期鉤子,基于 Signal 的查詢),我們將能夠以更好的方式編寫這些可組合函數(shù),并能夠為它們提供更多功能。
讓我們從一個非常簡單的示例開始。
使用 Signals 在 Angular Component 里實現(xiàn) mouse tracking 功能:
@Component({ standalone: true, template: ` {{ x() }} {{ y() }} `, }) export class MouseTrackerComponent implements AfterViewInit, OnDestroy { // injectables document = inject(DOCUMENT); // state encapsulated and managed by the composable x = signal(0); y = signal(0); ngAfterViewInit() { document.addEventListener('mousemove', this.update.bind(this)); } // a composable can update its managed state over time. update(event: MouseEvent) { this.x.update(() => event.pageX); this.y.update(() => event.pageY); } ngOnDestroy() { document.removeEventListener('mousemove', this.update.bind(this)); } }
可以對其重構,增加通用性:
// mouse-tracker.ts file export function useMouse() { // injectables const document = inject(DOCUMENT); // state encapsulated and managed by the composable const x = signal(0); const y = signal(0); // a composable can update its managed state over time. function update(event: MouseEvent) { x.update(() => event.pageX); y.update(() => event.pageY); } document.addEventListener('mousemove', update); // lifecycle to teardown side effects. inject(DestroyRef).onDestroy(() => document.removeEventListener('mousemove', update) ); // expose managed state as return value return { x, y }; }
其他 Component 也可以重用了:
@Component({ standalone: true, template: ` {{ mouse.x() }} {{ mouse.y() }} `, }) export class MouseTrackerComponent { mouse = useMouse(); }