Как переопределить некоторые действия в ngrx store?

У нас есть угловой проект на работе (который будет разработан, еще не начался). Это довольно сложно и имеет сложные потоки данных. Кроме того, у нас есть два вида пользователей в нашем приложении. Менеджер и пользователи. Эти пользователи будут видеть похожие взгляды, но для каждого из них есть разные пользовательские представления. У менеджера будет доступ к более функциональным возможностям, как вы могли себе представить. Чтобы управлять этим довольно большим и сложным приложением масштабируемым способом, мы следуем шаблону NX. Т.е. несколько приложений в рамках одного репо. В конце, в рамках одного репо, у меня есть следующие приложения.

Apps

Большая часть разработки будет выполнена в common приложении, а различные виды и настройки будут выполняться как в mngr-app и в user-app соответственно.

Мы также думаем о применении ngrx в нашем приложении для государственного управления. Я видел несколько примеров и руководств, как это сделать. Пока все в порядке. Эта часть предназначена только для справочной информации.

Наша проблема начинается после этого. Наша бизнес-команда также хочет, чтобы мы разрабатывали приложения для iOS и Android с веб-просмотрами, содержащими веб-приложение (я забыл упомянуть, что это отзывчивое веб-приложение). Итак, все, что мы сделали, будет отправлено в веб-браузер для мобильных пользователей. Тем не менее, бизнес-команда также хочет, чтобы мы разработали некоторые пользовательские собственные представления для мобильных приложений.

Давайте рассмотрим следующий пример: (это из примера ngrx)

Add book collection

Когда пользователь нажимает кнопку " Add Book to Collection, тип действия [Collection] Add Book отправляется в магазин, и эффект будет позаботиться о нем следующим образом:

@Effect()
addBookToCollection$: Observable<Action> = this.actions$
  .ofType(collection.ADD_BOOK)
  .map((action: collection.AddBookAction) => action.payload)
  .switchMap(book =>
    this.db.insert('books', [ book ])
      .map(() => new collection.AddBookSuccessAction(book))
      .catch(() => of(new collection.AddBookFailAction(book)))
  );

Это нормальный поток веб-приложения.

Наша бизнес-команда хочет, чтобы мы создали какую-то специальную логику для мобильных приложений, так что, когда пользователи переходят на эту страницу в мобильном приложении (iOS или Android) вместо того, чтобы добавлять книгу в коллекцию, она откроет собственную страницу и пользователь выполнит действия на этой собственной странице. Я имею в виду, что они хотят, чтобы веб-приложение вел себя по-другому, когда оно присутствует в мобильном приложении. Я могу добиться этого с помощью команды if(window.nativeFlag === true) в веб-приложении. Однако это всего лишь грязный хак, которого мы хотим избежать. Поскольку мы используем ngrx и rxjs мы чувствуем, что это можно сделать с помощью Observable of rxjs и Action ngrx.

До сих пор мы пытались открыть объект store и actions для DOM, чтобы мы могли получить к нему доступ в мобильном приложении.

  constructor(private store: Store<fromRoot.State>, private actions$: Actions) {
    window['store'] = this.store;
    window['actions'] = this.actions$;
  }

Таким образом, мы можем подписаться на событие [Collection] Add Book следующим образом

actions().ofType('[Collection] Add Book').subscribe(data => {
    console.log(data)
})

и получать уведомление, когда книга добавляется в коллекцию. Тем не менее, веб-приложение по-прежнему делает то, что обычно делает, и мы не можем переопределить это поведение.

Мой вопрос заключается в том, как подписаться на некоторые действия ngrx из мобильных приложений и отменить поведение веб-приложения?

редактировать

Я придумал решение самостоятельно, однако любые другие решения оценены, и я дам щедрость, если вы сможете придумать лучшую.

Ответ 1

То, что я смог сделать до сих пор

Я написал следующий bridge в веб-приложении.

window['bridge'] = {
  dispatch: (event: string, payload = '') => {
    // I had to use zone.run, 
    // otherwise angular won't update the UI on 
    // an event triggered from mobile.
    this.zone.run(() => {
      this.store.dispatch({type: event, payload: payload});
    });
  },
  ofEvent: (event: string) => {
    // I register this event on some global variable 
    // so that I know which events mobile application listens to.
    window['nativeActions'].push(event);
    return this.actions.ofType(event);
  }
};

Из приложения Android я могу слушать [Collection] Add Book следующим образом:

webView.loadUrl("
    javascript:bridge.ofEvent('[Collection] Add Book')
       .subscribe(function(book) {
           android.addBook(JSON.stringify(book.payload));
       });
");

Это вызовет мой метод addBook, и я addBook книгу на каком-то классе, чтобы использовать ее позже.

@JavascriptInterface
public void addBook(String book) {
    Log.i("addBook", book);
    this.book = book;
}

Я также добавлю кнопку в приложение для Android, чтобы удалить эту книгу.

webView.evaluateJavascript("
      bridge.dispatch('[Collection] Remove Book', "+this.book+")
", null);

С помощью этого фрагмента кода я смог прослушать событие, инициированное веб-приложением, и сохранить результат где-то. Я также смог вызвать какое-либо событие из приложения Android для удаления книги в веб-приложении.

Кроме того, ранее я упоминал, что я зарегистрировал это событие в глобальной переменной.

Я изменил свой @Effect следующим образом

@Effect()
addBookToCollection$: Observable<Action> = this.actions$.pipe(
  ofType<AddBook>(CollectionActionTypes.AddBook),
  map(action => action.payload),
  switchMap(book => {
    if (window['nativeActions'].indexOf(CollectionActionTypes.AddBook) > -1) {
      return of();
    } else {
      return this.db
        .insert('books', [book])
        .pipe(
        map(() => new AddBookSuccess(book)),
        catchError(() => of(new AddBookFail(book)))
        );
    }
  })
);

С if (window['nativeActions']..) Я смог проверить, зарегистрировано ли мобильное приложение для этого конкретного события. Однако я бы не выполнял этот контроль над каждым @Effect меня есть. На данный момент я подумываю написать какой-то пользовательский оператор RxJs чтобы RxJs эту проверку от моих effects. Я чувствую, что я близок к ответу, но я буду признателен за любой вклад.

Пользовательский оператор RxJs

Я написал следующий пользовательский оператор и решил свою проблему.

export function nativeSwitch(callback) {
    return function nativeSwitchOperation(source) {
        return Observable.create((subscriber: Observer<any>) => {
            let subscription = source.subscribe(value => {
                try {
                    if (window['nativeActions'].indexOf(value.type) > -1) {
                        subscriber.complete();
                    } else {
                        subscriber.next(callback(value));
                    }
                } catch (err) {
                    subscriber.error(err);
                }
            },
            err => subscriber.error(err),
            () => subscriber.complete());

            return subscription;
        });
    };
}

Изменен мой эффект следующим образом:

@Effect()
addBookToCollection$: Observable<Action> = this.actions$.pipe(
  ofType<AddBook>(CollectionActionTypes.AddBook),
  nativeSwitch(action => action.payload),
  switchMap((book: Book) =>
    this.db
      .insert('books', [book])
      .pipe(
      map(() => new AddBookSuccess(book)),
      catchError(() => of(new AddBookFail(book)))
      )
  )
);

Это делает именно то, что я хочу. Так как я вызываю subscriber.complete() если данное событие зарегистрировано мобильным приложением, метод в switchMap никогда не будет вызван.

Надеюсь, это поможет кому-то.