Подтвердить что ты не робот

React/Flux и xhr/routing/caching

Это больше похоже на то, "каково ваше мнение/исправляюсь ли я в этом?" вопрос.

Пытаясь быть как можно более строгим, понимая Flux, я пытался выяснить, где сделаны звонки XHR, обрабатываются веб-гейты/внешние раздражители, происходит маршрутизация и т.д.

Из того, что я читаю в разных статьях, интервью и просмотре примеров facebook, есть несколько способов справиться с этими вещами. После строгого потока создатели Action - это те, которые выполняют все вызовы XHR с возможностью запуска действий PENDING/SUCCESS/FAILURE до и после завершения запроса.
Другой был, исходя из facebook Ian Obermiller, все запросы READ (GET) обрабатываются непосредственно Магазинами (без участия создателя/диспетчера действий), а запросы WRITE (POST) обрабатываются создателями Action, проходящими через все действия > диспетчеp > хранить поток.

Некоторые договоренности/выводы, которые мы нарисовали/хотели бы придерживаться:

  • В идеале все, что происходит в/из системы, происходит только через Actions.
  • Асинхронные вызовы, выходящие/входящие в систему, будут иметь PENDING/PROGRESS (думаю, файлы загружаются)/SUCCESS/FAILURE Actions.
  • Один диспетчер по всему приложению.
  • Действие > Диспетчеp > Сохраненные вызовы строго синхронны, чтобы отправлять сообщения, которые не могут начать другую отправку внутри, чтобы избежать цепочки событий/действий.
  • Хранилища сохраняются в представлениях (учитывая его одностраничное приложение, вы хотите иметь возможность повторно использовать данные)

Несколько вопросов, с которыми мы пришли к какому-то заключению, но я не полностью удовлетворен:

  • Если вы примете подход, в котором магазины читают, а также действия для написания писем, как вы справляетесь с ситуациями, когда несколько магазинов могут использовать данные из одного вызова XHR?
    Пример: вызовы API, выпущенные TeamStore до /api/teams/{id}, который возвращает что-то вроде:

        {  
            entities: {  
                teams: [{  
                    name: ...,  
                    description: ...,  
                    members: [1, 2, 4],  
                    version: ...  
                }],  
                users: [{  
                    id: 1  
                    name: ...,  
                    role: ...,  
                    version: ...  
                },  
                {  
                    id: 2  
                    name: ...,  
                    role: ...,  
                    version: ...  
                },  
                {  
                    id: 3  
                    name: ...,  
                    role: ...,  
                    version: ...  
                }]  
            }  
        }  
    

    В идеале я также хотел бы обновить MemberStore с информацией, возвращаемой в этом API. Мы поддерживаем номер версии для каждого объекта, который обновляется при обновлении записи, и это то, что мы используем внутренне, отклоняем вызовы на устаревшие данные и т.д. Используя это, я мог бы иметь внутреннюю логику, где, если бы я был побочным эффектом какой-то другой вызов API, я знаю, что мои данные устарели, я запускаю обновление на этой записи.
    Возможно, решение состоит в том, что вам понадобится магазин для запуска действия (которое эффективно обновит другие зависимые магазины). Это короткое замыкание в Store > View > Action to Store > Action, и я не уверен, что это хорошая идея. У нас уже есть одна вещь, которая не синхронизируется с Магазинами, которые делают свои собственные звонки XHR. Концессии, подобные этим, в конечном итоге начнут проникать во всю систему.
    Или магазины, которые знают о других магазинах и могут общаться с ними. Но это ломает магазины, у которых нет правил Setters.

    1. Простым решением вышеупомянутой проблемы будет то, что вы будете придерживаться "Действия", являющиеся ТОЛЬКО местом, в котором происходит внешний входящий/исходящий стимул. Это упрощает логику обновления нескольких магазинов.
      Но теперь, где и как вы обрабатываете кеширование? Мы пришли к выводу, что кэширование произойдет на уровне API Utils/DAO. (если вы посмотрите на диаграмму потоков).
      Но это создает другие проблемы. Чтобы лучше понять/объяснить, что я имею в виду, например:

      • /api/teams возвращает список всех команд, с которыми я показываю список всех команд.
      • При нажатии на ссылку команды я перехожу к его подробному представлению, для которого требуются данные из /api/teams/{id}, если он еще не присутствует в магазине.
        Если Actions обрабатывает все XHR, View будет делать что-то вроде TeamActions.get([id]), которое делает TeamDAO.get([id]). Чтобы иметь возможность немедленно вернуть этот вызов (так как мы его кэшировали), DAO придется выполнять кэширование, но также поддерживать связь между коллекциями/элементами. Эта логика, по дизайну, уже присутствует в магазинах.
        Вот и вопросы:

      • Вы дублируете эту логику в DAO и магазинах?

      • Вы информируете DAO о Магазинах, и они могут спросить у Магазина, есть ли у них уже какие-то данные и просто вернуть сообщение 302, вы хорошо знаете последние данные.
    2. Как вы обрабатываете проверку, которая включает в себя XHR API? Что-то простое, как дубликаты имен команд.
      Представления напрямую попадают в DAO и делают что-то вроде TeamDAO.validateName([name]), которое возвращает обещание или вы создаете действие? Если вы создаете действие, через которое Store делает действительный/недопустимый поток обратно в представление, рассматривая его в основном переходные данные?

    3. Как вы обрабатываете маршрутизацию? Я посмотрел через ретранслятор, и я не уверен, что мне это нравится. Я не обязательно думаю, что принудительный способ JSX реагирования на предоставление маршрутных сопоставлений/конфигов вообще необходим. Кроме того, по-видимому, в нем используется собственный RouteDispatcher, который применяется к одному правилу диспетчера.
      Решение, которое я предпочитаю, было получено из некоторых сообщений в блогах /SO, где у вас есть сопоставления маршрутов, хранятся в RouteStore.
      RouteStore также поддерживает CURRENT_VIEW. Реактивный компонент AppContainer регистрируется в RouteStore и заменяет его дочерние представления CURRENT_VIEW на изменение. Текущие представления сообщают AppContainer, когда они полностью загружены, и AppContainer запускает RouteActions.pending/success/failure, возможно, с некоторым контекстом, чтобы информировать другие компоненты о достижении стабильного состояния, показывать/скрывать индикаторы занятости/загрузки.

