12

これはどちらかというと「あなたの意見はどうですか/私の考えは正しいですか?」です。質問。

Flux を理解しながら、できるだけ厳密にしようとして、XHR 呼び出しが行われる場所、Websockets/外部刺激が処理される場所、ルーティングが行われる場所などを把握しようとしました。

記事やインタビューを読んだり、Facebook の例を調べたりすると、これらのことを処理する方法がいくつかあります。PENDING/SUCCESS/FAILUREフラックスに厳密に従うと、アクション作成者は、リクエストが完了する前と完了後にアクションが発生する可能性があるすべての XHR 呼び出しを実行する人です。
もう 1 つは、Facebook の Ian Obermiller によるもので、すべての READ(GET) リクエストはストアによって直接処理され (アクション クリエーター/ディスパッチャーの関与なし)、WRITE(POST) リクエストはaction>dispatcher>storeフロー全体を通過するアクション クリエーターによって処理されます。

私たちが導き出した/固執したいいくつかの理解/結論:

  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 呼び出しを行う Store と同期していないものが既に 1 つあります。このような譲歩は、最終的にはシステム全体に忍び込み始めるでしょう。
    または、他の店舗を認識しており、それらと通信できる店舗。しかし、これは Stores have no Setters ルールに違反しています。

    1. 上記の問題に対する簡単な解決策は、外部の着信/発信刺激が発生する唯一の場所であるアクションに固執することです。これにより、複数のストアが更新されるロジックが簡素化されます。
      しかし今、キャッシングをどこでどのように処理するのでしょうか? キャッシングは API Utils/DAO レベルで行われるという結論に達しました。(フラックスダイアグラムを見ると)。
      しかし、これは他の問題をもたらします。例で私が意味することをよりよく理解/説明するには:

      • /api/teamsすべてのチームのリストを表示するすべてのチームのリストを返します。
      • チームのリンクをクリックすると/api/teams/{id}、ストアにまだ存在しない場合にデータを必要とする詳細ビューに移動します。
        Actions がすべての XHR を処理する場合、View は which が行うようなことTeamActions.get([id])を行いTeamDAO.get([id])ます。この呼び出しをすぐに返すことができるようにするために (キャッシュされているため)、DAO はキャッシュを行う必要がありますが、コレクション/アイテム間の関係も維持する必要があります。このロジックは、設計上、既にストアに存在しています。
        ここに質問があります:

      • このロジックを DAO とストアで複製しますか?

      • DAO にストアを認識させて、既にデータがあるかどうかをストアに問い合わせて、302 というメッセージを返すことができますか。
    2. XHR API を含む検証をどのように処理しますか? 重複するチーム名のような単純なもの。
      ビューはDAOに直接ヒットしTeamDAO.validateName([name])、プロミスを返すようなことをしますか、それともアクションを作成しますか? ほとんどの一時的なデータを考慮して、有効/無効がビューに戻るアクションを作成する場合は、どのストアを経由しますか?

    3. ルーティングをどのように処理しますか? 私はreact-routerを調べましたが、それが好きかどうかわかりません。ルートマッピング/構成を提供する反応的なJSXの方法を強制する必要があるとは必ずしも思いません。また、明らかに、単一のディスパッチャ ルールを実行する独自の RouteDispatcher を採用しています。
      私が好む解決策は、ルート マッピングが RouteStore に格納されているいくつかのブログ投稿/SO 回答から得られました。
      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

4

2 に答える 2

5

いくつかの経験則に基づいて、facebook Flux と Immutable.js を使用した私の実装は、あなたの懸念の多くに対応していると思います。

店舗

  • ストアは、 Immutable.Recordを介してデータ状態を維持し、グローバルなImmutable.OrderedMapRecordを介してインスタンスを参照してキャッシュを維持する責任がありますids
  • 読み取り操作の呼び出しWebAPIUtils書き込み操作のトリガーを直接格納します。actions
  • RecordAとの間の関係は、params を介してインスタンスFooRecordBから解決され、次のような呼び出しを介して取得されますRecordAfoo_idFooStore.get(this.foo_id)
  • ストアは 、 などのメソッドのみを公開しますgettersget(id)getAll()

APIユーティリティ

  • 私は ajax 呼び出しにSuperAgentを使用しています。各リクエストはPromise
  • URL + params のハッシュによってインデックス付けされた読み取りリクエストのマップを使用しますPromise
  • Promise解決または拒否されたときに、fooReceived や fooError などの ActionCreators を介してアクションをトリガーします。
  • fooErrorアクションには、サーバーから返された検証エラーを含むペイロードが含まれている必要があります。

コンポーネント

  • コントローラ ビュー コンポーネントは、ストアの変更をリッスンします。
  • コントローラー ビュー コンポーネント以外のすべてのコンポーネントは「純粋」であるため、ImmutableRenderMixinを使用して、本当に必要なものだけを再レンダリングします (つまり、Perf.printWasted時間を出力する場合は、非常に低く、数ミリ秒になるはずです)。
  • RelayとGraphQLはまだオープン ソース化されていないため、.propspropsType
  • 親コンポーネントは、必要な小道具のみを渡す必要があります。親コンポーネントが (この例を簡単にするためにここでvar fooRecord = { foo:1, bar: 2, baz: 3};は使用していません)などのオブジェクトを保持し、子コンポーネントがandを表示する必要がある場合、オブジェクト全体を渡すのではなく、子コンポーネントに小道具として渡すだけです。他のコンポーネントが値を編集する可能性があるため、このコンポーネントはこの値をまったく必要としませんが、子コンポーネントを再レンダリングします!Immutable.RecordfooRecord.foofooRecord.barfoofooRecordFoofooRecordBarfoo.baz

