【问题标题】:Redux and Calendar repeating eventsRedux 和 Calendar 重复事件
【发布时间】:2018-02-04 05:43:22
【问题描述】:

在 redux 存储中存储/处理重复事件的正确方法应该是什么?

问题:假设我们有一个通过复杂的业务逻辑生成重复事件的后端 API。一些事件可能具有相同的 ID。假设生成的输出看起来是这样的:

[
  {
    "id": 1,
    "title": "Weekly meeting",
    "all_day": true,
    "starts_at": "2017-09-12",
    "ends_at": "2017-09-12"
  },
  {
    "id": 3,
    "title": "Daily meeting1",
    "all_day": false,
    "starts_at": "2017-09-12",
    "ends_at": "2017-09-12",
  },
  {
    "id": 3,
    "title": "Daily meeting1",
    "all_day": false,
    "starts_at": "2017-09-13",
    "ends_at": "2017-09-13",
  },
  {
    "id": 3,
    "title": "Daily meeting1",
    "all_day": false,
    "starts_at": "2017-09-14",
    "ends_at": "2017-09-14",
  }
]

可能的解决方案是:通过像这样组成附加属性 uid 来生成唯一 ID:id + # + starts_at。这样我们就可以唯一地识别每个事件。 (我现在正在使用)

示例

[
  {
    "id": 1,
    "uid": "1#2017-09-12",
    "title": "Weekly meeting",
    "all_day": true,
    "starts_at": "2017-09-12",
    "ends_at": "2017-09-12"
  }
]

我想知道有没有其他方法,可能比组合唯一 id 更优雅?

