在深入了解了各种状态管理模式和现成的库(NgRx, Akita…)之后,你是否会产生一种敬畏,觉得它们内部的实现高深莫测,是只有框架作者才能触及的「黑魔法」?
今天,我们就来亲手打破这个迷思。雪狼将手把手带你,仅用几十行代码,打造一个我们专属的、可复用的、通用的状态管理基类 —— 一个迷你版的「NgRx」。
这个过程,不仅会让你拥有一个属于自己的实用工具,更重要的是,它将让你彻底洞悉所有状态管理库背后的核心思想,完成从「知识消费者」到「模式创造者」的蜕变。
我们的目标:一个可复用的 Store 基类#
我们不希望每个需要管理状态的服务(UsersStore, ProductsStore…)都重复去写状态管理的样板代码。我们想要一个 Store<T> 基类,它能帮我们处理掉所有「脏活累活」。任何具体的业务 Store,只需继承它,然后专注于自己的业务逻辑即可。
这个基类需要具备:
-
一个封装好的、私有的状态容器。
-
一个可供外部订阅的、公开的只读状态视图。
-
一套安全、规范的内部状态更新机制。

V1:经典的 RxJS 版本#
第一步:搭建骨架,封装 BehaviorSubject#
// rxjs-store.ts
import { Observable, BehaviorSubject } from 'rxjs';
export abstract class RxjsStore<T> {
private readonly stateSource: BehaviorSubject<T>;
readonly state$: Observable<T>;
constructor(initialState: T) {
this.stateSource = new BehaviorSubject<T>(initialState);
this.state$ = this.stateSource.asObservable();
}
getState(): T {
return this.stateSource.getValue();
}
protected setState(newState: Partial<T>): void {
const currentState = this.getState();
const nextState = { ...currentState, ...newState };
this.stateSource.next(nextState);
}
}通过 private readonly 和 asObservable(),我们建立了一道「防火墙」,确保了外界只能「看」(订阅),不能「摸」(直接调用 .next())。
第二步:学以致用,创建 TodosStore#
// todos.store.ts
import { Injectable } from '@angular/core';
import { RxjsStore } from './rxjs-store';
// ... 定义 TodosState, Todo 接口
@Injectable({ providedIn: 'root' })
export class TodosStore extends RxjsStore<TodosState> {
constructor() {
super({ todos: [], filter: 'ALL' });
}
addTodo(text: string): void {
const newTodo: Todo = { id: Date.now(), text, completed: false };
this.setState({
todos: [...this.getState().todos, newTodo],
});
}
// ... 其他方法
}TodosStore 只需专注于自己的业务逻辑,所有通用的状态管理机制,都已经被 RxjsStore 基类完美封装了。
V2:更现代的 Signal 版本#
在 Signals 时代,我们也可以打造一个 Signal-based 的 Store 基类,它更简单、更现代。
// signal-store.ts
import { signal, computed, Signal, WritableSignal } from '@angular/core';
export abstract class SignalStore<T> {
private readonly stateSource: WritableSignal<T>;
readonly state: Signal<T>;
constructor(initialState: T) {
this.stateSource = signal<T>(initialState);
this.state = this.stateSource.asReadonly();
}
protected patchState(newState: Partial<T>): void {
this.stateSource.update(currentState => ({ ...currentState, ...newState }));
}
}对比 V1,我们用 signal 替换了 BehaviorSubject,用 asReadonly() 替换了 asObservable(),用 update 和 patchState 替换了 setState。理念相通,但 API 更为简洁。
使用它来创建 TodosSignalStore:
// todos-signal.store.ts
@Injectable({ providedIn: 'root' })
export class TodosSignalStore extends SignalStore<TodosState> {
constructor() {
super({ todos: [], filter: 'ALL' });
}
// --- Selectors 现在是 computed signals ---
readonly filteredTodos = computed(() => {
const s = this.state(); // 获取当前状态
const { todos, filter } = s;
if (filter === 'ALL') return todos;
return filter === 'ACTIVE'
? todos.filter(t => !t.completed)
: todos.filter(t => t.completed);
});
// --- Actions ---
addTodo(text: string): void {
const newTodo: Todo = { id: Date.now(), text, completed: false };
this.patchState({ todos: [...this.state().todos, newTodo] });
}
// ...
}Selector 自然地变成了 computed 信号,实现了高效的、自动的缓存和派生。
结语#
恭喜你!你刚刚亲手打造了两个版本的、功能完备、可复用的状态管理解决方案。它们都包含了「单一数据源」、「不可变更新」、「Action」、「Selector」等所有现代状态管理的核心思想。
这个过程告诉我们,那些看似高大上的库,其底层原理也是由这些朴素的基础构建而成的。通过亲手实现,我们不仅收获了一个工具,更收获了一份洞察力。从现在起,你已经不再是一个单纯的「库的使用者」,你已经拥有了「模式的创造者」的视野与能力。
正如古人所言:「透过现象看本质。」 —— 意指通过观察事物的表象,从而洞察其内在的、根本的规律。我们通过亲手实现状态管理基类,正是践行了这一智慧,从「使用」的表象深入到「设计」的本质,真正掌握了核心精髓。