Цикл Js - Подход к синхронизации истории с государством?

Я пытаюсь синхронизировать историю браузера с состоянием, содержащимся в хранилище состояний onionify в очень простом цикле js пример приложения. У меня нет проблем с отображением из хранилища состояний в историю, проблема заключается в сокращении history.state обратно в хранилище состояний. По сути, я попал в бесконечный цикл потоков состояний, перетекающих друг в друга, это приводит к сбою браузера.

import { run } from "@cycle/run";
import { div, button, p, makeDOMDriver } from "@cycle/dom";
import onionify from "cycle-onionify";
import { makeHistoryDriver } from "@cycle/history";
import xs from "xstream";

const main = (sources) => {
  const popHistory$ = sources.history; // This is the problem...

  const action$ = xs.merge(
    sources.DOM.select(".decrement").events("click").map( () => -1 ),
    sources.DOM.select(".increment").events("click").map( () => +1 ),
  );

  const state$ = sources.onion.state$;

  const vdom$ = state$.map( state =>
    div([
      button(".decrement", "Decrement"),
      button(".increment", "Increment"),
      p(`Count: ${state.count}`)
    ])
  );

  const initReducer$ = xs.of( function initReducer() {
    return {count: 0};
  });

  const updateReducer$ = action$.map( x => function reducer(prevState) {
    return {count: prevState.count + x};
  });
  // this is where the inifinity loop starts
  const historyReducer$ = popHistory$.map( history => function(prevState) {
    return {count: history.state.count};
  });
  // You can't merge historyReducer$ here
  const reducer$ = xs.merge(initReducer$, updateReducer$, historyReducer$);

  const pushHistory$ = state$.map( state => ({
    type: "push",
    pathname: `/?count=${state.count}`,
    state: state
  }));

  return {
    DOM: vdom$,
    onion: reducer$,
    history: pushHistory$,
    debug: popHistory$
  }
};

const onionMain = onionify(main);

run(onionMain, {
  DOM: makeDOMDriver("#main"),
  history: makeHistoryDriver(),
  debug: $ => $.subscribe({next: console.log})
});

Я думаю, мой вопрос: есть ли более простой способ сделать это? Есть ли оператор, который помогает здесь? Я чувствую, что то, что я пытаюсь сделать, в принципе невозможно. Будем благодарны за любые ответы или ссылки на полезные ресурсы.

2 ответа

Учитывая, что я понял это, я подумал, что я могу также опубликовать минимальный рабочий пример маршрутизации с историей / состоянием в хранилище состояний лука. Это может быть полезно для кого-то в будущем в качестве ссылки. Ответ состоял в том, чтобы отфильтровать данные, возвращающиеся в историю из исторических публикаций.

import { run } from "@cycle/run";
import { div, button, p, makeDOMDriver } from "@cycle/dom";
import { makeHistoryDriver } from "@cycle/history";
import onionify from "cycle-onionify";
import xs from "xstream";

const main = (sources) => {
  const {
    DOM: domSource$,
    history: historySource$,
    onion: onionSource
  } = sources;

  const action$ = xs.merge(
    domSource$.select(".decrement").events("click")
      .mapTo( state => ({...state, count: state.count - 1, avoidHist: false}) ),

    domSource$.select(".increment").events("click")
      .mapTo( state => ({...state, count: state.count + 1, avoidHist: false}) ),

    historySource$.filter( e => e.state !== undefined ) // essentially captures forward and back button events
           .map( e => e.state.count )
           .map( count => state => ({...state, count, avoidHist: true}) ),

    historySource$.filter( e => e.state === undefined && e.hash !== "" ) // capture hash change events and grab state data with regex
           .map( e => e.hash )
           .map( hashStr => hashStr.match(/count=(-?\d{1,16})/) )
           .filter( val => val !== null )
           .map( arr => arr[1] )
           .map( str => Number(str) )
           .map( count => state => ({...state, count, avoidHist: true}) ) // add state property for filtering history
  );

  const state$ = onionSource.state$;
  const initReducer$ = xs.of( () => ({count: 0}) );
  const onionSink$ = xs.merge(initReducer$, action$);

  const domSink$ = state$.map( state =>
    div([
      button(".decrement", "Decrement"),
      button(".increment", "Increment"),
      p(`Count: ${state.count}`)
    ])
  );

  const historySink$ = state$.filter( state => state.avoidHist !== true ) // filter for avoid history property
    .map( state => ({
    type: "push",
    pathname: `#count=${state.count}`,
    state
  }));

  return {
    DOM: domSink$,
    history: historySink$,
    onion: onionSink$
  };
};

const onionMain = onionify(main);

run(onionMain, {
  DOM: makeDOMDriver("#main"),
  history: makeHistoryDriver()
});

Я предполагаю, что ключом к созданию поведения приложения, которое имитирует взаимодействие серверных веб-страниц с историей, является то, что ни одно из отображений из истории popstate не возвращается в историю. Кажется очевидным сейчас, но мне потребовалось некоторое время, чтобы понять это.

Из того, что я могу понять, я полагаю, что вам просто не хватает одного dropRepeats на потоке, который питает history Водитель.

Это не кусок кода, который я протестировал, но я считаю, что заменить эту строку

const pushHistory$ = state$.map(...)

с

 import dropRepeats from 'xstream/extra/dropRepeats'

 // ....

 const pushHistory$ = state$.compose(dropRepeats()).map(...)

Исправлю твою проблему.

Это отфильтрует pushState, когда состояние на самом деле не изменилось, но будет синхронизировать URL-адрес с тем, что находится в этом состоянии, и вам не потребуется менять все прослушиватели событий, как вы делали в предыдущем ответе:)

В дополнениях к xstream есть несколько интересных операторов https://github.com/staltz/xstream/blob/master/EXTRA_DOCS.md

Другие вопросы по тегам