Что-то, что я не смог создать, было чисто, если бы вы планировали маршрутизацию, похожую на Gmail, как бы вы это сделали? Некоторые замечания Gmail о том, что я большой поклонник:

  • URL-адреса не изменяются, пока страница не будет готова к загрузке. Он остается на текущем URL-адресе, пока его "Загрузка", и переходит на новый, когда загрузка завершена. Это делает так, что...
  • При сбое вы не потеряете свою текущую страницу вообще. Поэтому, если вы работаете, а "Отправить" не удается, вы не потеряете свою почту (т.е. Вы не потеряете текущий стабильный вид/состояние). (они не делают этого, потому что автоматическое сохранение - это pwn, но вы получаете идею). У вас есть возможность копировать/вставлять почту где-то для безопасного хранения, пока вы не сможете отправить ее снова.

Некоторые ссылки:
https://github.com/gaearon/flux-react-router-example http://ianobermiller.com/blog/2014/09/15/react-and-flux-interview/ https://github.com/facebook/flux

4b9b3361

Ответ 1

Это моя реализация с использованием facebook Flux и Immutable.js, которые, как мне кажется, отвечают на многие ваши проблемы, основанные на нескольких эмпирических правилах:

МАГАЗИНЫ

  • Магазины отвечают за сохранение состояния данных через Immutable.Record и сохраняют кеш через глобальный Immutable.OrderedMap ссылка Record на экземпляр ids.
  • Сохраняет непосредственно вызов WebAPIUtils для операций читать и запускает actions для операций write.
  • Отношения между RecordA и FooRecordB разрешаются из экземпляра RecordA с помощью параметров foo_id и извлекаются с помощью вызова, такого как FooStore.get(this.foo_id)
  • В магазинах хранятся только методы getters, такие как get(id), getAll() и т.д.

