使用 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 框应该调用$.post
,ActionCreator.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 到服务器:操作、存储、视图?的主要内容,如果未能解决你的问题,请参考以下文章
Codeigniter:如何构建使用表单验证和重新填充的编辑表单?