状态管理设计
本章节的目标是:让习惯了「在组件里随手写 ref/Reactive 或 useState」的前端同学,也能快速理解在这个 Angular 架构里状态应该放在哪儿、怎么流动。
1. 我们到底在管理哪些“状态”?
以 cross-platform-app 为例,常见状态大致可以分为三类:
- 页面局部 UI 状态:表单输入值、开关是否选中、当前激活 Tab 等。
- 模块业务状态:当前菜单列表、用户列表、搜索条件、分页信息等。
- 应用级状态:当前语言、全局 Loading 是否显示、登录用户信息、数据库连接状态等。
直觉上可以这么记:
- 离页面越近 → 越适合放在组件内部或模块 service 中;
- 越跨模块 / 越全局 → 越适合交给
core/shared层的服务。
2. 轻量状态:用 Service + RxJS 代替 “全局 store 一把梭”
在很多 Vue3 项目里,默认方案是「所有状态都丢到 Pinia/Vuex 里」;久而久之 store 变成「垃圾桶」。
这里推荐的做法是:
- 每个 Feature 模块拥有自己的 Service 作为「小 store」;
- 真正全局的状态,才放到
core/services或shared的抽象基类中。
2.1 示例:菜单模块的状态流
粗略流程:
HomePage通过HomeService发起菜单同步请求;HomeService调用数据库服务,把菜单写入本地 SQLite;MenuService通过查询服务统一读取/过滤菜单,维护当前列表与搜索条件;- 页面只需要订阅
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 合并冲突。