React / Flux和xhr /路由/缓存

问题描述 投票:12回答:2

这更像是“你的意见是什么/我在思考这个问题时是否正确?”题。

在理解Flux的同时尽量严格,我试图找出XHR调用的位置,处理websockets /外部刺激,路由发生等等。

从我阅读的文章,访谈和浏览facebook示例中,我可以通过几种方式处理这些问题。严格遵循通量,Action创建者可以执行所有XHR调用,并且可能在请求完成之前和之后触发PENDING/SUCCESS/FAILURE操作。 另一个来自facebook的Ian Obermiller,所有READ(GET)请求都由Stores直接处理(没有Action创建者/调度员的参与),WRITE(POST)请求由Action Creators处理整个action>dispatcher>store流程。

我们提出/想要坚持的一些理解/结论:

  1. 理想情况下,进出系统的任何事情都只能通过Actions进行。
  2. 离开/进入系统的异步调用将具有PENDING/PROGRESS(think file uploads)/SUCCESS/FAILURE操作。
  3. 整个App中的单一调度程序。
  4. Action>Dispatcher>Store调用是严格同步的,以坚持调度无法在内部启动另一个调度以避免链接事件/操作。
  5. 存储在视图中持久存在(考虑到它是单页面应用程序,您希望能够重用数据)

我们得出一些结论的一些问题,但我并不完全满意:

  1. 如果采用Stores do Reads和Actions to Writes的方法,您如何处理多个Stores可能能够使用来自单个XHR调用的数据的情况? 示例:TeamStore向/api/teams/{id}发出的API调用,返回类似于: { 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: ... }] } } 理想情况下,我还想使用此API中返回的信息更新MemberStore。我们维护每个实体的版本号,这些版本在记录更新时更新,这是我们在内部使用的拒绝对陈旧数据的调用等。使用这个,我可以有一个内部逻辑,如果我作为副作用一些其他API调用,我知道我的数据是陈旧的,我触发了对该记录的刷新。 看起来,解决方案是你需要商店来触发一个动作(这将有效地更新其他相关商店)。这会使Store> View> Action to Store> Action短路,我不确定它是不是一个好主意。我们已经有一件事与商店进行他们自己的XHR调用不同步了。像这样的让步最终会开始蔓延到整个系统。 或知道其他商店并能够与他们沟通的商店。但这打破了商店没有Setters规则。 解决上述问题的一个简单方法就是坚持将动作作为外部传入/传出刺激的唯一场所。这简化了多个商店更新的逻辑。 但是现在,你在哪里以及如何处理缓存?我们得出结论,缓存将发生在API Utils / DAO级别。 (如果你看一下通量图)。 但这引入了其他问题。为了更好地理解/解释我的意思,例如: /api/teams返回所有球队的列表,我显示所有球队的列表。 在点击团队链接时,我会查看其详细信息视图,如果商店中尚未存在数据,则需要/api/teams/{id}的数据。 如果Actions处理所有的XHR,那么View会像TeamActions.get([id])那样执行TeamDAO.get([id])。为了能够立即返回此调用(因为我们已将其缓存),DAO必须进行缓存,但也要保持集合/项之间的关系。按照设计,这种逻辑已经存在于商店中。 这里有问题: 你是否在DAO和商店中复制了这个逻辑? 你是否让DAO了解商店,他们可以向商店询问他们是否已经拥有一些数据并且只返回302说,你很高兴你有最新的数据。 您如何处理涉及XHR API的验证?简单的重复团队名称。 视图直接命中DAO并执行像TeamDAO.validateName([name])这样的东西,它会返回一个承诺,或者你是否创建了一个Action?如果您创建一个Action,Store会通过哪个有效/无效流回View,考虑到它主要是瞬态数据? 你如何处理路由?我查看了react-router,我不确定是否喜欢它。我不一定认为迫切需要JSX提供路由映射/配置的方法。此外,显然,它使用了自己的RouteDispatcher,它依赖于单个调度程序规则。 我更喜欢的解决方案来自一些博客帖子/ SO答案,其中路由映射存储在RouteStore中。 RouteStore还维护CURRENT_VIEW。 reactConConiner组件已在RouteStore中注册,并在更改时将其子视图替换为CURRENT_VIEW。当前视图通知AppContainer它们完全加载并且AppContainer触发RouteActions.pending / success / failure,可能带有一些上下文,以通知其他组件达到稳定状态,显示/隐藏繁忙/加载指示。 我无法设计干净的东西是你设计类似于Gmail的路由,你会怎么做? Gmail的一些观察结果我很喜欢: 在页面准备好加载之前,URL不会更改。它在“加载”时保留在当前URL上,并在加载完成后移动到新URL。这样做...... 如果失败,您根本不会丢失当前页面。因此,如果您正在撰写,并且“发送”失败,则不会丢失您的邮件(即您不会丢失当前的稳定视图/状态)。 (他们不会这样做,因为自动保存是可以的,但你明白了)你可以选择将邮件复制/粘贴到某个地方以便安全保存,直到你可以再次发送。 一些参考: https://github.com/gaearon/flux-react-router-example http://ianobermiller.com/blog/2014/09/15/react-and-flux-interview/ https://github.com/facebook/flux
