【问题标题】:Pattern for updating editable list of elements with Observable使用 Observable 更新可编辑元素列表的模式
【发布时间】:2017-04-14 17:14:01
【问题描述】:

技术背景:TypeScript 中带有 RxJS 的 Angular (4)。但我认为问题与技术无关。

我有一个元素数组。该数组由服务提供,可能来自远程 API(后端)。

元素服务:

  private elements: Element[];
  private elementsSource = new ReplaySubject<Element[]>(1);
  elementsObservable$ = this.elementsSource.asObservable();

组件订阅该 observable 并用数据填充 HTML 列表。现在列表的每个元素都可以由用户独立编辑。编辑后更改必须保存并发送到 a) 后端,b) 订阅该元素更改的其他组件。

因为列表元素可能被编辑并且它们可能是“脏的”,所以我不想在其他组件中使用 2 路数据绑定来立即显示更改。因此,我复制 observable 的输出并处理此副本,然后将带有新列表的更新发送到服务。

元素列表组件:

  elements: Element[];

  constructor(private elementService: ElementService) { }

  ngOnInit() {
    this.subscribeElements();
  }

  private subscribeElements() {
    this.elementService.getElements().subscribe(elements => this.elements = clone(elements));
  }

  private onChangeSave() {
    this.elementService.updateElements(clone(this.elements));
  }

但元素列表也可以在其他组件中/由其他用户编辑。在这种情况下,ElementService 将通过 websocket 使用新数据进行更新。这将通过 observable 发送到 ElementListComponent。

现在如果:

  • 用户 A 将开始编辑一个或多个项目,而用户 B 将更新一些其他元素,或者
  • 用户将开始在一个组件中编辑一个元素 X,然后在另一个组件中编辑元素 Y,或者
  • 用户将开始编辑元素 X 和 Y,保存对元素 X 的更改(保存按钮独立于列表中的元素),

然后未保存的更改将丢失,因为列表将被新的更新的列表替换。

我考虑过在 subscribe() 中迭代数组并分别更新本地数组中的每个元素 - 添加新元素,删除缺失的元素,如果现在不编辑它们,则更新已更改。这样对数组的引用就不会改变,脏元素也不会被“重置”。

这真的是最好的方法吗?还是我错过了什么?

【问题讨论】:

  • 您使用重播主题而不是普通主题或行为主题是否有特殊原因?
  • 是的,我将其用作冷可观察对象,它将为第一个观察者启动,并为接下来的所有观察者返回缓存值。在我的案例中,不同的组件连接到这个 observable,我不想将最后、最新鲜的价值返回给新客户。

标签: angular design-patterns rxjs reactive-programming observable


【解决方案1】:

考虑Mediator 模式。
中介者定义了控制一组对象如何交互的对象。
机场的控制塔是 Mediator 的真实例子。每架飞机都与塔台通信,因此塔台拥有所有必要的信息。

【讨论】:

  • 好吧,这里的服务充当中介。问题在于它为每个同事服务的数据。将所有新数据提供给同事会使其失去他的“肮脏”更改。
【解决方案2】:

好的,我想出了一个解决方案。我不是通过 Observable 提供具有 Elements 的完整数组,而是首先为每个 Element 发出一个事件,然后在以后每个 Element 创建/更新/删除时发出一个事件。

元素服务:

@Injectable()
export class ElementService {

  private elementsSource = new ReplaySubject<ElementChange>();
  /** Observable that returns separate observable for each subscriber with independent copies of emitted items. */
  private elementsObservable = Observable.defer(() => this.elementsSource.asObservable().map(x => <ElementChange>clone(x)));

  constructor(private logger: Logger) { }

  /**
   * Returns observable that emits new value on every change (add, update or remove) of every single Element entity.
   * Each observer receives independent copy of entity. New observers receives full history of entities - all events emitted from the beginning
   * of obserable's work.
   */
  getElements(): Observable<ElementChange> {
    return this.elementsObservable;
  }

  createOrUpdateElements(element: Element) {
    if (!element.id) { // generate id for new instance
      element.id = uniqid();
    }

    this.elementsSource.next(new ElementChange(element));
  }

  removeElemenet(element: Element) {
    if (element.id) {
      this.elementsSource.next(new ElementChange(element, true));
    } else {
      this.logger.warn("Cannot emit remove event for unknown Element without ID. Element should be first created by method of this service.", element);
    }
  }

}

