Skip to main content

状态管理设计

本章节的目标是:让习惯了「在组件里随手写 ref/Reactive 或 useState」的前端同学,也能快速理解在这个 Angular 架构里状态应该放在哪儿、怎么流动。

1. 我们到底在管理哪些“状态”?​

以 cross-platform-app 为例,常见状态大致可以分为三类:

  1. 页面局部 UI 状态:表单输入值、开关是否选中、当前激活 Tab 等。
  2. 模块业务状态:当前菜单列表、用户列表、搜索条件、分页信息等。
  3. 应用级状态:当前语言、全局 Loading 是否显示、登录用户信息、数据库连接状态等。

直觉上可以这么记:

  • 离页面越近 → 越适合放在组件内部或模块 service 中;
  • 越跨模块 / 越全局 → 越适合交给 core/shared 层的服务。

2. 轻量状态:用 Service + RxJS 代替 “全局 store 一把梭”​

在很多 Vue3 项目里,默认方案是「所有状态都丢到 Pinia/Vuex 里」;久而久之 store 变成「垃圾桶」。

这里推荐的做法是:

  • 每个 Feature 模块拥有自己的 Service 作为「小 store」;
  • 真正全局的状态,才放到 core/services 或 shared 的抽象基类中。

2.1 示例:菜单模块的状态流​

粗略流程:

  1. HomePage 通过 HomeService 发起菜单同步请求;
  2. HomeService 调用数据库服务,把菜单写入本地 SQLite;
  3. MenuService 通过查询服务统一读取/过滤菜单,维护当前列表与搜索条件;
  4. 页面只需要订阅 MenuService 暴露出来的可观察数据(或在本项目中使用 request + Promise 方式)。

2.2 对比一个简单的 Vue3 写法​

传统写法(Vue3 + Pinia 示例):

// stores/menu.ts
export const useMenuStore = defineStore('menu', {
state: () => ({ list: [] as MenuItem[], keyword: '' }),
actions: {
async fetchAll() {
const res = await axios.get('/api/menu');
this.list = res.data;
},
},
});

// 组件中
const menuStore = useMenuStore();
onMounted(() => menuStore.fetchAll());

优点是简单,但问题在于:

  • store 状态默认就是全局的,所有页面都可以随意读写;
  • 对于中大型项目,很容易出现「谁在改这个状态?」完全说不清。

本架构中的写法(简化示例):

// features/menu/services/menu.service.ts
@Injectable({ providedIn: 'root' })
export class MenuService extends AbstractAppService {
private menus$ = new BehaviorSubject<Menu[]>([]);

async syncFromRemote(): Promise<void> {
const result = await this.request<Menu[]>(AppUrl.Menu.Sync);
if (result.code === '200') {
// 写入本地 DB 省略...
this.menus$.next(result.data);
}
}

observeMenus(): Observable<Menu[]> {
return this.menus$.asObservable();
}
}

// HomePage 中
export class HomePage extends AbstractAppPage implements OnInit {
menus: Menu[] = [];

constructor(private readonly menuService: MenuService) {
super();
}

async ngOnInit() {
this.menuService.observeMenus().subscribe(list => this.menus = list);
await this.menuService.syncFromRemote();
}
}

这里 MenuService 就是这个模块的「小 store」。它并不是全局可见,而是只对需要它的模块开放。

3. 应用级状态:事件总线 + 抽象基类​

某些状态本身不是「数据」,而是「系统状态/信号」,例如:

  • 是否展示全局 Loading 遮罩;
  • 是否需要弹出某个全局 Dialog;
  • 全局语言、主题等。

这类状态在项目中通过 事件总线 + 抽象基类 来管理(在前面的文档中已经详细展开,这里只从状态角度再看一遍)。

你不需要在每个页面维护 isLoading,也不需要在每个地方手写 MatDialog 或 ElMessage。状态在一个地方产生,通过总线分发给真正关心它的组件。

4. 何时需要「更重」的状态管理(类似 Vuex/NgRx)?​

当前架构默认不强制使用 NgRx 这类重量级方案,而是遵循这样一条经验:

只要「单个模块 + 少量全局状态」能解决的,就先用 Service + 事件总线;
当你发现状态已经跨越多个模块、多个页面,而且「需要时间旅行调试 / 严格不可变约束」时,再考虑引入 NgRx。

可以将两种方式粗略对比成:

场景推荐方案
模块内部的列表、表单、搜索条件对应模块的 Service(如 MenuService)
全局 Loading、语言、用户信息等core/shared 中的服务 + 事件总线
金融/交易类应用,需要时间旅行调试可在此架构之上再集成 NgRx 等方案

5. 小结:对传统 Vue3 状态管理的改进点​

从「心智模型」上可以这样理解:

  • Vue3 + Pinia:倾向于一个或少数几个全局仓库,任何组件随时可以读写。
  • 当前架构:鼓励为每个模块建立自己的「小仓库(Service)」,并把真正全局的状态交给 core/shared 层统一管理。

好处包括:

  • 边界更清楚:看到某个 Service,就知道它只属于哪个模块,不会被乱用。
  • 更靠近后端分层思路:模块 service 类似 Java 里的 application/service 层,core service 类似 infrastructure 层。
  • 更利于团队协作:不同人可以各自负责一个模块,减少全局 store 合并冲突。