使用Flux构建一个编辑表单,它实际向服务器发送数据:动作、存储、视图

Using Flux to build an edit form, who actually POSTs data to the server: actions, stores, views?

本文关键字:服务器 视图 存储 动作 数据 构建 Flux 表单 编辑 一个 使用      更新时间:2023-09-26

我找到了很多关于如何为React和Flux获取数据的资源、博客和意见,但很少有关于向服务器写入数据的。有人能提供一个"首选"方法的基本原理和一些示例代码吗?在构建一个简单的编辑表单的背景下,将更改持久化到RESTful web API?

具体来说,哪个Flux盒子应该调用$.post,在哪里调用ActionCreator.receiveItem()(它做什么),以及在商店的注册方法中有什么?

相关链接:

  • 当使用React + Flux时,动作或存储应该负责转换数据吗?
  • 通量存储或动作(或两者)应该接触外部服务吗?
  • 在Flux应用中ajax请求应该在哪里?

简短回答

  • 你的表单组件应该从Store中获取它的状态,在用户输入时创建"update"动作,在表单提交时调用"save"动作。
  • 操作创建者将执行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
};

行动/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
        });
    },
};

商店/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记录将在本地保持最新状态,当提交表单时,我们尝试将其保存在服务器上。就这样吧:)

  1. 组件/视图用于显示数据和事件
  2. action与事件(onClick, onChange…)绑定,用于在承诺解决或失败后与资源通信和调度事件。确保至少有两个事件,一个成功,一个ajax失败。
  3. store被订阅到调度程序正在调度的事件。一旦接收到数据,存储就会更新存储的值并发出变化。
  4. 组件/视图被订阅到store,一旦发生更改就重新渲染。

flux store,或者action(或者两者)应该接触外部服务吗?方法对我来说是很自然的。

也有一些情况下,当你需要触发一些操作作为其他操作被触发的结果,这是你可以从一个相关的存储触发操作,结果存储和视图被更新。