/** Model for Element changes emiting by Observable. */
export class ElementChange {
  constructor(public data: Element, public remove: boolean = false) { };
}

元素组件:

export class ElementComponent implements OnInit {

  private elements = new Array<Element>();

  constructor(private elementService: ElementService) { }

  ngOnInit() {
    this.subscribeElements();
  }

  private subscribeElements() {
    this.elementService.getElements()
      .groupBy(change => change.remove)
      .subscribe(group => {
        if (group.key === false) { // adding / updating
          group
            .map(change => change.data)
            .subscribe(element => {
              let idx = this.elements.findIndex(e => e.id === element.id);
              if (idx === -1) { // add
                this.elements.push(element);
              } else { // update
                this.elements[idx] = element;
              }
            });
        } else { // removing
          group
            .map(element => this.elements.findIndex(e => e.id === element.data.id))
            .filter(idx => idx !== -1)
            .subscribe(idx => {
              this.elements.splice(idx, 1);
            });
        }
      });
  }

  private addAddElementClick() {
    this.elements.push(new Element());
  }

  private onElementSave(element: Element) {
    this.elementService.createOrUpdateElements(element);
  }

  private onElementEditCancel(idx: number) {
    this.removeIfLocalOnlyEntity(idx);
  }

  private onElementRemove(idx: number) {
    this.removeIfLocalOnlyEntity(idx) || this.elementService.removeElemenet(this.elements[idx]);
  }

  /**
   * Removes Element from array if it's local only entity, created locally and never sent to service.
   * @param idx Index of entity in elements array
   * @returns True if entity was removed, false otherwise.
   */
  private removeIfLocalOnlyEntity(idx: number): boolean {
    if (this.elements[idx].id === undefined) {
      this.elements.splice(idx, 1);
      return true;
    }
    return false;
  }

}

优点:

  • 只要两个用户/组件不修改同一个元素,就可以在不放弃其他更改的情况下对其进行处理(添加、编辑、删除)。
  • 为每个观察者克隆元素实例。由于此对象修改在一个组件中不会在另一个组件中可见,直到 ElementService#createOrUpdateElements() 方法被显式调用。

缺点:

  • 发射的元素用remove 标志包装在另一个类中。
  • ElementComponent#subscribeElements() 中的长订阅实现。使用if 子句而不是groupBy() 可能会更短(半行),但这样更易读且易于扩展。

奖金

当新的观察者订阅时,他会得到所有过去的事件。其中一些可能引用同一个元素(例如添加元素,修改它,再次修改它),但我们的ElementComponent 只需要最后一个。所以ReplaySubject可以替换成自定义的ReplayLastDistinctSubject

/**
 * Subject that emits only last distinct instance of each item that was emitted by the source Observable(s),
 * regardless of when the observer subscribes. After subscription all new items are normally emitted to the Observer,
 * even if they are not distinct. This way on subsciption Observer gets only the latest version of each emitted item so far
 * and then gets all new emits.
 * 
 * Besides distinct filtering the ReplayLastDistinctSubject behaves similary to the ReplaySubject.
 * 
 * Method of comparing items can be specified by providing specific key selector.
 */
export class ReplayLastDistinctSubject<T> extends Subject<T> {

  private values: T[] = [];

  constructor(private keySelector: (value: T) => any = (x) => x) {
    super();
  }

  protected _subscribe(subscriber: Subscriber<T>): Subscription {
    const subscription = super._subscribe(subscriber);
    if (subscription && !(<ISubscription>subscription).closed) {

      const len = this.values.length;
      for (let i = 0; i < len && !subscriber.closed; i++) {
        subscriber.next(this.values[i]);
      }
    }
    return subscription;
  }

  next(value: T) {
    this.addNewDistinctValue(value);
    super.next(value);
  }

  private addNewDistinctValue(value: T) {
    this.values = this.values.filter(x => this.keySelector(x) !== this.keySelector(value));
    this.values.push(value);
  }
}

使用ReplayLastDistinctSubject 新订阅者将只获得每个元素的最后一个过去事件,因此它不会对旧版本的元素进行不必要的操作。

非常欢迎任何想法、cmets 或更好的想法 :-)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-07-24
    • 2022-06-17
    • 1970-01-01
    • 1970-01-01
    • 2022-12-18
    • 2014-01-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多