APIUTILS

  • Я использую SuperAgent для вызовов ajax. Каждый запрос завернут в Promise
  • Я использую карту read запроса Promise, индексированного хешем URL + params
  • Я запускаю действие через ActionCreators, например fooReceived или fooError, когда Promise разрешен или отклонен.
  • fooError действие обязательно должно содержать полезную нагрузку с ошибками проверки, возвращаемыми сервером.

компоненты

  • Компонент просмотра контроллера прослушивает изменения в хранилищах.
  • Все мои компоненты, кроме компонента контроллера, являются "чистыми", поэтому я использую ImmutableRenderMixin только для повторного отображения того, что действительно нужно (это означает, что if вы печатаете Perf.printWasted время, оно должно быть очень низким, несколько мс.
  • Поскольку Relay и GraphQL еще не открыты, я обязуюсь как можно более четко указать мой компонент props через propsType.
  • Родительский компонент должен только пропускать необходимые реквизиты. Если мой родительский компонент содержит объект, например var fooRecord = { foo:1, bar: 2, baz: 3}; (я не использую Immutable.Record здесь для простоты этого примера), а мой дочерний компонент должен отображать fooRecord.foo и fooRecord.bar, я не передавать весь объект foo, а только fooRecordFoo и fooRecordBar в качестве реквизита моего дочернего компонента, потому что другой компонент может редактировать значение foo.baz, делая повторный рендеринг дочернего компонента, пока этот компонент не нужно вообще это значение!

ПРОКЛАДКА - Я просто использую ReactRouter

ВЫПОЛНЕНИЕ

Вот пример:

апи

apiUtils/Request.js

var request = require('superagent');

//based on http://stackoverflow.com/a/7616484/1836434
var hashUrl = function(url, params) {
    var string = url + JSON.stringify(params);
    var hash = 0, i, chr, len;
    if (string.length == 0) return hash;
    for (i = 0, len = string.length; i < len; i++) {
        chr   = string.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
}

var _promises = {};

module.exports = {

    get: function(url, params) {
        var params = params || {};
        var hash = hashUrl(url, params);
        var promise = _promises[hash];
        if (promise == undefined) {
            promise = new Promise(function(resolve, reject) {
                request.get(url).query(params).end( function(err, res) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(res);
                    }
                });
            });
            _promises[hash] = promise;
        }
        return promise;
    },

    post: function(url, data) {
        return new Promise(function(resolve, reject) {

            var req = request
                .post(url)
                .send(data)
                .end( function(err, res) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(res);
                    }
                });

        });
    }

};

apiUtils/FooAPI.js

var Request = require('./Request');
var FooActionCreators = require('../actions/FooActionCreators');

var _endpoint = 'http://localhost:8888/api/foos/';

module.exports = {

    getAll: function() {
        FooActionCreators.receiveAllPending();
        Request.get(_endpoint).then( function(res) {
            FooActionCreators.receiveAllSuccess(res.body);
        }).catch( function(err) {
            FooActionCreators.receiveAllError(err);
        });
    },

    get: function(id) {
        FooActionCreators.receivePending();
        Request.get(_endpoint + id+'/').then( function(res) {
            FooActionCreators.receiveSuccess(res.body);
        }).catch( function(err) {
            FooActionCreators.receiveError(err);
        });
    },

    post: function(fooData) {
        FooActionCreators.savePending();
        Request.post(_endpoint, fooData).then (function(res) {
            if (res.badRequest) { //i.e response return code 400 due to validation errors for example
                FooActionCreators.saveInvalidated(res.body);
            }
            FooActionCreators.saved(res.body);
        }).catch( function(err) { //server errors
            FooActionCreators.savedError(err);
        });
    }

    //others foos relative endpoints helper methods...

};

магазины

магазины/BarStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../APIUtils/BarAPI')
var CHANGE_EVENT = 'change';

var _bars = Immutable.OrderedMap();

class Bar extends Immutable.Record({
    'id': undefined,
    'name': undefined,
    'description': undefined,
}) {

    isReady() {
        return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar data if it is ready.
    }

    getBar() {
        return BarStore.get(this.bar_id);
    }
}

function _rehydrate(barId, field, value) {
    //Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
    _bars = _bars.updateIn([barId, field], function() {
        return value;
    });
}


var BarStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_bars.has(id)) {
            BarAPI.get(id);
            return new Bar(); //we return an empty Bar record for consistency
        }
        return _bars.get(id)
    },

    getAll: function() {
        return _bars.toList() //we want to get rid of keys and just keep the values
    },

    Bar: Bar,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

var _setBar = function(barData) {
    _bars = _bars.set(barData.id, new Bar(barData));
};

var _setBars = function(barList) {
    barList.forEach(function (barData) {
        _setbar(barData);
    });
};

BarStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   
        case ActionTypes.BAR_LIST_RECEIVED_SUCESS:
            _setBars(action.barList);
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_RECEIVED_SUCCESS:
            _setBar(action.bar);
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_REHYDRATED:
            _rehydrate(
                action.barId,
                action.field,
                action.value
            );
            BarStore.emitChange();
            break;
    }
});

module.exports = BarStore;

магазины/FooStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/FooConstants').ActionTypes;
var BarStore = require('./BarStore');
var FooAPI = require('../APIUtils/FooAPI')
var CHANGE_EVENT = 'change';

var _foos = Immutable.OrderedMap();

class Foo extends Immutable.Record({
    'id': undefined,
    'bar_id': undefined, //relation to Bar record
    'baz': undefined,
}) {

    isReady() {
        return this.id != undefined;
    }

    getBar() {
        // The whole point to store an id reference to Bar
        // is to delegate the Bar retrieval to the BarStore,
        // if the BarStore does not have this Bar object in
        // its cache, the BarStore will trigger a GET request
        return BarStore.get(this.bar_id); 
    }
}

function _rehydrate(fooId, field, value) {
    _foos = _foos.updateIn([voucherId, field], function() {
        return value;
    });
}

var _setFoo = function(fooData) {
    _foos = _foos.set(fooData.id, new Foo(fooData));
};

var _setFoos = function(fooList) {
    fooList.forEach(function (foo) {
        _setFoo(foo);
    });
};

var FooStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_foos.has(id)) {
            FooAPI.get(id);
            return new Foo();
        }
        return _foos.get(id)
    },

    getAll: function() {
        if (_foos.size == 0) {
            FooAPI.getAll();
        }
        return _foos.toList()
    },

    Foo: Foo,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

FooStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {
        case ActionTypes.FOO_LIST_RECEIVED_SUCCESS:
            _setFoos(action.fooList);
            FooStore.emitChange();
            break;

        case ActionTypes.FOO_RECEIVED_SUCCESS:
            _setFoo(action.foo);
            FooStore.emitChange();
            break;

        case ActionTypes.FOO_REHYDRATED:
            _rehydrate(
                action.fooId,
                action.field,
                action.value
            );
            FooStore.emitChange();
            break;
    }
});

module.exports = FooStore;

<сильные > компоненты

компоненты/BarList.react.js(компонент контроллера)

var React = require('react/addons');
var Immutable = require('immutable');

var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        barList: BarStore.getAll(),
    };
}

module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {
        var barItems = this.state.barList.toJS().map(function (bar) {
            // We could pass the entire Bar object here
            // but I tend to keep the component not tightly coupled
            // with store data, the BarItem can be seen as a standalone
            // component that only need specific data
            return <BarItem
                        key={bar.get('id')}
                        id={bar.get('id')}
                        name={bar.get('name')}
                        description={bar.get('description')}/>
        });

        if (barItems.length == 0) {
            return (
                <p>Loading...</p>
            )
        }

        return (
            <div>
                {barItems}
            </div>
        )

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

компоненты/BarListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    // I use propTypes to explicitly telling
    // what data this component need. This 
    // component is a standalone component
    // and we could have passed an entire
    // object such as {id: ..., name, ..., description, ...}
    // since we use all the datas (and when we use all the data it's
    // a better approach since we don't want to write dozens of propTypes)
    // but let do that for the example sake 
    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    }

    render: function() {

        return (
            <li>
                <p>{this.props.id}</p>
                <p>{this.props.name}</p>
                <p>{this.props.description}</p>
            </li>
        )

    }

});

