使用 Flux 构建一个编辑表单,谁实际将数据 POST 到服务器:操作、存储、视图?

Posted

技术标签:

【中文标题】使用 Flux 构建一个编辑表单,谁实际将数据 POST 到服务器:操作、存储、视图?【英文标题】:Using Flux to build an edit form, who actually POSTs data to the server: actions, stores, views? 【发布时间】:2015-10-12 23:17:03 【问题描述】:

我找到了很多关于如何为 React 和 Flux 获取数据的资源、博客和意见,但在将数据写入服务器方面却少之又少。在构建一个简单的编辑表单以保持对 RESTful Web API 的更改的上下文中,有人可以为“首选”方法提供一个基本原理和一些示例代码吗?

具体来说,哪个 Flux 框应该调用$.postActionCreator.receiveItem() 在哪里调用(以及它的作用),以及 store 的注册方法中有什么?

相关链接:

Should the action or store be responsible for transforming data when using React + Flux? Should flux stores, or actions (or both) touch external services? Where should ajax request be made in Flux app?

【问题讨论】:

【参考方案1】:

简答

您的表单组件应从 Store 中检索其状态,对用户输入创建“更新”操作,并在表单提交时调用“保存”操作。 动作创建者将执行 POST 请求,并根据请求结果触发“save_success”动作或“save_error”动作。

通过实施示例的详细回答

apiUtils/BarAPI.js

var Request = require('./Request'); //it's a custom module that handles request via superagent wrapped in Promise
var BarActionCreators = require('../actions/BarActionCreators');

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

module.exports = 

    post: function(barData) 
        BarActionCreators.savePending();
        Request.post(_endpoint, barData).then (function(res) 
            if (res.badRequest)  //i.e response returns code 400 due to validation errors for example
                BarActionCreators.saveInvalidated(res.body);
            
            BarActionCreators.savedSuccess(res.body);
        ).catch( function(err)  //server errors
            BarActionCreators.savedError(err);
        );
    ,

    //other helpers out of topic for this answer

;

actions/BarActionCreators.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../apiUtils/VoucherAPI');

module.exports = 

    save: function(bar) 
        BarAPI.save(bar.toJSON());
    ,

    saveSucceed: function(response) 
        AppDispatcher.dispatch(
            type: ActionTypes.BAR_SAVE_SUCCEED,
            response: response
        );
    ,

    saveInvalidated: function(barData) 
        AppDispatcher.dispatch(
            type: ActionTypes.BAR_SAVE_INVALIDATED,
            response: response
        )
    ,

    saveFailed: function(err) 
        AppDispatcher.dispatch(
            type: ActionTypes.BAR_SAVE_FAILED,
            err: err
        );
    ,

    savePending: function(bar) 
        AppDispatcher.dispatch(
            type: ActionTypes.BAR_SAVE_PENDING,
            bar: bar
        );
    

    rehydrate: function(barId, field, value) 
        AppDispatcher.dispatch(
            type: ActionTypes.BAR_REHYDRATED,
            barId: barId,
            field: field,
            value: value
        );
    ,

;

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,
    'save_status': "not saved" //better to use constants here
) 

    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); //not defined is this example
            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));
;

BarStore.dispatchToken = AppDispatcher.register(function(action) 
    switch (action.type)
       

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

        case ActionTypes.BAR_SAVE_PENDING:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() 
                return "saving";
            );
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_SUCCEED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() 
                return "saved";
            );
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_INVALIDATED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() 
                return "invalid";
            );
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_FAILED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() 
                return "failed";
            );
            BarStore.emitChange();
            break;

        //many other actions outside the scope of this answer

        default:
            break;
    
);

module.exports = BarStore;

组件/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> //we should wrapped the following p's in a Link to the editing page of the Bar record with id = this.props.id. Let's assume that's what we did and when we click on this <li> we are redirected to edit page which renders a BarDetail component
                <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>
        )

    ,

);

通过这个基本示例,每当用户通过 BarDetail 组件中的表单编辑 Bar 项目时,底层的 Bar 记录将在本地保持最新,当提交表单时,我们会尝试将其保存在服务器。就是这样:)

【讨论】:

【参考方案2】:
    组件/视图用于显示数据和触发事件 动作与事件(onClick、onChange...)相关联,并用于与资源通信并在承诺解决或失败后调度事件。确保您至少有两个事件,一个代表成功,一个代表 ajax 失败。 商店订阅了调度程序正在调度的事件。收到数据后,商店会更新存储的值并发出更改。 组件/视图订阅到商店,并在发生更改后重新呈现。

Should flux stores, or actions (or both) touch external services? 方法对我来说是很自然的。

在某些情况下,由于触发了某些其他操作,您需要触发某些操作,您可以在此处触发相关商店的操作,从而更新商店和视图。

【讨论】:

以上是关于使用 Flux 构建一个编辑表单,谁实际将数据 POST 到服务器:操作、存储、视图?的主要内容,如果未能解决你的问题,请参考以下文章

Flux:中间错误应该存储在哪里?

Flux - 谁应该更改正在收集的模型中的数据?

如何将动态表单保存到数据库/编辑表单回来

Codeigniter:如何构建使用表单验证和重新填充的编辑表单?

Flux Actions 到 Redux Actions 的转换

用于登录或基本上大多数表单处理的 Flux 架构