2017-08-26 1 views
5

Was sollte die richtige Art sein, wiederkehrende Ereignisse im Redux-Speicher zu speichern/zu verarbeiten?Wiederholen von Redux- und Kalenderereignissen

Problem: Nehmen wir an, wir haben eine Backend-API, die wiederkehrende Ereignisse über eine komplizierte Geschäftslogik generiert. Einige der Ereignisse haben möglicherweise dieselbe ID. Lassen Sie uns sagen, dass generierte Ausgabe so aussieht:

[ 
    { 
    "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", 
    } 
] 

Mögliche Lösung wäre: erzeugen eindeutige ID, indem zusätzliche Eigenschaft uid zusammengesetzt wie folgt aus: id + # + starts_at. Auf diese Weise konnten wir jedes Vorkommnis eindeutig identifizieren. (Ich verwende dieses Recht jetzt) ​​

Beispiel:

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

Ich frage mich, gibt es eine andere Art und Weise, vielleicht elegante als eindeutige ID zusammengesetzt zu haben?

Antwort

0

Am Ende ist das, was ich implementiert habe (zu Demonstrationszwecken nur - nicht verwandter Code ist weggelassen):

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 
    } 
} 

Es gibt auch einen Datenreduzierer, der jedoch aufgrund der generischen Implementierung, die für allgemeine Listenantworten wiederverwendet wird, im übergeordneten Reduzierer verknüpft ist. Ereignisdaten werden aktualisiert und die Start-/Endeigenschaft wird entfernt, da sie nach Bereich (ISO 8601 time interval string) zusammengesetzt ist. Dies kann später von moment.range verwendet oder durch '/' geteilt werden, um Start-/Enddaten zu erhalten. Ich habe mich für ein Array von Bereichszeichenfolgen entschieden, um das Überprüfen vorhandener Bereiche zu vereinfachen, da diese möglicherweise größer werden. Ich denke, dass der primitive String-Vergleich (indexOf oder es6) schneller ist als das Schleifen komplexer Strukturen in solchen Fällen.

data.js (abgespeckte Version):

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 
    } 
} 

Dies kann dann durch eine Kalenderkomponente mit Ereignis-Selektor verwendet werden:

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 
} 

Und hier ist, wie sich die Datenstruktur in der aussieht redux Entwickler-Tools:

https://i.imgur.com/5nzrG6e.png

Eac h Zeit die Ereignisse abgerufen werden, ihre Daten aktualisiert werden (wenn es eine Änderung gibt) und Referenzen hinzugefügt werden. Hier ist ein Screenshot von redux diff nach neuen Ereignissen holen reichen:

enter image description here

Hope this jemand hilft, werde ich nur hinzufügen, dass dies noch nicht getestet Schlacht ist aber mehr ein Beweis für ein Konzept, das funktioniert.

[Bearbeiten] BTW. Ich werde wahrscheinlich etwas von dieser Logik in das Backend verschieben, da dann keine Eigenschaften geteilt/hinzugefügt/gelöscht werden müssen.

1

Es gibt einen möglichen Fehler mit Ihrer aktuellen Lösung. Was passiert, wenn id und start_id von zwei Ereignissen identisch sind? Ist das in Ihrer Domain möglich?

Deshalb verwende ich normalerweise this nice lib in solchen Fällen. Es erzeugt wirklich kurze eindeutige IDs, die einige nette Eigenschaften haben, wie Garantien, sich nicht zu schneiden, unvorhersehbar zu sein und so weiter.

Fragen Sie sich auch, ob Sie tatsächlich eindeutige IDs in Ihrem Fall benötigen. Sieht so aus, als ob dein Backend keine Chance hat, Events zu unterscheiden, also warum? Der Redux Store behält Ihre Veranstaltung ohne uid.

+0