компоненты/BarDetail.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

var BarActionCreators = require('../actions/BarActionCreators');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    },

    handleSubmit: function(event) {
        //Since we keep the Bar data up to date with user input
        //we can simply save the actual object in Store.
        //If the user goes back without saving, we could display a 
        //"Warning : item not saved" 
        BarActionCreators.save(this.props.id);
    },

    handleChange: function(event) {
        BarActionCreators.rehydrate(
            this.props.id,
            event.target.name, //the field we want to rehydrate
            event.target.value //the updated value
        );
    },

    render: function() {

        return (
            <form onSubmit={this.handleSumit}>
                <input
                    type="text"
                    name="name"
                    value={this.props.name}
                    onChange={this.handleChange}/>
                <textarea
                    name="description"
                    value={this.props.description}
                    onChange={this.handleChange}/>
                <input
                    type="submit"
                    defaultValue="Submit"/>
            </form>
        )

    },

});

components/FooList.react.js(компонент вида контроллера)

var React = require('react/addons');

var FooStore = require('../stores/FooStore');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        fooList: FooStore.getAll(),
    };
}


module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        FooStore.addChangeListener(this._onChange);
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        FooStore.removeChangeListener(this._onChange);
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {

        if (this.state.fooList.size == 0) {
            return <p>Loading...</p>
        }

        return this.state.fooList.toJS().map(function (foo) {
            <FooListItem 
                fooId={foo.get('id')}
                fooBar={foo.getBar()}
                fooBaz={foo.get('baz')}/>
        });

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

компоненты/FooListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')

var Bar = require('../stores/BarStore').Bar;

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        fooId: React.PropTypes.number.isRequired,
        fooBar: React.PropTypes.instanceOf(Bar).isRequired,
        fooBaz: React.PropTypes.string.isRequired
    }

    render: function() {

        //we could (should) use a component here but this answer is already too long...
        var bar = <p>Loading...</p>;

        if (bar.isReady()) {
            bar = (
                <div>
                    <p>{bar.get('name')}</p>
                    <p>{bar.get('description')}</p>
                </div>
            );
        }

        return (
            <div>
                <p>{this.props.fooId}</p>
                <p>{this.props.fooBaz}</p>
                {bar}
            </div>
        )

    },

});

Пропустите полный цикл для FooList:

Состояние 1:

  • Пользователь попадает на страницу/foos/перечисляет Foos через компонент FooList для просмотра контроллера
  • FooList вызовы компонентов контроллера-контроллера FooStore.getAll()
  • _foos карта пуста в FooStore, поэтому FooStore выполняет запрос через FooAPI.getAll()
  • Компонент контроллера FooList отображает себя как состояние загрузки с его state.fooList.size == 0.

Вот реальный вид нашего списка:

++++++++++++++++++++++++
+                      +
+     "loading..."     +
+                      +
++++++++++++++++++++++++
  • FooAPI.getAll() запрос разрешает и запускает действие FooActionCreators.receiveAllSuccess
  • FooStore получает это действие, обновляет его внутреннее состояние и испускает изменения.

Состояние 2:

  • FooList компонент-элемент управления контроллера получает событие изменения и обновляет его состояние, чтобы получить список из FooStore
  • this.state.fooList.size больше не == 0, поэтому список может реально отображаться (обратите внимание, что мы используем toJS() для явного получения необработанного javascript-объекта, так как React не обрабатывает корректное отображение на необработанном объекте).
  • Мы передаем необходимые реквизиты для компонента FooListItem.
  • Вызывая foo.getBar(), мы сообщим FooStore, что мы хотим вернуть запись Bar.
  • getBar() метод записи foo извлекает запись Bar через BarStore
  • BarStore не имеет этой записи Bar в кеше _bars, поэтому он запускает запрос через BarAPI для его получения.
  • То же самое происходит для всех foo в this.sate.fooList компонента FooList компонента контроллера
  • Теперь страница выглядит примерно так:
