Akita状态管理实战:高效构建可维护的前端应用

欢欢的笔记 框架 阅读 2,402
赞 10 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

去年底接手一个中后台管理系统重构,前端技术栈要从老旧的 jQuery + 自研状态管理迁移到现代框架。团队里有人推 NgRx,有人想用 Redux Toolkit,但考虑到项目规模不大(也就 10 个左右的核心模块),加上我对 Angular 比较熟,最后选了 Akita —— 它轻量、API 简洁,文档也够直白,特别适合不想折腾复杂状态流的小团队。

Akita状态管理实战:高效构建可维护的前端应用

说实话,一开始没想太多,就觉得它比 NgRx 少写一堆 boilerplate,store、query、service 三件套搭起来快得很。但真上手后才发现,有些坑藏得挺深。

最大的坑:性能问题

项目做到一半,测试反馈说列表页滚动卡顿,数据量大一点就明显掉帧。我一开始以为是 Angular 的变更检测问题,结果排查半天发现是 Akita 的 store 更新触发了太多不必要的重渲染。

具体来说,我们有个 ProductStore,里面存了上千条商品数据。每次用户筛选或排序,都会调用 update() 方法更新整个数组。虽然 Akita 内部用了 immutable 更新,但 Angular 组件订阅了 select() 后,只要 store 变了,整个列表就重新渲染 —— 即使只有排序顺序变了,数据本身没变。

折腾了半天,发现关键在于 如何让组件只响应真正需要变化的部分。Akita 的 select 默认是引用比较,但如果你每次 update 都传入新数组(哪怕内容一样),引用就变了,订阅就会触发。

后来我改成了这样:在 service 里做缓存和 diff,只在必要时才更新 store:

// product.service.ts
import { Injectable } from '@angular/core';
import { ProductStore } from './product.store';
import { tap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class ProductService {
  private lastSortedData: any[] = [];

  constructor(private productStore: ProductStore) {}

  sortProducts(sortKey: string) {
    const currentData = this.productStore._value(); // 注意:这是内部 API,慎用
    const sorted = [...currentData].sort((a, b) => a[sortKey] > b[sortKey] ? 1 : -1);

    // 关键:只在排序结果真的变化时才更新 store
    if (JSON.stringify(sorted) !== JSON.stringify(this.lastSortedData)) {
      this.productStore.update({ products: sorted });
      this.lastSortedData = sorted;
    }
  }
}

这里用了 JSON.stringify 做浅比较,虽然性能不完美,但对我们的数据量(<5000 条)够用。当然,更优雅的做法是用 Lodash 的 isEqual,或者自己写个 diff 算法,但时间紧,先这么凑合了。

嵌套状态更新的恶心事

另一个头疼的问题是嵌套对象更新。比如用户资料里有个 settings 对象,里面又有 themelang 等字段。Akita 的 update 默认是 shallow merge,直接写:

this.userStore.update({ settings: { theme: 'dark' } });

会把整个 settings 覆盖掉,其他字段就没了。这谁顶得住?

官方文档其实提到了用 upsert 或者传函数,但一开始没注意。后来改成这样:

this.userStore.update(state => ({
  ...state,
  settings: {
    ...state.settings,
    theme: 'dark'
  }
}));

虽然啰嗦点,但安全。为了少写这种样板代码,我还封装了个工具函数:

// utils.ts
export function deepUpdate<T extends Record<string, any>>(
  state: T,
  path: string,
  value: any
): T {
  const keys = path.split('.');
  let result = { ...state };
  let current = result;

  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];
    current[key] = { ...current[key] };
    current = current[key];
  }

  current[keys[keys.length - 1]] = value;
  return result;
}

用的时候:

this.userStore.update(state => deepUpdate(state, 'settings.theme', 'dark'));

虽然不是最高效的,但至少避免了手写三层展开。不过要注意,这个函数只处理简单嵌套,循环引用或复杂结构会炸,好在我们业务里没遇到。

最终的解决方案

综合下来,我们定了几条规则:

  • 所有 store 更新必须通过 service,禁止组件直接调用 store
  • 大数组更新前做 diff,避免无意义的引用变更
  • 嵌套对象更新统一用 deepUpdate 工具函数
  • 组件里用 select 时尽量 select 具体字段,别整个 state 都 subscribe

比如列表组件,不再这样写:

// ❌ 别这么干
this.products$ = this.productQuery.select();

而是:

// ✅ 只取需要的
this.products$ = this.productQuery.select('products');
this.loading$ = this.productQuery.select('loading');

这样即使其他字段(比如 filters)变了,列表也不会重渲染。

回顾与反思

用 Akita 整体体验还是不错的,比 NgRx 轻快太多,学习成本低,适合中小项目。但它的“简单”也意味着你要自己处理很多边界情况,比如上面说的性能和嵌套更新。

做得好的地方:

  • store 结构清晰,每个实体一个 store,职责分明
  • 配合 Angular 的 async pipe,数据流很直观
  • devtools 插件够用,调试状态变化不费劲

还能优化的点:

  • 那个 JSON.stringify 的 diff 方案其实有隐患,极端情况下可能漏更新(比如对象里有函数或 undefined),但目前没出问题,先放着
  • 没有内置的 undo/redo 机制,如果业务需要,得自己实现,有点麻烦
  • store 之间通信不太方便,我们最后用了一个全局 event bus 来解耦,感觉有点倒退

总的来说,Akita 不是银弹,但它在“够用”和“简洁”之间找到了一个不错的平衡点。如果你项目不大,又不想被 NgRx 的模板代码搞疯,Akita 值得试试。但记住,别以为它能自动解决所有性能问题,该做的优化一样不能少。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的 Akita 实践方案,欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论