【问题标题】:How to log Redux actions with additional state-change details per action如何使用每个操作的附加状态更改详细信息记录 Redux 操作
【发布时间】:2016-10-22 05:51:31
【问题描述】:

我有一个 Redux 应用程序,我将在其中记录用户的操作,并希望将我的操作记录到不同的位置(例如,我的日志文件;如果有问题,我的错误日志;Google Analytics)。目前,每个地方都有自己的中间件

/middleware
  log_action_to_file.js
  log_some_actions_to_analytics.js

我的动作对象很简洁,只包含进行状态更改所需的信息。但是,对于人类可读的日志,拥有更多信息(通常是操作的结果)很有用。理论上我们可以只添加一个状态的diff,但实际上我的日志的读者会想要一些语义上更有意义的东西:

// actions
{
   type: "MOVED_FORWARD_X_PAGES,
   numberOfPages: 5,
   // nowShowingPage: 53         <== log file would like this additional info
}
{
   type: "SELECTED_ITEM_FROM_LIST,
   index: 4,
   // itemSelected: "cheese"     <== log file would like this additional info
}

我可以有一个单独的文件,其中包含一个完整的操作列表,用于指定需要记录的其他属性:

export function addPropertiesBeforeLogging(action, nextState) {
  let loggableAction = { ...action};
  if (action.type == "MOVED_FORWARD_X_PAGES") {
    loggableAction.nowShowingPage = nextState.showingPage;
  } else if (...)

但是每次我添加一个新动作时,我都需要记住更新这个文件,它会变得非常混乱。

有什么更好的方法来处理这个问题?我是否应该向动作对象本身添加一些额外的信息,以自我描述要添加的状态数据?

//actions.js

export function moveForwardXPages(numberOfPages) {
  return {
    type: "MOVED_FORWARD_X_PAGES",
    numberOfPages
    additionalLogDataFromNextState: {
      nowShowingPage: "next.showingPage"
    }
  };
}

然后我的中间件可以解析该字符串以知道在新状态下要查询什么?还是这将状态和动作耦合得太紧了?有什么更好的方法?

【问题讨论】:

    标签: redux


    【解决方案1】:

    我认为你的方向是正确的。 正如您所说,您可以在您的操作中添加一个新字段,其中放置您需要记录的额外信息。例如,如果您查看 FSA (Flux Standard Action),它允许属性 meta 用于此目的。

    可选的元属性可以是任何类型的值。它适用于不属于有效负载的任何额外信息。

    要记录您的操作,一个好的解决方案是使用一个或多个专用的redux-middleware 负责此操作,以捕获具有特殊“特征”的每个操作。 例如,您可以使用符号(例如 LOG)标记动作并使用中间件轻松捕获它,或者,您可以捕获呈现属性 meta 的动作。

    real-world redux repo 中的示例启发的小sn-p:

    // LOG symbol
    export const LOG = Symbol('LOG')
    
    // action
    import { LOG } from './....'
    export function moveForward () {
    return {
       [LOG]: {
           type: "MOVED_FORWARD_X_PAGES,
           numberOfPages: 5,
           nowShowingPage: 53        // <== log file would like this additional info
           }
    }
    
    // A Redux middleware that interprets actions with LOG info specified.
    import { LOG } from './....'
    export default store => next => action => {
      const log = action[LOG]
      if (typeof log === 'undefined') {
        return next(action)
      }
      // here perform log
      // ...
    }
    

    看看这个库redux-segment(redux 的分析集成),它使用属性 meta 告诉中间件记录操作。

    希望对你有帮助。


    更新

    我尝试在下面回答您的问题。

    redux 的一个很好的模式是对动作(和组件)隐藏状态形状。所以通过你需要登录的项目的路径并不是什么大问题。

    说了这么多,不知道有没有简单优雅的解决你的问题,不过我尽量给你一些思路:)

    例如,您可以在操作中设置 selector 的名称,而不是 路径,您想使用它来获取要记录的信息。

    这样安排: - 您将动作与状态解耦,因此将来您可以在不更改动作的情况下更改状态形状,而只需更改选择器 - 您可以使用reselect 并编写更多选择器来记录更多信息 - 你不需要使用eval - 你需要为每一个需要记录的信息设置一个选择器

    一些代码(未经测试)可以更好地解释我的意思。

    // action moveForward.js
    export function moveForward () {
      return {
       type: "MOVED_FORWARD_X_PAGES",
       payload: { numberOfPages: 5, nowShowingPage: 53 }
       meta: { selector: 'getShowedPage' }
       }
    }
    
    // action selectItem.js
    export function selectItem () {
      return {
       type: "SELECTED_ITEM_FROM_LIST",
       payload: { index: 4 }
       meta: { selector: 'getSelectedItem' }
       }
    }
    
    // reducer pages.js
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        case "MOVED_FORWARD_X_PAGES":
          return Object.assign({}, state, {
              numberOfPages: action.payload.numberOfPages,
              nowShowingPage: action.payload.nowShowingPage,
              // ...
           })
      }
    
    // reducer items.js
    const initialState = { list: [], currentItem: undefined }
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        case "SELECTED_ITEM_FROM_LIST":
          return Object.assign({}, state, {
              currentItem: action.payload.index,
              // ...
           })
      }
    
    
    // log_middleware.js
    import logger from './logger'
    export default store => next => action => {
        // if action not contains extra info to log (meta) go ahead
        if (!action.meta)
          return next(action)
    
        // Save current state and get the next one
        const prevState = store.getState()
        const returnedValue = next(nextAction)
        const nextState = store.getState()
    
       // Perform log 
       logger(prevState, nextState, action)
    
       // return the next state
       return returnedValue   
    }
    
    
    // selectors.js 
    const getShowedPage = state => state.nowShowingPage
    const getSelectedItem = state => state.list[state.currentItem]"
    
    export default selectors = {
       getShowedPage,
       getSelectedItem
    }
    
    // logger.js
    import selectors from '../selectors'
    
    export default logger (prevState, nextState, action) {
        const selector = state => selectors[action.meta.selector]
        const infoToLog = selector(nextState)
    
        // Perform log
        console.log(infoToLog)
    }
    

    要编写更多选择器,您可以使用reselect

    最后看一下 D. Abramov 的 this 选择器视频教程和 redux-logger 源代码。

    【讨论】:

    • 谢谢。您如何看待想要定义根据下一个状态记录什么的动作的问题?例如。 nowShowingPage 基于下一个状态,并且动作创建者在分派之前无法知道实际值。我的方法是添加状态路径的字符串表示来获取fromnext(),但这看起来有点像使用eval,不知道是不是看起来干净。 (它也可能变得复杂:要从我的第二个示例中获取“奶酪”,仅给出索引,我需要传递 itemSelected: "next.list[action.index]" 或其他东西。)
    • @SamFen 我已经更新了答案,告诉您如何使用middlewareselectors 来面对您的问题。顺便说一句,我不知道它是否符合您的要求。也许使用redux-saga 可以简化日志记录过程。
    • 谢谢。我还没有深入研究selectors,但这似乎正是解决方案。完美。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-19
    相关资源
    最近更新 更多