zoukankan      html  css  js  c++  java
  • 使用 Immutable Subject 来驱动 Angular 应用

    现状

    最近在重构手上的一个 Angular 项目,之前是用的自己写的一个仿 Elm 架构的库来进行的状态管理,期间遇到了这些痛点:

    1. 样板代码太多
    2. 异步处理太过繁琐
    3. 需要单独维护一个 npm 包

    其中,一、二两点是促使我重构的原因,第三点是促使我更换状态管理方案的理由(太懒了,根本不想去维护这个项目)。

    样板代码太多

    Elm 架构中,有下面几个重要的概念:

    1. Message(在 Redux 中被称作 Action)
    2. Update(在 Redux 中被称作 Reducer)
    3. Model

    这三个概念用 Elm 代码来实现显得非常优雅:

    1. Message:可区分联合(Discriminated Union),还可以在单独的 Case 中使用元祖来内联复杂数据结构
    2. Update:对可区分联合使用模式匹配
    3. Model:不可变记录类型(Record Type),方便基于当前的值生成新的值

    但是 JS 并没有这些语言特性,大部分的行为需要靠大量额外的代码来模拟,而且模拟的效果也只是差强人意。

    异步处理太过繁琐

    在处理异步操作的时候,我们往往需要处理三种状态:请求中、成功、失败,这放在 Elm 中,往往需要三个 Message 才能进行完整的描述,而且还需要 3 个对应的 Update 分支,用 Elm 来写的话其实也还好,但是如果要用 JS 来实现的话,那可真是让人头大。

    备选方案

    NgRx

    这是 Angular 生态中最强大的专门用来做状态管理的库了,流行度可以排上 No.1。各种各样的配套设施非常齐全,而且对 Redux 用户、RxJS 用户比较友好,不过这两个优点也就意味着它对新手不太友好,而且代码量与我现在的方案相比也很难减少。同样的,它所支持的全局单一状态让我不是特别感冒,如果可以的话,我更希望能够使用模块单一状态。

    NgXs

    这是一个简化版的 NgRx 库,相较于 NgRx 来说,它要显得简洁很多,但是还是免不了写那么多的样板代码(Action 之类的东西),而且同样也不支持我想要的模块单一状态。

    akita

    这是一个对框架无依赖的状态管理库,但是从样例代码风格上来看,更适合 OOP 风格的 Angular。它支持多个 Store,也支持在任意位置修改 Store(官方建议在一个专门的 Service 中修改 Store),这两点让它看起来更像是一个 OO 的 MobX。在最开始接触到这个库的时候,我已经非常心动了,基本上满足了我的大多数需求,不过在稍微进行了一些实践过后,还是选择了放弃,因为太过 OO,所以样板代码又通过另一种方式膨胀了起来。

    MobX

    MobX 理念我非常喜欢,所以我原本是很想把 MobX 跟 rxjs 搭配使用的,但是 MobX 来到 Angular 应用中后显得有些水土不服,在很多地方都需要手动的将把 RxJS 与 MobX 进行来回的转换,这大大的增加了我的重构成本,最终只能作罢。

    最终方案

    最终,我没有选择以上任何一种备选方案,而是选择使用 immutable 与 RxJS 来混搭一个状态管理方案—— ImmutableSubject

    其源码如下所示:

    import { BehaviorSubject } from 'rxjs';
    
    export class ImmutableSubject<TImmutable> extends BehaviorSubject<TImmutable> {
        update(action: (value: TImmutable) => TImmutable) {
            this.next(action(this.value));
        }
    }
    

    我仅仅只是为 BehaviorSubject 做了一点点的拓展,让调用方更方便地修改 Store。使用起来就像下面这样:

    import { Injectable } from '@angular/core';
    import { ImmutableSubject } from './ImmutableSubject';
    import { List } from 'immutable';
    import { BlogCategoryEditDto } from '../models/BlogCategoryEditDto.model';
    import { CategoriesService } from '../api/categories.service';
    import { BlogCategoryType } from '../models/BlogCategoryType.enum';
    
    @Injectable()
    export class CategoryStore {
        constructor(
            private cateSvc: CategoriesService
        ) { }
        
        /// ======= Queries =======
        
        $categories = new ImmutableSubject(List<BlogCategoryEditDto>());
        
        /// ======= Actions =======
    
        refreshList(cateType: BlogCategoryType) {
            this.cateSvc.list(cateType).subscribe(categories => {
                this.$categories.update(x => {
                    return List(categories);
                });
            });
        }
    
        addCategory(dto: BlogCategoryEditDto) {
            this.$categories.update(x => x.push(dto));
        }
    
        deleteCategory(categoryId: number) {
            this.$categories.update(x => x.filterNot(c => c.categoryId === categoryId));
        }
    
        updateCategory(dto: BlogCategoryEditDto) {
            this.$categories.update(x => x.map(c => c.categoryId === dto.categoryId ? dto : c));
        }
    }
    

    Store 类中使用 ImmutableSubject 作为真正存储数据的地方,同时使用两行注释将查询与修改进行分离开来,从形式上极大地减少了样板代码的出现。

    对于异步操作来说,可以直接让 Action 返回一个 Observable 来指示异步处理的过程,而 Query 本身就是 Observable ,所以可以自然而然的获得处理异步的能力,而这并不需要添加太多额外的代码。

    Immutable 为我们提供了大量使用的不可变数据结构,可以在 JS 中实现 Record Type 大部分的特性。

    Immutable 与 RxJS 都是非常出名的第三方库,所以根本不需要费心去维护升级,而且它们的维护者也非常的可靠,在未来很长一段时间内它们也都可以获得及时的更新。

    尽管我的 ImmutableSubject 看起来非常的简陋,但他确实可以在一定的约束下为我解决问题。在使用 ImmutableSubject 的时候需要遵守以下约定:

    1. Store 中的状态应该是 immutable
    2. 对状态的修改只能在 Store 类的 Actions 中使用 update 方法进行
    3. Query 应该总是使用 pipeImmutableSubject 中派生
    4. 不再使用的 ImmutableSubject 需要手动释放

    关于 EventSourcing

    使用类似 Elm 架构的一个好处就是可以使用“时间旅行”功能,因为所有对状态的修改都会被记录下来,或者说,当前状态就是由历史操作记录积累而成(即事件溯源)。不过这个功能我并不需要,对于我现在的应用规模来说,事件溯源不仅不能直接解决任何问题,反而会提高代码的复杂性。对于我来说,仅仅只是 CQRS 就已经足够了。

  • 相关阅读:
    IOS Charles(代理服务器软件,可以用来拦截网络请求)
    Javascript中addEventListener和attachEvent的区别
    MVC中实现Area几种方法
    Entity Framework Code First 中使用 Fluent API 笔记。
    自定义JsonResult解决 序列化类型 System.Data.Entity.DynamicProxies 的对象时检测到循环引用
    序列化类型 System.Data.Entity.DynamicProxies 的对象时检测到循环引用
    An entity object cannot be referenced by multiple instances of IEntityChangeTracker 的解决方案
    Code First :使用Entity. Framework编程(8) ----转发 收藏
    Code First :使用Entity. Framework编程(6) ----转发 收藏
    Code First :使用Entity. Framework编程(5) ----转发 收藏
  • 原文地址:https://www.cnblogs.com/JacZhu/p/10271143.html
Copyright © 2011-2022 走看看