javascript reactjs reactjs-flux
2个回答
5
投票

这是我使用facebook Flux和Immutable.js的实现,我认为根据一些经验法则回答你的许多问题:

STORES

  • 商店负责通过Immutable.Record维护数据状态,并通过Immutable.OrderedMap引用Record实例的全局ids维护缓存。
  • 商店直接调用WebAPIUtils进行读操作,并触发actions进行写操作。
  • RecordAFooRecordB之间的关系是通过RecordA params从foo_id实例中解决的,并通过FooStore.get(this.foo_id)等调用获得
  • 商店只暴露getters方法,如get(id)getAll()等。

APIUTILS

  • 我使用SuperAgent进行ajax调用。每个请求都包含在Promise
  • 我使用由url + params的哈希索引的读取请求Promise的映射
  • Promise被解析或拒绝时,我通过ActionCreators触发操作,例如fooReceived或fooError。
  • fooError操作当然应包含服务器返回的验证错误的有效负载。

组件

  • 控制器视图组件侦听存储中的更改。
  • 我所有的组件,除了控制器视图组件,都是'纯'的,所以我使用ImmutableRenderMixin只重新渲染它真正需要的东西(意思是如果你打印Perf.printWasted时间,它应该是非常低,几毫秒。
  • 由于Relay and GraphQL还没有开源,我强制通过props保持我的组件propsType尽可能明确。
  • 父组件应该只传递必要的道具。如果我的父组件包含一个对象,如var fooRecord = { foo:1, bar: 2, baz: 3};(为了简化本例,我不在这里使用Immutable.Record)并且我的子组件需要显示fooRecord.foofooRecord.bar,我不传递整个foo对象但只传递fooRecordFoofooRecordBar作为我的子组件的道具,因为另一个组件可以编辑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's 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;

组件

components / 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's do that for the example's 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:

  • 用户通过FooListcontroller-view组件点击页面/ foos /列出Foos
  • FooListcontroller-view组件调用FooStore.getAll()
  • _foos地图在FooStore是空的,所以FooStore通过FooAPI.getAll()执行请求
  • FooList控制器视图组件自其state.fooList.size == 0以来将自身呈现为加载状态。

这是我们列表的实际外观:

++++++++++++++++++++++++
+                      +
+     "loading..."     +
+                      +
++++++++++++++++++++++++
  • FooAPI.getAll()请求解决并触发FooActionCreators.receiveAllSuccess行动
  • FooStore接收此操作,更新其内部状态,并发出更改。

州2:

  • FooList controller-view组件接收更改事件并更新其状态以从FooStore获取列表
  • this.state.fooList.size不再是== 0所以列表实际上可以渲染自己(请注意,我们使用toJS()显式获取原始javascript对象,因为React无法正确映射非原始对象)。
  • 我们将所需的道具传递给FooListItem组件。
  • 通过调用foo.getBar(),我们告诉FooStore我们想要Bar记录。
  • getBar()记录的Foo方法通过Bar检索BarStore记录
  • BarStoreBar缓存中没有这个_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 theBarActionCreators. - TheBarStore`通过更新其内部存储并发出更改来响应此操作。这就是现在有趣的部分......

州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等),请在评论中告诉我:)。


0
投票

我的实施中有一些差异:

  1. 我喜欢采用飞重模式的商店。也就是说,除非被强制执行,否则所有操作都是“getOrRetrieveOrCreate”
  2. 我不得不放弃重大发展,支持事件/国家。异步通信仍应使用promises,即操作中的事物使用它们,否则使用事件进行通信。如果视图总是呈现当前状态,那么您需要像“isLoading”这样的状态来呈现微调器。或者您需要触发事件然后更新视图上的状态。我认为应对具有承诺的行动可能是反模式(不完全确定)。
  3. URL更改将触发相应的操作。 GET应该工作并且是幂等的,因此URL更改通常不会导致失败。但是它可能会导致重定向。我有一个“authRequired”装饰器用于某些动作。如果您未经过身份验证,那么我们会将您重定向到登录页面,并将目标URL列为重定向路径。
  4. 为了验证,我们正在考虑从一个动作开始,在我们开始之前触发“xyzModel:willSaveData”;然后触发“xyzModel:didSaveData”或“xyzModel:failedSaveData”事件。听这些事件的商店将指示“保存”到关注的视图。它还可能向关注的视图指示“hasValidationError”。如果要解除错误。您可以从视图中触发一个操作,该操作指示错误“wasReceived”,这会删除“hasValidationError”标志,或者可选择执行其他操作,例如清除所有验证错误。由于验证的风格不同,验证很有意思。理想情况下,由于输入元素的限制,您可以创建一个可接受大多数输入的应用程序。然后,服务器可能不同意这些选择:/。
© www.soinside.com 2019 - 2024. All rights reserved.