ROUTING - 私は単にReactRouterを使用しています

実装

基本的な例を次に示します。

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

};

店舗

ストア/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>
        )

    }

});

components/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();
    }

});

components/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>
        )

    },

});

for のループ全体を見てみましょうFooList:

状態 1:

  • FooListユーザーは、コントローラー ビュー コンポーネントを介して Foos を一覧表示するページ /foos/ にアクセスします。
  • FooListコントローラー ビュー コンポーネントの呼び出しFooStore.getAll()
  • _foosマップは空なFooStoreのでFooStore、経由でリクエストを実行しますFooAPI.getAll()
  • FooListコントローラー ビュー コンポーネントは、state.fooList.size == 0.

リストの実際の外観は次のとおりです。

++++++++++++++++++++++++
+                      +
+     "loading..."     +
+                      +
++++++++++++++++++++++++
  • FooAPI.getAll()FooActionCreators.receiveAllSuccessリクエストが解決され、アクションがトリガーされます
  • FooStoreこのアクションを受け取り、内部状態を更新し、変更を発行します。

状態 2:

  • FooListcontroller-view コンポーネントは変更イベントを受け取り、その状態を更新してからリストを取得します。FooStore
  • this.state.fooList.size== 0リストが実際にそれ自体をレンダリングすることはできなくなりました (未加工のオブジェクトへのマッピングをまだ正しく処理していないtoJS()ため、未加工の JavaScript オブジェクトを明示的に取得するために使用していることに注意してください)。React
  • FooListItem必要な小道具をコンポーネントに渡しています。
  • 電話をかけることで、レコードを取り戻したいことを にfoo.getBar()伝えます。FooStoreBar
  • getBar()FooレコードのメソッドはBarBarStore
  • BarStoreBarはキャッシュにこのレコードを持っていないため、それを取得するために_barsリクエストをトリガーします。BarAPI
  • コントローラービューコンポーネントのすべてFooで同じことが起こりますthis.sate.fooListFooList
  • ページは次のようになります。
++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 バー: +
+ 「読み込んでいます...」 +
+ +
++++++++++++++++++++++++

BarAPI.get(2)- ここで、(Foo2 によって要求された) が ( Foo1 によって要求された) 前に解決されるとしましょうBarAPI.get(1)。非同期なので、完全にもっともらしいです。- BarActionCreators BarStore` のBarAPIトリガーは、内部ストアを更新することでこのアクションに応答し、変更を発行します。それが今の楽しい部分です...BAR_RECEIVED_SUCCESS' action via the. - The

状態 3:

  • FooListコントローラー ビュー コンポーネントは、その状態を更新することで変更に対応しますBarStore
  • renderメソッドが呼び出されます
  • この呼び出しは、から実際のレコードをfoo.getBar()取得するようになりました。このレコードは効果的に取得されているため、 は古い props を現在の props と比較し、オブジェクトが変更されたことを判断します! ビンゴ、コンポーネントを再レンダリングできます (ここでのより良いアプローチは、別の FooListBarDetail コンポーネントを作成して、このコンポーネントのみを再レンダリングできるようにすることです。ここでは、変更されていない Foo の詳細も再レンダリングしますが、簡単にそうしましょう)。BarBarStoreBarImmutablePureRenderMixinBarFooListItem
  • ページは次のようになります。
++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 バー: +
+ 「バー名」 +
+ 「バーの説明」 +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 バー: +
+ 「読み込んでいます...」 +
+ +
++++++++++++++++++++++++

詳細でない部分 (アクションクリエーター、定数、ルーティングなど、フォーム付きコンポーネントの使用、POST など) から詳細を追加したい場合はBarListDetail、コメントで教えてください:)。

于 2015-08-26T10:27:09.223 に答える
0

私の実装におけるいくつかの違い:

  1. フライ級のパターンを採用している店が好きです。つまり、強制されない限り、すべての操作は「getOrRetrieveOrCreate」です。

  2. イベント/状態を優先して、重い開発を約束するのをやめなければなりませんでした。非同期通信は引き続き promise を使用する必要があります。つまり、アクション内のものはそれらを使用します。それ以外の場合、通信はイベントを使用して行われます。ビューが常に現在の状態をレンダリングする場合、スピナーをレンダリングするには「isLoading」のような状態が必要です。または、発生するイベントが必要で、ビューの状態を更新します。約束を伴うアクションからの応答は、アンチパターンである可能性があると思います(完全にはわかりません)。

  3. URL を変更すると、適切なアクションが発生します。GET は機能し、冪等であるため、通常、URL の変更によってエラーが発生することはありません。ただし、リダイレクトが発生する場合があります。一部のアクションには「authRequired」デコレーターがあります。認証されていない場合は、ターゲット URL がリダイレクト パスとしてリストされたログイン ページにリダイレクトされます。

  4. 検証のために、開始する前に「xyzModel:willSaveData」を起動するアクションから開始することを考えています。次に、「xyzModel:didSaveData」または「xyzModel:failedSaveData」イベントのいずれかを発生させます。これらのイベントをリッスンするストアは、関心のあるビューに「保存」を示します。また、気になるビューに「hasValidationError」を示す場合もあります。エラーを消したい場合。エラー「wasReceived」を示すビューからアクションを起動して、「hasValidationError」フラグを削除するか、オプションですべての検証エラーをクリアするなどの何かを実行できます。さまざまなスタイルの検証があるため、検証は興味深いものです。理想的には、入力要素によって課される制限により、ほとんどすべての入力を受け入れるアプリを作成できます。また、

于 2015-05-20T16:36:05.497 に答える