在angular2应用程序中管理状态 - 副作用?

时间:2016-06-09 10:49:21

标签: javascript angular rxjs

这是一个更普遍的问题,但基于Victor Savkin的帖子 Managing state in angular2

让我们考虑那里使用RxJs的方法:

interface Todo { id: number; text: string; completed: boolean; }
interface AppState { todos: Todo[]; visibilityFilter: string; }

function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
  return actions.scan((state, action) => {
    if (action instanceof AddTodoAction) {
      const newTodo = {id: action.todoId, text: action.text, completed: false};
      return [...state, newTodo];
    } else {
      return state;
    }
  }, initState);
}

一切都很好,但让我们添加更多要求:

  • 添加新的Todo项目后,应将其文本发送到后端并进行分析,以提取可能的截止日期和地点。
  • 如果Todo项目有截止日期,则应将其添加到我的Google日历

所以,如果我添加Todo“在星期四在Sally's Saloon完成我的头发”,第一个电话我将从Sally's Saloon的后端获得,并且日期定于本周(或下周)周四和第二个电话将添加此todo到我的Google日历,并在日历中标记项目。

所以我的新Todo项目结构可能如下所示:

interface Todo { 
  id: number; 
  text: string; 
  completed: boolean; 
  location?: Coordinates; 
  date?: Date; 
  inCalendar?: boolean; 
  parsed?: boolean;
}

现在我有两个副作用:

  1. 添加待办事项后,我需要解析文本
  2. 将日期添加到Todo之后,我需要将其添加到日历中。
  3. 我如何处理这种方法中的这些副作用? Redux说减速器应该保持清洁,他们也有Sagas的概念。

    选项1 - 在添加待办事项时触发副作用的新事件

    function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
      return actions.scan((state, action) => {
        if (action instanceof AddTodoAction) {
          const newTodo = {id: action.todoId, text: action.text, completed: false};
          actions.onNext(new ParseTodoAction(action.todoId));
          return [...state, newTodo];
        } else if (action instanceOf ParseTodoAction){
          const todo = state.find(t => t.todoId === action.todoId)
          parserService
          .parse(todo.todoId, todo.text)
          .subscribe(r => actions.onNext(new TodoParsedAction(todo.todoId, r.coordinates, r.date)))
        } else {
          return state;
        }
      }, initState);
    }
    

    但这会失败,因为州内还没有新的待办事项。 我当然可以只使用TodoParsedAction而不是ParseTodoAction只调用后端内联调用,但是这也会假设后端调用需要更长的时间来处理,并且当它完成状态时就已经有了新Todo项目等待发生的麻烦。

    选项2 - 订阅操作并检查每个待办事项是否缺少属性

    actions
    .flatMap(todos => Observable.from(todos))
    .subscribe(todo => {
      if (!todo.coordinates && !todo.parsed) {
         parserService
          .parse(todo.todoId, todo.text)
          .subscribe(r => actions.onNext(new TodoParsedAction(todo.todoId, r.coordinates, r.date)))
      }
      if (todo.date && todo.inCalendar === undefined) {
        calendarService
          .add(todo.text, todo.date)
          .subscribe(_ => actions.onNext(new TodoInCalendarAction(todo.todoId, true)))
      }
    })
    

    但是这种感觉不对 - 不应该是所有由行为管理的东西,我应该总是遍历所有的待办事项吗?

1 个答案:

答案 0 :(得分:3)

您的选项1无法按照规定运作:actions Observable<Action>观察值是只读的,而onNext不是该API的一部分。你需要Observer<Action>来支持选项1.这突出了选项1的真正缺陷:你的状态函数(与Redux reducer相同)需要和无副作用。这意味着他们不能也不应该派出更多行动。

现在在你引用的博客文章中,实际上代码实际上是在一个Subject中传递,它既是Observer也是Observable。所以你可能有一个onNext。但是我可以告诉你,当你处理该主题发布的数据时递归地向主题发布数据会让你陷入麻烦之中,并且很难让头痛的事情正常工作。

在Redux中,调用后端处理以丰富您的状态的典型解决方案是在您决定调度AddTodo时在开始时调度多个操作。这通常可以通过使用redux-thunk和调度功能来实现,并且#34;智能操作&#34;:

而不是:

export function addToDo(args) {
    return new AddToDoAction(args);
}

你做的事:

export function addToDo(args) {
    return (dispatch) => {
        dispatch(new AddToDoAction(args)); // if you want to dispatch the Todo before parsing
        dispatch(parseToDo(args)); // handle parsing
    };
}

export function parseToDo(args) {
    return (dispatch) => {
        if (thisToDoNeedsParsing(args)) {
            callServerAndParse(args).then(result => {
                  // dispatch an action to update the Todo
                  dispatch(new EnrichToDoWithParsedData(result));
            });
        }
    };
}

// UI code would do:
dispatch(addToDo(args));

UI会调度一个智能操作(thunk),它将调度AddToDoAction以获取您所在州的未解析的待办事项(如果您愿意,您的UI可以选择不显示它,直到解析完成)。然后它会调度另一个智能操作(thunk),它将实际调用服务器以获取更多数据,然后发送EnrichToDoWithParsedData操作并显示结果,以便更新您的Todo。

至于更新日历......您可以使用上面的模式(在possiblyUpdateCalendar()addToDo中插入parseToDo的调用,这样如果todo拥有所有内容你需要它,它可以更新日历,并在完成时发送一个动作来标记添加的待办事项。

现在我展示的这个例子是Redux特有的,我不认为你正在使用的基于RxJs的例子有像thunk一样的东西。在您的方案中添加对此的支持的一种方法是向主题添加flatMap运算符,如下所示:

let actionStream = actionsSubject.flatMap(action => {
    if (typeof action !== "function") {
        // not a thunk.  just return it as a simple observable
        return Rx.Observable.of(action);
    }
    // call the function and give it a dispatch method to collect any actions it dispatches
    var actions = [];
    var dispatch = a => actions.push(a);
    action(dispatch);

    // now return the actions that the action thunk dispatched
    return Rx.Observable.of(actions);
});

// pass actionStream to your stateFns instead of passing the raw subject
var state$ = stateFn(initState, actionStream);

// Now your UI code *can* pass in "smart" actions:
actionSubject.onNext(addTodo(args));
// or "dumb" actions:
actionSubject.onNext(new SomeSimpleAction(args));

请注意,上面的所有代码都在调度操作的代码中。我没有表现出你的任何状态功能。你的状态函数是纯粹的,如:

function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
  return actions.scan((state, action) => {
    if (action instanceof AddTodoAction) {
      const newTodo = {id: action.todoId, text: action.text, completed: false};
      return [...state, newTodo];
    } else if (action instanceof EnrichTodoWithParsedData) {
      // (replace the todo inside the state array with a new updated one)
    } else if (action instanceof AddedToCalendar) {
      // (replace the todo inside the state array with a new updated one)
    }
    } else {
      return state;
    }
  }, initState);
}