React/Flux 和 xhr/路由/缓存

2023-11-26

这更像是“你的意见是什么/我的想法正确吗?”问题。

在理解 Flux 的同时尽可能严格,我试图弄清楚 XHR 调用在哪里进行、websockets/外部刺激处理、路由发生等。

根据我阅读的文章、采访和 Facebook 示例,有几种处理这些事情的方法。严格遵循 Flux,Action 创建者是执行所有 XHR 调用的人,并且有可能PENDING/SUCCESS/FAILURE请求完成之前和之后触发的操作。
另一个来自 facebook 的 Ian Obermiller 的说法是,所有 READ(GET) 请求均由 Stores 直接处理(无需 Action 创建者/调度程序的参与),而 WRITE(POST) 请求由 Action Creator 处理,贯穿整个过程。action>dispatcher>store flow.

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

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

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

  1. 如果您采用存储执行读取、操作执行写入的方法,那么如何处理多个存储可能能够使用来自单个 XHR 调用的数据的情况?
    示例:TeamStore 发出的 API 调用/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: ...  
                }]  
            }  
        }  
    

    理想情况下,我还想使用此 API 返回的信息更新 MemberStore。我们为每个实体维护一个版本号,该版本号在记录更新时更新,这就是我们在内部使用的拒绝对过时数据的调用等。使用这个,我可以有一个内部逻辑,如果我作为一些其他 API 调用,我知道我的数据已过时,我会触发该记录的刷新。
    解决方案似乎是您需要存储来触发操作(这将有效地更新其他依赖存储)。这会短路 Store>View>Action 到 Store>Action,我不确定这是否是一个好主意。我们已经有一件事与商店进行自己的 XHR 调用不同步。诸如此类的让步最终将开始渗透到整个系统中。
    或者了解其他商店并能够与他们进行通信的商店。但这打破了商店没有设置者的规则。

    1. 解决上述问题的一个简单方法是,坚持将“动作”作为外部传入/传出刺激发生的唯一场所。这简化了多个商店更新的逻辑。
      但现在,您在哪里以及如何处理缓存?我们得出的结论是,缓存将发生在 API Utils/DAO 级别。 (如果你看一下通量图)。
      但这会带来其他问题。为了更好地理解/解释我举的例子的意思:

      • /api/teams返回所有团队的列表,我用它来显示所有团队的列表。
      • 单击团队的链接后,我会进入其详细信息视图,该视图需要来自/api/teams/{id}如果商店中尚未存在。
        如果 Actions 处理所有 XHR,View 会执行类似的操作TeamActions.get([id])这确实TeamDAO.get([id])。为了能够立即返回这个调用(因为我们已经缓存了它),DAO 必须进行缓存,但还要维护集合/项目之间的关系。按照设计,这种逻辑已经存在于商店中。
        问题来了:

      • 您是否在 DAO 和 Store 中重复此逻辑?

      • 你是否让 DAO 知道 Stores,他们可以询问 Store 是否已经有一些数据,然后返回 302 说,你很好,你有最新的数据。
    2. 您如何处理涉及 XHR API 的验证?一些简单的事情,比如重复的团队名称。
      视图直接命中 DAO 并执行类似的操作TeamDAO.validateName([name])它返回一个承诺还是您创建一个操作?如果您创建一个操作,考虑到其主要是瞬态数据,有效/无效的存储将通过该操作流回视图?

    3. 你如何处理路由?我浏览了react-router,我不确定我喜欢它。我认为根本不需要强制使用react-ish JSX 方式来提供路由映射/配置。而且,显然,它使用了自己的 RouteDispatcher,它遵循单一调度程序规则。
      我更喜欢的解决方案来自一些博客文章/SO 答案,其中您将路由映射存储在 RouteStore 中。
      RouteStore还维护CURRENT_VIEW。 React AppContainer 组件向 RouteStore 注册,并在更改时用 CURRENT_VIEW 替换其子视图。当前视图在完全加载时通知 AppContainer,并且 AppContainer 可能会在某些上下文中触发 RouteActions.pending/success/failure,以通知其他组件达到稳定状态,显示/隐藏繁忙/加载指示。

    我无法清晰地设计的是,如果你要设计类似于 Gmail 的路由,你会怎么做?我对 Gmail 的一些忠实粉丝的观察:

    • 在页面准备好加载之前,URL 不会更改。它在“加载”时停留在当前 URL,并在加载完成后移动到新 URL。这使得...
    • 失败时,您根本不会丢失当前页面。因此,如果您正在撰写,并且“发送”失败,您不会丢失邮件(即,您不会丢失当前的稳定视图/状态)。 (他们不这样做,因为自动保存是 le 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


这是我使用 facebook Flux 和 Immutable.js 的实现,我认为根据一些经验法则,我认为它可以满足您的许多担忧:

STORES

  • 存储负责通过以下方式维护数据状态不可变记录并通过全局维护缓存不可变的有序映射参考Record实例通过ids.
  • 门店直接致电WebAPIUtils for read操作和触发actions for write运营。
  • 之间的关系RecordA and FooRecordB解决自RecordA实例通过一个foo_idparams 并通过调用检索,例如FooStore.get(this.foo_id)
  • 商店只暴露getters方法如get(id), getAll(), etc.

APIUTILS

  • I use 超级特工用于 ajax 调用。每个请求都包含在Promise
  • 我使用的地图read要求Promise通过 url + params 的哈希索引
  • 我通过 ActionCreators 触发操作,例如 fooReceived 或 fooErrorPromise已解决或被拒绝。
  • fooError操作当然应该包含服务器返回的带有验证错误的有效负载。

成分

  • 控制器视图组件监听存储中的更改。
  • 除了控制器视图组件之外,我的所有组件都是“纯”的,所以我使用不可变渲染混合仅重新渲染真正需要的内容(这意味着如果您打印Perf.printWasted时间,应该非常低,几毫秒。
  • Since 中继和 GraphQL尚未开源,我强制保留我的组件props尽可能明确地通过propsType.
  • 父组件应该只传递必要的道具。如果我的父组件包含一个对象,例如var fooRecord = { foo:1, bar: 2, baz: 3};(我没有使用Immutable.Record为了简单起见,这里)和我的子组件需要显示fooRecord.foo and fooRecord.bar, I do not通过整个foo对象但仅fooRecordFoo and fooRecordBar作为我的子组件的道具,因为其他组件可以编辑foo.baz值,使子组件重新渲染,而该组件根本不需要这个值!

ROUTING- 我只是用反应路由器

执行

这是一个基本示例:

api

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

};

stores

商店/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;

成分

组件/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>
        )

    },

});

组件/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:

State 1:

  • 用户通过以下方式点击页面 /foos/ 列出 FoosFooList控制器视图组件
  • FooList控制器视图组件调用FooStore.getAll()
  • _foos地图为空FooStore so FooStore通过执行请求FooAPI.getAll()
  • The FooList控制器视图组件将自身呈现为加载状态,因为它state.fooList.size == 0.

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

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

State 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 in this.sate.fooList of 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酒吧动作创作者. - TheBarStore` 通过更新其内部存储并发出更改来响应此操作。这就是现在有趣的部分......

State 3:

  • The FooList控制器视图组件响应BarStore通过更新其状态来改变。
  • The render方法被称为
  • The foo.getBar()现在调用检索一个真实的Bar记录自BarStore。从此Bar记录已被有效检索,ImmutablePureRenderMixin将比较旧的 props 和当前的 props 并确定Bar对象发生了变化!宾果,我们可以重新渲染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 等的组件)只需在评论中告诉我:)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React/Flux 和 xhr/路由/缓存 的相关文章

随机推荐