Nun erst einmal vielen Dank für die Wiederholung, es ist gut zu hören andere Leute Meinungen bei der Fehlersuche. Wenn 'id' und 'start_id' zufällig identisch sind (in meinem Fall), kann ich sagen, dass es dasselbe Ereignis ist. Nehmen wir an, Sie haben 10 verschiedene Klassen, die sich täglich wiederholen. In meinem Fall habe ich mich für eine dynamische MM-Tabelle entschieden, die beim Hinzufügen eines Abonnements ausgefüllt wird. Bis dahin werden Ereignisse dynamisch dargestellt. Auf diese Weise verschmutze ich DB nicht mit ungültigen Einträgen/Relationen. Eine andere Möglichkeit wäre, einen Datensatz für jedes Ereignis zu erstellen (und das kann eine Weile dauern) –

1

Vielleicht nicht viel von einer Verbesserung (wenn überhaupt), aber nur JSON.stringify zu verwenden, um nach Duplikaten zu suchen, könnte eindeutige IDs überflüssig machen.

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 

Ich denke, dies nur zu Ihrer bestehenden Lösung vorzuziehen wäre, wenn aus irgendeinem Grunde, Sie alle die Einzigartigkeit Logik auf der Client-Seite halten wollen würden.

+0

Nun, es geht nicht darum, die Eindeutigkeit zu bewahren, sondern die Daten im Speicher richtig zu behandeln und vielleicht auch normalisierte Daten zu haben an anderer Stelle verwendet werden, indem dieselbe UID wiederverwendet wird. In meinem Fall funktioniert der zusammengesetzte Schlüssel, sonst gibt es immer eine Option, um ends_at zu dem zusammengesetzten Schlüssel hinzuzufügen, und das würde das Ereignis kugelsicher machen, selbst wenn sich das Enddatum geändert hat (in meinem Fall sind diese Daten vollkommen gleich.) Das Startdatum ist mutiert) –

1

Soweit ich die Beispiele verstehe, die Sie gegeben haben, scheint es, als ob der Server ein bestimmtes Ereignis sendet, wenn sich die Details des Ereignisses ändern.

Wenn dies der Fall ist und Sie die Änderungen an Ereignissen verfolgen möchten, könnte Ihre Form möglicherweise ein Array von Objekten mit allen Feldern des Ereignisses sein, die die aktuellen Daten enthalten, und eine history-Eigenschaft, die ein Array von alle vorherigen (oder n letzten) Ereignisobjekte und die Zeitstempel, zu denen sie empfangen wurden. So würden Ihre Reducer aussehen und nur die fünf letzten Event-Änderungen für jedes Event speichern. Ich erwarte, dass die Aktion eine payload Eigenschaft besitzt, die Ihre Standard-event-Eigenschaft und eine Timestamp-Eigenschaft hat, die einfach im Aktionsersteller ausgeführt werden kann.

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; 
    } 
}; 

Dazu benötigen Sie keine eindeutige ID. Bitte lassen Sie mich wissen, wenn ich Ihr Problem falsch verstanden habe.

Edit: Dies ist eine geringfügige Erweiterung der pattern in der Redux-Dokumentation, um vorherige Ereignisse zu speichern.

+0

Das ist, dass ich Starts_at beibehalten muss, die für einige Ereignisse mit derselben ID unterscheidet (alles andere ist gleich). Ihre Implementierung sammelt nur alle Ereignisse als getrennte Arrays. Die Normalisierung sollte so erfolgen, dass Sie mit einer ID (oder UID) ein genaues Ereignis erhalten und es für andere Teile des Codes wiederverwenden können. Meine Implementierung funktioniert bereits. Es geht also nicht darum, dass es funktioniert, sondern darum, wie es optimiert werden kann und welche Standards für diese Fälle gelten (falls vorhanden). –

+0

Und wenn sich eine Ereignisdaten ändern (alles andere als start_at), sollte die Änderung an alle Ereignisse mit derselben ID weitergegeben werden. –

Verwandte Themen