【问题讨论】:

    标签: json events calendar redux repeat


    【解决方案1】:

    最后这是我实现的(仅用于演示目的 - 省略了无关代码):

    eventRoot.js:

    import { combineReducers } from 'redux'
    import ranges from './events'
    import ids from './ids'
    import params from './params'
    import total from './total'
    
    export default resource =>
      combineReducers({
        ids: ids(resource),
        ranges: ranges(resource),
        params: params(resource)
      })
    

    events.js:

    import { GET_EVENTS_SUCCESS } from '@/state/types/data'
    
    export default resource => (previousState = {}, { type, payload, requestPayload, meta }) => {
      if (!meta || meta.resource !== resource) {
        return previousState
      }
      switch (type) {
        case GET_EVENTS_SUCCESS:
          const newState = Object.assign({}, previousState)
          payload.data[resource].forEach(record => {
            // ISO 8601 time interval string -
            // http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
            const range = record.start + '/' + record.end
            if (newState[record.id]) {
              if (!newState[record.id].includes(range)) {
                // Don't mutate previous state, object assign is only a shallow copy
                // Create new array with added id
                newState[record.id] = [...newState[record.id], range]
              }
            } else {
              newState[record.id] = [range]
            }
          })
          return newState
        default:
          return previousState
      }
    }
    

    还有一个数据缩减器,但它链接在父级缩减器中,因为通用实现可重复用于常见列表响应。更新事件数据并删除开始/结束属性,因为它由范围 (ISO 8601 time interval string) 组成。这可以稍后由 moment.range 使用或由 '/' 拆分以获取开始/结束数据。我选择了范围字符串数组来简化对现有范围的检查,因为它们可能会变大。我认为在这种情况下,原始字符串比较(indexOf 或 es6 包括)会比在复杂结构上循环更快。

    data.js(精简版):

    import { END } from '@/state/types/fetch'
    import { GET_EVENTS } from '@/state/types/data'
    
    const cacheDuration = 10 * 60 * 1000 // ten minutes
    const addRecords = (newRecords = [], oldRecords, isEvent) => {
      // prepare new records and timestamp them
      const newRecordsById = newRecords.reduce((prev, record) => {
        if (isEvent) {
          const { start, end, ...rest } = record
          prev[record.id] = rest
        } else {
          prev[record.id] = record
        }
        return prev
      }, {})
      const now = new Date()
      const newRecordsFetchedAt = newRecords.reduce((prev, record) => {
        prev[record.id] = now
        return prev
      }, {})
      // remove outdated old records
      const latestValidDate = new Date()
      latestValidDate.setTime(latestValidDate.getTime() - cacheDuration)
      const oldValidRecordIds = oldRecords.fetchedAt
        ? Object.keys(oldRecords.fetchedAt).filter(id => oldRecords.fetchedAt[id] > latestValidDate)
        : []
      const oldValidRecords = oldValidRecordIds.reduce((prev, id) => {
        prev[id] = oldRecords[id]
        return prev
      }, {})
      const oldValidRecordsFetchedAt = oldValidRecordIds.reduce((prev, id) => {
        prev[id] = oldRecords.fetchedAt[id]
        return prev
      }, {})
      // combine old records and new records
      const records = {
        ...oldValidRecords,
        ...newRecordsById
      }
      Object.defineProperty(records, 'fetchedAt', {
        value: {
          ...oldValidRecordsFetchedAt,
          ...newRecordsFetchedAt
        }
      }) // non enumerable by default
      return records
    }
    
    const initialState = {}
    Object.defineProperty(initialState, 'fetchedAt', { value: {} }) // non enumerable by default
    
    export default resource => (previousState = initialState, { payload, meta }) => {
      if (!meta || meta.resource !== resource) {
        return previousState
      }
      if (!meta.fetchResponse || meta.fetchStatus !== END) {
        return previousState
      }
      switch (meta.fetchResponse) {
        case GET_EVENTS:
          return addRecords(payload.data[resource], previousState, true)
        default:
          return previousState
      }
    }
    

    这可以被带有事件选择器的日历组件使用:

    const convertDateTimeToDate = (datetime, timeZoneName) => {
      const m = moment.tz(datetime, timeZoneName)
      return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0)
    }
    
    const compileEvents = (state, filter) => {
      const eventsRanges = state.events.list.ranges
      const events = []
      state.events.list.ids.forEach(id => {
        if (eventsRanges[id]) {
          eventsRanges[id].forEach(range => {
            const [start, end] = range.split('/').map(d => convertDateTimeToDate(d))
            // You can add an conditional push, filtered by start/end limits
            events.push(
              Object.assign({}, state.events.data[id], {
                start: start,
                end: end
              })
            )
          })
        }
      })
      return events
    }
    

    这是数据结构在 redux 开发工具中的样子:

    每次获取事件时,都会更新其数据(如果有更改)并添加引用。这是获取新事件范围后redux diff的屏幕截图:

    希望这对某人有所帮助,我只是补充一点,这还没有经过实战测试,而是更多地证明了一个有效的概念。

    [编辑]顺便说一句。我可能会将其中一些逻辑移至后端,因为这样就不需要拆分/加入/删除属性。

    【讨论】:

      【解决方案2】:

      据我了解您给出的示例,每当事件的详细信息发生变化时,服务器似乎都在发送特定事件。

      如果是这样,并且您想跟踪事件的更改,您可能的形状可能是一个对象数组,其中包含保存当前数据的事件的所有字段,以及一个包含所有元素的数组的历史属性前一个(或 n 个最近的)事件对象和接收它们的时间戳。这就是你的 reducer 的外观,每个事件只存储五个最近的事件更改。我希望该操作具有 payload 属性,该属性具有您的标准 event 属性和时间戳属性,这可以在操作创建器中轻松完成。

      const event = (state = { history: [] }, action) => {
        switch (action.type) {
          case 'EVENT_FETCHED':
            return ({
              ...action.payload.event,
              history: [...state.history, action.payload].slice(-5),
            });
          default:
            return state;
          }
        };
      
      
      const events = (state = { byID: {}, IDs: [] }, action) => {
        const id = action.payload.event.ID;
        switch (action.type) {
          case 'EVENT_FETCHED':
            return id in state.byID
              ? {
                ...state,
                byID: { ...state.byID, [id]: event(state.byID[id], action) },
              }
              : {
                byID: { ...state.byID, [id]: event(undefined, action) },
                IDs: [id],
              };
          default:
            return state;
        }
      };
      

      这样做,您不需要任何唯一 ID。如果我误解了您的问题,请告诉我。

      编辑:这是 Redux 文档中 pattern 的轻微扩展,用于存储以前的事件。

      【讨论】:

      • 事情是我需要保留starts_at,这对于具有相同ID的某些事件是不同的(其他一切都是相同的)。您的实现只是将所有事件收集为单独的数组。规范化应该以这样一种方式完成,即通过使用 ID(或 UID),您可以获得确切的事件并将其重新用于代码的其他部分。我的实现已经工作了,所以不是让它工作的问题,而是如何优化它以及这种情况的标准是什么(如果存在的话)。
      • 当一个事件数据发生变化时(除了start_at),变化应该传播到所有具有相同ID的事件。
      【解决方案3】:

      也许没有太大的改进(如果有的话),但仅使用 JSON.stringify 检查重复项可能会使唯一 ID 过时。

      const existingEvents = [
        {
          "id": 3,
          "title": "Daily meeting1",
          "all_day": false,
          "starts_at": "2017-09-14",
          "ends_at": "2017-09-14",
        }
      ];
      
      const duplicate = {
          "id": 3,
          "title": "Daily meeting1",
          "all_day": false,
          "starts_at": "2017-09-14",
          "ends_at": "2017-09-14",
      };
      
      const eventIsDuplicate = (existingEvents, newEvent) => {
          const duplicate = 
          existingEvents.find(event => JSON.stringify(event) == JSON.stringify(newEvent));
          return typeof duplicate != 'undefined';
      };
      
      console.log(eventIsDuplicate(existingEvents, duplicate)); // true
      

      如果出于某种原因,您希望将所有唯一性逻辑保留在客户端,我想这只会比您现有的解决方案更可取。

      【讨论】:

      • 好吧,重点不在于保留唯一性,而在于正确处理存储中的数据,并且可能通过重新使用相同的 UID 来获得可以在其他地方使用的规范化数据。让组合键在我的情况下有效,否则总是有一个选项可以将 ends_at 添加到组合键中,即使结束日期已更改,这也会使事件防弹(在我的情况下,这些数据完全相等..只有开始日期已更改)
      【解决方案4】:

      您当前的解决方案可能存在缺陷。如果两个事件的idstart_id 相同,会发生什么?您的域中是否可能出现这种情况?

      因此,在这种情况下,我通常使用this nice lib。它产生非常短的唯一 ID,它们具有一些很好的属性,比如保证不相交、不可预测等等。

      还要问问自己,在你的情况下你是否真的需要唯一的 ID。看起来您的后端无论如何都没有机会区分事件,那何必呢? Redux 商店将很乐意在没有 uid 的情况下保留您的事件事件。

      【讨论】:

      • 首先感谢您的重播,在解决问题时很高兴听到其他人的意见。如果idstart_id 恰好相同(在我的情况下),我可以说这是同一个事件。假设您有 10 个不同的学生班级,每天都在重复。就我而言,我选择了在添加订阅时填充的动态 MM 表。在此之前,事件是动态呈现的。这样我就不会用无效的条目/关系污染数据库。其他选项是为每个事件的发生创建一个记录(这可能需要一段时间对客户来说)
      猜你喜欢
      • 2012-02-10
      • 1970-01-01
      • 2015-08-10
      • 2013-07-06
      • 1970-01-01
      • 1970-01-01
      • 2014-06-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多