美文网首页我爱编程
Stop using ngrx/effects for that

Stop using ngrx/effects for that

作者: 萧哈哈 | 来源:发表于2018-04-18 22:57 被阅读404次

    翻译说明: 意译,中英文对照, 有特定含义的英文术语保留。
    本文翻译已取得作者 Michael Pearson 授权。
    原文链接

    Sometimes the simplest implementation for a feature ends up creating more complexity than it saves, only shoving the complexity elsewhere.
    有时候某个功能最简单的实现最终比起它所带来的的创建更多的复杂性,只是把复杂性推到别处。

    The eventual result is buggy architecture that nobody wants to touch.
    最终的结果是没有人愿意碰的容易出bug的架构。

    Ngrx/store is an Angular library that helps to contain the complexity of individual features.
    Ngrx/store 是个 Angular 库, 它有助于控制单个功能的复杂性。

    One reason is that ngrx/store embraces functional programming, which restricts what can be done inside a function in order to achieve more sanity outside of it.
    一个原因是 ngrx/store 遵循函数式编程, 限制了在函数内部可以做的事情,从而在其之外实现更健全的功能。

    In ngrx/store, reducers, selectors, and RxJS operators are pure functions.
    ngrx/store 中, reducers, selectors, 以及 RxJS 操作符都是纯函数。

    Pure functions are easier to test, debug, reason about, parallelize, and combine.
    纯函数非常容易测试, 调试, 理解, 并行处理 以及 组合。

    A function is pure if
    函数是纯的, 如果

    • given the same input, it always returns the same output.
      给定同样的输入, 总是会返回相同的输出。
    • it produces no side effects.
      不会产生副作用。

    Side effects are impossible to avoid, but they are isolated in ngrx/store so that the rest of the application can be composed of pure functions.
    副作用不可避免, 但是它们被隔离在 ngrx/store 中,因此,应用的其余部分可以由纯函数组成。

    Side Effects 副作用

    When a user submits a form, we need to make a change on the server.
    当用户提交了表单, 我们需要在服务器上做出改变。

    The change on the server and response to the client is a side effect.
    服务器上的改变以及对客户端的响应就是一种副作用。

    This could be handled in the component:
    在组件中可以这样处理:

    this.store.dispatch({
      type: "SAVE_DATA",
      payload: data
    });
    this.saveData(data) // POST request to server
      .map(res => this.store.dispatch({type: "DATA_SAVED"}))
      .subscribe()
    

    It would be nice if we could simply dispatch an action inside the component when the user submits the form and handle the side effect elsewhere.
    当用户提交表单时, 如果我们能够在组件内派发 action, 而在其他地方处理副作用, 那就太美妙了。

    Ngrx/effects is middleware for handling side effects in ngrx/store.
    ngrx/store 中, Ngrx/effects 是处理副作用的中间件。

    It listens for dispatched actions in an observable stream, performs side effects, and returns new actions either immediately or asynchronously.
    它监听 observable 流中派发的 actions, 执行副作用, 然后立即或者异步返回新的 actions。

    The returned actions get passed along to the reducer.
    返回的 actions 被传给 reducer。

    Being able to handle side effects in an RxJS-friendly way makes for cleaner code.
    能够以 RxjJS 友好的方式处理副作用,会使代码更加清晰。

    After dispatching the initial action SAVE_DATA from the component you create an effects class to handle the rest:
    在组件里派发 最初的 action SAVE_DATA之后, 你可以创建 effects 类处理其余部分:

    @Effect() saveData$ = this.actions$
      .ofType('SAVE_DATA')
      .pluck('payload')
      .switchMap(data => this.saveData(data))
      .map(res => ({type: "DATA_SAVED"}))
    

    This simplifies the job of the component to only dispatching actions and subscribing to observables.
    这简化了组件的工作,组件只用于派发actions 以及订阅 observables。

    Ngrx/effects is easy to abuse (Ngrx/effects 很容易被滥用)

    Ngrx/effects is a very powerful solution, so it is easy to abuse.
    Ngrx/effects 是个很强大的解决方案, 所以它很容易被滥用。

    Here are some common anti-patterns of ngrx/store that Ngrx/effects makes easy:
    这里有一些 ngrx/store 的常见的反模式。

    1、Duplicate/derived state (重复/派生的状态)

    Let’s say you’re working on some kind of media playing app, and you have these properties in your state tree:
    假设你在做某种媒体播放应用,状态树中有这些属性:

    export interface State {
      mediaPlaying: boolean;
      audioPlaying: boolean;
      videoPlaying: boolean;
    }
    

    Because audio is a type of media, whenever audioPlaying is true, mediaPlaying must also be true.
    因为音频是媒体的一种类型, 每当 audioPlayingtrue 时, mediaPlaying 也必须是 true

    So here’s the question: “How do I make sure mediaPlaying is updated whenever audioPlaying is updated?”
    那么问题来了: "每当audioPlaying更新了, 我如何确保mediaPlaying 也更新了?"

    Incorrect answer: Use an effect!
    错误回答: 使用 effect!

    @Effect() playMediaWithAudio$ = this.actions$
     .ofType("PLAY_AUDIO")
     .map(() => ({type: "PLAY_MEDIA"}))
    

    Correct answer: If the state of mediaPlaying is completely predicted by another part of the state tree, then it isn’t true state.It’s derived state. That belongs in a selector, not in the store.
    正确答案: 如果 mediaPlaying 的状态完全由状态树中的另一部分决定, 那么它就不是真正的状态。它是派生的状态。它应归于选择器,而不是存储。

    audioPlaying$ = this.store.select('audioPlaying');
    videoPlaying$ = this.store.select('videoPlaying');
    mediaPlaying$ = Observable.combineLatest(
      this.audioPlaying$,
      this.videoPlaying$,
      (audioPlaying, videoPlaying) => audioPlaying || videoPlaying
    )
    

    Now our state can stay clean and normalized, and we’re not using ngrx/effects for something that isn’t a side effect.
    现在 我们的状态可以保持清晰以及归一化, 我们不再对一些不是副作用的东西使用 ngrx/effects

    2. Coupling actions and reducers (耦合 actions 和 reducers)

    Imagine you have these properties in your state tree:
    假设在状态树中有这些属性:

    export interface State {
      items: {[index: number]: Item};
      favoriteItems: number[];
    }
    

    Then the user deletes an item.
    然后用户删除了 一项条目。

    When the delete request returns, the action DELETE_ITEM_SUCCESS is dispatched to update our app state.
    当删除请求返回时, 会派发 action DELETE_ITEM_SUCCESS 来更新应用的状态。

    In the items reducer the item is removed from the items object.
    items reducer中, 该项条目会从 items 对象中删除。

    But if that item id was in the favoriteItems array, the item it’s referring to will be missing.
    但是, 如果该条目的 id 是在 favoriteItems 数组中, 它所引用的条目将会丢失。

    So the question is, how can I make sure the id is removed from favoriteItems whenever the DELETE_ITEM_SUCCESS action is dispatched?
    因此, 问题是: 每当 DELETE_ITEM_SUCCESS action 派发时, 我如何确保 条目对应的 id 也从 favoriteItems 中删除

    Incorrect answer: Use an Effect!
    错误回答: 使用 Effect!

    @Effect() this.actions$
      .ofType("DELETE_ITEM_SUCCESS")
      .map(() => ({type: "REMOVE_FAVORITE_ITEM_ID"}))
    

    So now we will have two actions dispatched back-to-back, and two reducers returning new states back-to-back.
    现在, 我们连续派发了 2 个 action, 2 个 reducer 连续返回新的状态。

    Correct answer: DELETE_ITEM_SUCCESS can be handled by both the items reducer and the favoriteItems reducer.
    正确答案: DELETE_ITEM_SUCCESS 可以同时被 items reducer 和 favoriteItems reducer 处理

    export function favoriteItemsReducer(state = initialState, action: Action) {
      switch(action.type) {
        case 'REMOVE_FAVORITE_ITEM':
        case 'DELETE_ITEM_SUCCESS':
          const itemId = action.payload;
          return state.filter(id => id !== itemId);
        default: 
          return state;
      }
    }
    

    The purpose of actions is to decouple what happened from how state is supposed to change.
    actions 的目的是将所发生的事情与状态应该改变的情况解耦。

    What happened was DELETE_ITEM_SUCCESS.
    所发生的事情是 DELETE_ITEM_SUCCESS.

    It is the job of the reducers to cause the appropriate state change.
    产生 合适的 状态改变 是 reducers 的工作。

    Removing an id from favoriteItems is not a side effect of deleting the item.
    favoriteItems 中移除 id 并不是 删除 条目的副作用。

    The whole process is completely synchronous and can be handled by the reducers. Ngrx/effects is not needed.
    整个过程完全是同步的, 可以被 reducers 处理。 并不需要 Ngrx/effects

    3. Fetching data for a component (为组件获取数据)

    Your component needs data from the store, but the data needs to be fetched from the server first.
    你的组件需要 store 中的数据, 但是这个数据首先需要从服务器获取。

    The question is, how can we get the data into the store so the component can select it?
    问题是: 如何将数据导入到 store 中,以便组件可以选择它?

    Painful answer: Use an effect!
    痛苦的回答: 使用 effect !

    In the component we trigger the request by dispatching an action:
    在组件中, 我们通过派发 action 触发请求:

    ngOnInit() {
      this.store.dispatch({type: "GET_USERS"});
    }
    

    In the effects class we listen for GET_USERS:
    在 effects 类中, 我们监听 GET_USERS:

    @Effect getUsers$ = this.actions$
      .ofType('GET_USERS')
      .withLatestFrom(this.userSelectors.needUsers$)
      .filter(([action, needUsers]) => needUsers)
      .switchMap(() => this.getUsers())
      .map(users => ({type: 'RECEIVE_USERS', users}))
    

    Now let’s say a user decides it’s taking too long for the users route to load, so they navigate away.
    现在假设用户觉得加载 users 路由花费的时间太长了, 所以他们离开了。

    To be efficient and not load useless data, we want to cancel that request.
    为了提高效率,不加载无用数据,我们想要取消这个请求。

    When the component is destroyed, we will unsubscribe from the request by dispatching an action:
    当组件销毁时, 我们会通过派发 action 来取消对该请求的订阅。

    ngOnDestroy() {
      this.store.dispatch({type: "CANCEL_GET_USERS"})
    }
    

    In the effects class we listen for both actions now:
    现在, 在 effects 类中,我们监听 2 个 action:

    @Effect getUsers$ = this.actions$
      .ofType('GET_USERS', 'CANCEL_GET_USERS')
      .withLatestFrom(this.userSelectors.needUsers$)
      .filter(([action, needUsers]) => needUsers)
      .map(([action, needUsers]) => action)
      .switchMap(action => action.type === 'CANCEL_GET_USERS' ? 
        Observable.of() :
        this.getUsers()
          .map(users => ({type: 'RECEIVE_USERS', users}))
      )
    

    Okay. Now another developer adds a component that needs the same HTTP request to be made (no assumptions should be made about other components).
    好的. 现在 又一个 开发人员添加了一个组件, 这个组件需要发起同样的请求(对其他组件不应作任何假设。)

    The component dispatches the same actions in the same places.
    这个组件在同样的地方派发了同样的 action.

    If both components are active at the same time, the first component to initialize will trigger the HTTP request.
    如果 这 2 个组件同时是活跃的, 第一个初始化的组件会触发 HTTP 请求.

    When the second component initializes, nothing additional will happen because needUsers will be false. Great!
    当第二个组件初始化时,不会发生额外的事情,因为 needUsers 将是 false。太棒了!

    Then, when the first component is destroyed, it will dispatch CANCEL_GET_USERS. But the second component still needs that data.
    然后, 当第一个组件销毁时会派发 CANCEL_GET_USERS。 但是第二个组件仍然需要数据。

    How can we prevent the request from being canceled?
    我们怎么才能阻止请求被取消?

    Maybe have a count of all the subscribers?
    或许再添加一些 订阅者?

    I won’t bother exploring that, but you get the point. We start to hope there is a better way of managing these data dependencies.
    我不会去探究这个问题,但你明白我的意思。我们开始希望有一种更好的方法来管理这些数据依赖关系。

    Now let’s say another component comes into the picture, and it depends on data that cannot be fetched until after the users data is in the store.
    现在假设另一个组件出现在图中,它依赖于users数据, 该数据只有存在于 store 中之后才能获取到。

    It could be a websocket connection for a chat, or additional information about some of the users, or whatever.
    它可能是 聊天的 websocket 连接, 或者是关于某些用户的额外信息, 或者是其他什么东西。

    We don’t know if this component will be initialized before or after the other two components subscribe to users.
    我们并不知道该组件是在其他 2 个组件订阅users之前还是之后初始化。

    The best help I found for this particular scenario is this great post.
    对于这个特殊的场景我找到的最好的帮助是这篇很棒的文章。

    In his example, callApiY requires callApiX to have been completed already.
    在他的例子中, callApiY 要求 callApiX 已经完成。

    I’ve stripped out the comments to make it look less intimidating, but feel free to read the original post to learn more:
    我删除了评论,让它看起来不那么吓人,但你可以阅读原文了解更多:

    @Effect() actionX$ = this.updates$
        .ofType('ACTION_X')
        .map(toPayload)
        .switchMap(payload => this.api.callApiX(payload)
            .map(data => ({type: 'ACTION_X_SUCCESS', payload: data}))
            .catch(err => Observable.of({type: 'ACTION_X_FAIL', payload: err}))
        );
    @Effect() actionY$ = this.updates$
        .ofType('ACTION_Y')
        .map(toPayload)
        .withLatestFrom(this.store.select(state => state.someBoolean))
        .switchMap(([payload, someBoolean]) => {
            const callHttpY = v => {
                return this.api.callApiY(v)
                    .map(data => ({
                        type: 'ACTION_Y_SUCCESS', 
                        payload: data
                    }))
                    .catch(err => Observable.of({
                        type: 'ACTION_Y_FAIL', 
                        payload: err
                     }));
            }
            
            if(someBoolean) {
                return callHttpY(payload);
            }
            return Observable.of({type: 'ACTION_X', payload})
                .merge(
                    this.updates$
                        .ofType('ACTION_X_SUCCESS', 'ACTION_X_FAIL')
                        .first()
                        .switchMap(action => {
                           if(action.type === 'ACTION_X_FAIL') {
                              return Observable.of({
                                type: 'ACTION_Y_FAIL', 
                                payload: 'Because ACTION_X failed.'
                              });
                            }
                            return callHttpY(payload);
                        })
                );
        });
    

    Now add on the requirement that HTTP requests should be canceled when components are no longer interested, and it gets more complicated.
    现在添加一个要求,当组件不再感兴趣时,HTTP请求应该被取消,这变得更加复杂。


    So why so much trouble for managing data dependencies when RxJS is supposed to make it really easy?
    那么当 RxJS 应当使数据依赖管理更加简单时,为什么会有这么多的问题?

    While data arriving from the server technically is a side effect, I don’t believe that ngrx/effects is the best way to manage this.
    虽然从技术上来说,来自服务器的数据是个副作用,但我不认为ngrx/effects 是管理这个的最好方法。

    Components are I/O interfaces for the user. They show data and dispatch actions from users.
    组件是用户的I/O接口。它们显示数据并派发用户发出的 actions。

    When a component loads, it is not dispatching an action from a user. It wants to show data.
    当组件加载时,它不会派发来自用户的 action。它想显示数据。

    That looks like a subscription, not the side effect of an action.
    这看起来像是订阅, 而不是 action 的副作用。

    It’s very common to see apps using actions to trigger data fetches.
    使用actions 来触发数据获取是很常见的。

    These apps implement a custom interface to observables through side effects.
    这些应用通过副作用实现了 observable 的自定义接口。

    And as we’ve seen, this interface can become very awkward and unwieldy.
    正如我们所见,这个接口会变得非常笨重。

    Subscribing to, unsubscribing from, and chaining the observables themselves is much more straightforward.
    对 observables 的订阅, 取消订阅 以及 链接更加直观。


    Less painful answer: The component will register its interest in data by subscribing to an observable of the data.
    不那么痛苦的答案: 组件会通过订阅 observable 来获取其感兴趣的数据。

    We will create observables that contain the HTTP requests we want to make.
    我们会创建 包含 我们想发起的 http 请求的 observable。

    We will see how much easier it is to manage multiple subscriptions and chain requests off of each other using pure RxJS than it is to do those things with effects.
    我们将会看到,使用纯粹的 RxJS 来管理多个订阅和链请求比使用那些带有 effects 的东西要容易得多。

    Create these observables in a service:
    在服务里创建这些 observables:

    public requireUsers$ = this.userSelectors.needUsers$
      .filter(needUsers => needUsers)
      .do(() => this.store.dispatch({type: 'GET_USERS'}))
      .switchMap(() => this.getUsers())
      .do(users => this.store.dispatch({type: 'RECEIVE_USERS', users}))
      .finally(() => this.store.dispatch({type: 'CANCEL_GET_USERS'}))
      .share();
    
    public users$ = this.muteFirst(
      this.requireUsers$.startWith(null), 
      this.userSelectors.users$
    )
    

    Subscriptions to users$ will be passed up to both requireUsers$ and userSelectors.users$, but will only receive the values from userSelectors.users$ (example implementation of ``.)
    users$ 的订阅会同时传给requireUsers$userSelectors.users$
    但是只会接收来自 userSelectors.users$ 的值。
    In the component:
    在组件中:

    ngOnInit() {
      this.users$ = this.userService.users$;
    }
    

    Because this data dependency is now just an observable, we can subscribe and unsubscribe in the template using the async pipe and we no longer need to dispatch actions.
    因为这个数据依赖现在仅仅是个 observable, 我们可以在模板中使用 async 管道 订阅 和取消订阅, 我们不再需要派发 actions。

    If the app navigates away from the last component subscribed to the data, the HTTP request is canceled or the websocket is closed.
    如果应用从订阅数据的最后一个组件中导航离开,则HTTP请求被取消,或者websocket被关闭。

    Chains of data dependencies can be handled like this:
    数据依赖链可以这样处理:

    public requireUsers$ = this.userSelectors.needUsers$
      .filter(needUsers => needUsers)
      .do(() => this.store.dispatch({type: 'GET_USERS'}))
      .switchMap(() => this.getUsers())
      .do(users => this.store.dispatch({type: 'RECEIVE_USERS', users}))
      .share();
    
    public users$ = this.muteFirst(
      this.requireUsers$.startWith(null), 
      this.userSelectors.users$
    )
    
    public requireUsersExtraData$ = this.users$
      .withLatestFrom(this.userSelectors.needUsersExtraData$)
      .filter(([users, needData]) => Boolean(users.length) && needData)
      .do(() => this.store.dispatch({type: 'GET_USERS_EXTRA_DATA'}))
      .switchMap(() => this.getUsers())
      .do(users => this.store.dispatch({
        type: 'RECEIVE_USERS_EXTRA_DATA', 
        users
      }))
      .share();
    
    public usersExtraData$ = this.muteFirst(
      this.requireUsersExtraData$.startWith(null),
      this.userSelectors.usersExtraData$
    )
    

    Here’s a side-by-side comparison of the above method vs this method:
    下面是上述方法与此方法的并行比较:


    对数据依赖链的处理 ngrx/effects vs 普通的 RxJS

    Using plain observables requires fewer lines of code, and automatically unsubscribes from data dependencies all the way up the chain. (I omitted the finallys originally included in order to make the comparison clearer, but even without them the requests still get canceled appropriately.)
    使用普通的 observables 需要的代码行更少, 并且自动地从数据依赖项中取消订阅。(为了使对比更清楚,我省略了 “finally”,但是即使没有它们,请求也会被适当地取消。)

    Conclusion 结论

    Ngrx/effects is a great tool! But consider these questions before using it:
    Ngrx/effects 是个很棒的工具! 但是在使用它之前考虑下这些问题:

    1、Is this really a side effect?
    这真的是个副作用么?

    2、Is ngrx/effects really the best way to handle this?
    ngrx/effects 真的是处理这个问题的最佳方式么?

    相关文章

      网友评论

        本文标题:Stop using ngrx/effects for that

        本文链接:https://www.haomeiwen.com/subject/xuwpkftx.html