Redux 和日历重复事件
Redux and Calendar repeating events
在 redux 存储中存储/处理重复事件的正确方法应该是什么?
问题:假设我们有一个后端 API 通过复杂的业务生成重复事件 logic.Some 事件可能具有相同的 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 更优雅?
您当前的解决方案可能存在缺陷。如果两个事件的 id
和 start_id
相同会发生什么?在您的域中是否可能出现这种情况?
因此,我通常在这种情况下使用 this nice lib。它会生成非常短的唯一 ID,这些 ID 具有一些不错的属性,例如保证不相交、不可预测等。
还要问问自己,在您的案例中是否真的需要唯一 ID。看起来你的后端无论如何都没有机会区分事件,所以何必呢?没有 uid
,Redux store 会很乐意保留你的 events 事件。
也许没有太大的改进(如果有的话)但仅使用 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
我想这只会比您现有的解决方案更可取,如果出于某种原因,您希望在客户端保留所有唯一性逻辑。
据我了解您提供的示例,每当事件的详细信息发生变化时,服务器似乎都会发送特定事件。
如果是这样,并且您想跟踪事件的变化,您的可能形状可能是一个对象数组,其中包含保存当前数据的事件的所有字段,以及一个历史记录 属性这是所有先前(或最近的 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 的轻微扩展,用于存储以前的事件。
最后这是我实现的(仅用于演示目的 - 省略了无关代码):
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
}
}
还有一个数据缩减器,但由于通用实现被重新用于公共列表响应,它链接在父缩减器中。更新事件数据并删除 start/end 属性,因为它由范围 (ISO 8601 time interval string) 组成。这可以稍后被 moment.range 使用或用 '/' 拆分以获得 start/end 数据。我选择了范围字符串数组来简化对现有范围的检查,因为它们可能会变大。我认为在这种情况下,原始字符串比较(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 的屏幕截图:
希望这对某些人有所帮助,我只想补充一点,这仍然没有经过实战测试,但更多的是对一个有效概念的证明。
[编辑] 顺便说一句。我可能会将其中的一些逻辑移到后端,因为这样就不需要拆分/加入/删除属性了。
在 redux 存储中存储/处理重复事件的正确方法应该是什么?
问题:假设我们有一个后端 API 通过复杂的业务生成重复事件 logic.Some 事件可能具有相同的 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 更优雅?
您当前的解决方案可能存在缺陷。如果两个事件的 id
和 start_id
相同会发生什么?在您的域中是否可能出现这种情况?
因此,我通常在这种情况下使用 this nice lib。它会生成非常短的唯一 ID,这些 ID 具有一些不错的属性,例如保证不相交、不可预测等。
还要问问自己,在您的案例中是否真的需要唯一 ID。看起来你的后端无论如何都没有机会区分事件,所以何必呢?没有 uid
,Redux store 会很乐意保留你的 events 事件。
也许没有太大的改进(如果有的话)但仅使用 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
我想这只会比您现有的解决方案更可取,如果出于某种原因,您希望在客户端保留所有唯一性逻辑。
据我了解您提供的示例,每当事件的详细信息发生变化时,服务器似乎都会发送特定事件。
如果是这样,并且您想跟踪事件的变化,您的可能形状可能是一个对象数组,其中包含保存当前数据的事件的所有字段,以及一个历史记录 属性这是所有先前(或最近的 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 的轻微扩展,用于存储以前的事件。
最后这是我实现的(仅用于演示目的 - 省略了无关代码):
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
}
}
还有一个数据缩减器,但由于通用实现被重新用于公共列表响应,它链接在父缩减器中。更新事件数据并删除 start/end 属性,因为它由范围 (ISO 8601 time interval string) 组成。这可以稍后被 moment.range 使用或用 '/' 拆分以获得 start/end 数据。我选择了范围字符串数组来简化对现有范围的检查,因为它们可能会变大。我认为在这种情况下,原始字符串比较(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 的屏幕截图:
希望这对某些人有所帮助,我只想补充一点,这仍然没有经过实战测试,但更多的是对一个有效概念的证明。
[编辑] 顺便说一句。我可能会将其中的一些逻辑移到后端,因为这样就不需要拆分/加入/删除属性了。