++++++++++++++++++++++++
+                      +
+  Foo1 "name1"        +
+  Foo1 "baz1"         +
+  Foo1 bar:           +
+     "loading..."     +
+                      +
+  Foo2 "name2"        +
+  Foo2 "baz2"         +
+  Foo2 bar:           +
+     "loading..."     +
+                      +
+  Foo3 "name3"        +
+  Foo3 "baz3"         +
+  Foo3 bar:           +
+     "loading..."     +
+                      +
++++++++++++++++++++++++

-Теперь скажем, что BarAPI.get(2) (запрошенный Foo2) разрешается до BarAPI.get(1) (запрос Foo1). Поскольку это асинхронно, это совершенно правдоподобно. - BarAPI запускает BAR_RECEIVED_SUCCESS' action via the BarActionCreators . - The BarStore` отвечает на это действие, обновляя его внутреннее хранилище и испуская изменения. Что теперь самое интересное...

Состояние 3:

  • Компонент контроллера FooList отвечает на изменение BarStore, обновляя его состояние.
  • Метод render называется
  • Теперь вызов foo.getBar() возвращает реальную запись Bar из BarStore. Поскольку эта запись Bar была эффективно восстановлена, ImmutablePureRenderMixin будет сравнивать старые реквизиты с текущими реквизитами и определить, что объекты Bar изменены! Bingo, мы могли бы повторно отобразить компонент FooListItem (лучший подход здесь заключался бы в создании отдельного компонента FooListBarDetail, позволяющего повторно отображать только этот компонент, здесь мы также перерисовываем детали Foo, которые не изменились, но для ради простоты позвольте просто сделать это).
  • Теперь страница выглядит следующим образом:
++++++++++++++++++++++++
+                      +
+  Foo1 "name1"        +
+  Foo1 "baz1"         +
+  Foo1 bar:           +
+     "loading..."     +
+                      +
+  Foo2 "name2"        +
+  Foo2 "baz2"         +
+  Foo2 bar:           +
+    "bar name"        +
+    "bar description" +
+                      +
+  Foo3 "name3"        +
+  Foo3 "baz3"         +
+  Foo3 bar:           +
+     "loading..."     +
+                      +
++++++++++++++++++++++++

Если вы хотите, чтобы я добавил более подробную информацию из не детализированной части (например, создатели действий, константы, маршрутизация и т.д., использование компонента BarListDetail с формой, POST и т.д.), просто скажите мне в комментариях:).

Ответ 2

Несколько отличий в моей реализации:

  • Мне нравятся магазины с использованием мухи. То есть, если to, все операции "getOrRetrieveOrCreate"

  • Мне пришлось отказаться от обещания тяжелого развития в пользу события/состояния. Асинхронная связь должна по-прежнему использовать promises, что есть, вещи в действиях используют их, иначе связь происходит с использованием Мероприятия. Если представление всегда отображает текущее состояние, тогда вам нужно как "isLoading", чтобы сделать счетчик. Или вам нужно событие после этого обновите состояние в представлении. Я думаю, что ответ действие с обещанием может быть анти-шаблоном (не совсем уверенным).

  • При изменении URL-адреса срабатывает соответствующее действие. GET должен работать и быть idempotent, поэтому изменение URL-адреса обычно не должно приводить к сбою. Однако это может привести к перенаправлению. У меня есть "authRequired" декоратор для некоторых действий. Если вы не прошли аутентификацию, мы перенаправляет вас на страницу входа с целевым URL, указанным в качестве путь перенаправления.

  • Для проверки мы думаем начать с действия, запуская "xyzModel: willSaveData", прежде чем мы начнем; затем запускается либо "xyzModel: didSaveData", либо "xyzModel: failedSaveData". Магазин, слушая эти события, будет указывать на "сохранение" взглядов, которые ухаживают. Он также может указывать на "hasValidationError" на виды, которые заботятся. Если вы хотите отклонить ошибку. Вы можете запустить действие из представления, которое указывает, что ошибка "wasReceived", которая удаляет флаг "hasValidationError" или, возможно, может сделать что-то еще, например, очистить все ошибки проверки. Валидации интересны из-за разных стилей валидации. В идеале вы можете создать приложение, которое будет принимать большинство входных данных из-за ограничений, налагаемых вашими входными элементами. С другой стороны, серверы могут не согласиться с этими вариантами:/.