如何在 REACT 和 FLUX 中创建 API 调用
How to create API calls in REACT and FLUX
我是 React 和 Flux 的新手,我很难弄清楚如何从服务器加载数据。我可以毫无问题地从本地文件加载相同的数据。
所以首先我有这个控制器视图 (controller-view.js) 将初始状态传递给视图 (view.js)
控制器-view.js
var viewBill = React.createClass({
getInitialState: function(){
return {
bill: BillStore.getAllBill()
};
},
render: function(){
return (
<div>
<SubscriptionDetails subscription={this.state.bill.statement} />
</div>
);
}
});
module.exports = viewBill;
view.js
var subscriptionsList = React.createClass({
propTypes: {
subscription: React.PropTypes.array.isRequired
},
render: function(){
return (
<div >
<h1>Statement</h1>
From: {this.props.subscription.period.from} - To {this.props.subscription.period.to} <br />
Due: {this.props.subscription.due}<br />
Issued:{this.props.subscription.generated}
</div>
);
}
});
module.exports = subscriptionsList;
我有一个操作文件,可以为我的应用加载 INITAL 数据。所以这是 不是 作为用户操作调用的数据,而是从控制器视图 getInitialState 调用的数据
InitialActions.js
var InitialiseActions = {
initApp: function(){
Dispatcher.dispatch({
actionType: ActionTypes.INITIALISE,
initialData: {
bill: BillApi.getBillLocal() // I switch to getBillServer for date from server
}
});
}
};
module.exports = InitialiseActions;
然后我的数据 API 看起来像这样
api.js
var BillApi = {
getBillLocal: function() {
return billed;
},
getBillServer: function() {
return $.getJSON('https://theurl.com/stuff.json').then(function(data) {
return data;
});
}
};
module.exports = BillApi;
这是商店
store.js
var _bill = [];
var BillStore = assign({}, EventEmitter.prototype, {
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
getAllBill: function() {
return _bill;
}
});
Dispatcher.register(function(action){
switch(action.actionType){
case ActionTypes.INITIALISE:
_bill = action.initialData.bill;
BillStore.emitChange();
break;
default:
// do nothing
}
});
module.exports = BillStore;
因此,正如我之前提到的,当我在操作中使用 BillApi.getBillLocal() 在本地加载数据时,一切正常。但是当我更改为 BillApi.getBillServer() 时,我在控制台中收到以下错误...
Warning: Failed propType: Required prop `subscription` was not specified in `subscriptionsList`. Check the render method of `viewBill`.
Uncaught TypeError: Cannot read property 'period' of undefined
我还在BillApi.getBillServer()中添加了一个console.log(数据),我可以看到数据是从服务器返回的。但它显示 AFTER 我在控制台收到警告,我认为这可能是问题所在。任何人都可以提供一些建议或帮助我解决它吗?抱歉这么长 post.
更新
我对 api.js 文件进行了一些更改(在此处检查更改和 DOM 错误 plnkr.co/edit/HoXszori3HUAwUOHzPLG),因为有人建议问题是由于我的处理方式造成的承诺。但它似乎仍然与您在 DOM 错误中看到的问题相同。
这是一个异步问题。使用 $.getJSON().then()
是不够的。因为它 returns 是一个 promise 对象,所以你必须通过像 api.getBill().then(function(data) { /*do stuff with data*/ });
这样的事情来处理调用时的 promise
我用下面的代码做了一个CodePen example:
function searchSpotify(query) {
return $.getJSON('http://ws.spotify.com/search/1/track.json?q=' + query)
.then(function(data) {
return data.tracks;
});
}
searchSpotify('donald trump')
.then(function(tracks) {
tracks.forEach(function(track) {
console.log(track.name);
});
});
另一种方法是在使用数据之前检查订阅属性是否存在。
尝试将您的代码修改为如下所示:
render: function(){
var subscriptionPeriod = '';
var subscriptionDue = ['',''];
var subscriptionGenerated = '';
if(this.props.subscription !== undefined){
subscriptionPeriod = this.props.subscription.period;
subscriptionDue = [this.props.subscription.due.to,this.props.subscription.due.from];
subscriptionGenerated = this.props.subscription.generated;
}
return (
<div >
<h1>Statement</h1>
From: {subscriptionPeriod[0]} - To {subscriptionPeriod[1]} <br />
Due: {subscriptionDue}<br />
Issued:{subscriptionGenerated}
</div>
);
}
在 return 之前的渲染函数中尝试添加以下内容:
如果(this.props.subscription!=未定义){
// 在这里做点什么
}
由于您的数据改变了顶级组件的状态,一旦它具有定义了订阅属性的数据,它将重新触发渲染。
如果我没理解错你可以试试这样的东西
// InitialActions.js
var InitialiseActions = {
initApp: function(){
BillApi.getBill(function(result){
// result from getJson is available here
Dispatcher.dispatch({
actionType: ActionTypes.INITIALISE,
initialData: {
bill: result
}
});
});
}
};
module.exports = InitialiseActions;
//api.js
var BillApi = {
getBillLocal: function() {
console.log(biller);
return biller;
},
getBill: function(callback) {
$.getJSON('https://theurl.com/stuff.json', callback);
}
};
$.getJSON 不会 return 来自 http 请求的值。它使其可用于回调。
此处详细解释了其背后的逻辑:How to return the response from an asynchronous call?
从您的代码看来,预期的流程类似于:
- 某些组件触发初始化操作,
- 初始化动作调用API
- 等待服务器的结果(我认为这里是问题所在:您的组件渲染在服务器返回结果之前开始),
- 然后将结果传递给商店,
- 发出变化
- 触发重新渲染。
在典型的通量设置中,我建议将其结构略有不同:
- 某些组件调用 API(但尚未向调度程序触发操作)
- API 执行
getJSON
并等待服务器结果
- 仅在接收到结果后,API用接收到的数据触发INITIALIZE动作
- store 响应操作,并根据结果更新自身
- 然后发出变化
- 触发重新渲染
我不太熟悉 jquery、promises 和链接,但我认为这将大致转化为代码中的以下更改:
- controller-view 需要一个存储更改侦听器:添加一个
componentDidMount()
函数,将事件侦听器添加到 flux 存储更改。
- 在控制器视图中,事件侦听器触发一个
setState()
函数,该函数从商店中获取最新的 _bill。
- 将
dispatcher.dispatch()
从 actions.js 移动到 api.js(替换 return data
);
这样,您的组件最初应该呈现一些 'loading' 消息,并在来自服务器的数据进入时立即更新。
我会将我的 Actions、Stores 和 Views(React 组件)分开。
首先,我会像这样实现我的 Action:
import keyMirror from 'keymirror';
import ApiService from '../../lib/api';
import Dispatcher from '../dispatcher/dispatcher';
import config from '../env/config';
export let ActionTypes = keyMirror({
GetAllBillPending: null,
GetAllBillSuccess: null,
GetAllBillError: null
}, 'Bill:');
export default {
fetchBills () {
Dispatcher.dispatch(ActionTypes.GetAllBillPending);
YOUR_API_CALL
.then(response => {
//fetchs your API/service call to fetch all Bills
Dispatcher.dispatch(ActionTypes.GetAllBillSuccess, response);
})
.catch(err => {
//catches error if you want to
Dispatcher.dispatch(ActionTypes.GetAllBillError, err);
});
}
};
下一个是我的商店,因此我可以跟踪在 api 调用期间可能突然发生的所有更改:
class BillStore extends YourCustomStore {
constructor() {
super();
this.bindActions(
ActionTypes.GetAllBillPending, this.onGetAllBillPending,
ActionTypes.GetAllBillSuccess, this.onGetAllBillSuccess,
ActionTypes.GetAllBillError , this.onGetAllBillError
);
}
getInitialState () {
return {
bills : []
status: Status.Pending
};
}
onGetAllBillPending () {
this.setState({
bills : []
status: Status.Pending
});
}
onGetAllBillSuccess (payload) {
this.setState({
bills : payload
status: Status.Ok
});
}
onGetAllBillError (error) {
this.setState({
bills : [],
status: Status.Errors
});
}
}
export default new BillStore();
最后,你的组件:
import React from 'react';
import BillStore from '../stores/bill';
import BillActions from '../actions/bill';
export default React.createClass({
statics: {
storeListeners: {
'onBillStoreChange': BillStore
},
},
getInitialState () {
return BillStore.getInitialState();
},
onBillStoreChange () {
const state = BillStore.getState();
this.setState({
bills : state.bills,
pending: state.status === Status.Pending
});
},
componentDidMount () {
BillActions.fetchBills();
},
render () {
if (this.state.pending) {
return (
<div>
{/* your loader, or pending structure */}
</div>
);
}
return (
<div>
{/* your Bills */}
</div>
);
}
});
假设您实际上是从 API 中获取数据,但为时已晚并且首先抛出错误,请尝试以下操作:
在您的控制器 -view.js 中,添加以下内容:
componentWillMount: function () {
BillStore.addChangeListener(this._handleChangedBills);
},
componentWillUnmount: function () {
BillStore.removeChangeListener(this._handleChangedBills);
},
_handleChangedBills = () => {
this.setState({bill: BillStore.getAllBill()});
}
然后在您的 getInitialState 函数中,提供一个空对象,该对象具有您的代码所期望的结构(具体而言,其中包含一个 'statement' 对象)。像这样:
getInitialState: function(){
return {
bill: { statement: [] }
};
},
发生的事情是,当您获得初始状态时,它没有正确地从商店中获取,return 未定义的对象也是如此。当您随后请求 this.state.bill.statement 时,bill 已初始化但未定义,因此它找不到任何称为 statement 的东西,因此您需要添加它的原因。在组件有更多时间之后(这是一个异步问题就像其他海报所说的那样),它应该可以从商店中正确获取。这就是为什么我们等待商店为我们发出更改,然后我们从商店中获取数据的原因。
我是 React 和 Flux 的新手,我很难弄清楚如何从服务器加载数据。我可以毫无问题地从本地文件加载相同的数据。
所以首先我有这个控制器视图 (controller-view.js) 将初始状态传递给视图 (view.js)
控制器-view.js
var viewBill = React.createClass({
getInitialState: function(){
return {
bill: BillStore.getAllBill()
};
},
render: function(){
return (
<div>
<SubscriptionDetails subscription={this.state.bill.statement} />
</div>
);
}
});
module.exports = viewBill;
view.js
var subscriptionsList = React.createClass({
propTypes: {
subscription: React.PropTypes.array.isRequired
},
render: function(){
return (
<div >
<h1>Statement</h1>
From: {this.props.subscription.period.from} - To {this.props.subscription.period.to} <br />
Due: {this.props.subscription.due}<br />
Issued:{this.props.subscription.generated}
</div>
);
}
});
module.exports = subscriptionsList;
我有一个操作文件,可以为我的应用加载 INITAL 数据。所以这是 不是 作为用户操作调用的数据,而是从控制器视图 getInitialState 调用的数据
InitialActions.js
var InitialiseActions = {
initApp: function(){
Dispatcher.dispatch({
actionType: ActionTypes.INITIALISE,
initialData: {
bill: BillApi.getBillLocal() // I switch to getBillServer for date from server
}
});
}
};
module.exports = InitialiseActions;
然后我的数据 API 看起来像这样
api.js
var BillApi = {
getBillLocal: function() {
return billed;
},
getBillServer: function() {
return $.getJSON('https://theurl.com/stuff.json').then(function(data) {
return data;
});
}
};
module.exports = BillApi;
这是商店 store.js
var _bill = [];
var BillStore = assign({}, EventEmitter.prototype, {
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
getAllBill: function() {
return _bill;
}
});
Dispatcher.register(function(action){
switch(action.actionType){
case ActionTypes.INITIALISE:
_bill = action.initialData.bill;
BillStore.emitChange();
break;
default:
// do nothing
}
});
module.exports = BillStore;
因此,正如我之前提到的,当我在操作中使用 BillApi.getBillLocal() 在本地加载数据时,一切正常。但是当我更改为 BillApi.getBillServer() 时,我在控制台中收到以下错误...
Warning: Failed propType: Required prop `subscription` was not specified in `subscriptionsList`. Check the render method of `viewBill`.
Uncaught TypeError: Cannot read property 'period' of undefined
我还在BillApi.getBillServer()中添加了一个console.log(数据),我可以看到数据是从服务器返回的。但它显示 AFTER 我在控制台收到警告,我认为这可能是问题所在。任何人都可以提供一些建议或帮助我解决它吗?抱歉这么长 post.
更新
我对 api.js 文件进行了一些更改(在此处检查更改和 DOM 错误 plnkr.co/edit/HoXszori3HUAwUOHzPLG),因为有人建议问题是由于我的处理方式造成的承诺。但它似乎仍然与您在 DOM 错误中看到的问题相同。
这是一个异步问题。使用 $.getJSON().then()
是不够的。因为它 returns 是一个 promise 对象,所以你必须通过像 api.getBill().then(function(data) { /*do stuff with data*/ });
我用下面的代码做了一个CodePen example:
function searchSpotify(query) {
return $.getJSON('http://ws.spotify.com/search/1/track.json?q=' + query)
.then(function(data) {
return data.tracks;
});
}
searchSpotify('donald trump')
.then(function(tracks) {
tracks.forEach(function(track) {
console.log(track.name);
});
});
另一种方法是在使用数据之前检查订阅属性是否存在。
尝试将您的代码修改为如下所示:
render: function(){
var subscriptionPeriod = '';
var subscriptionDue = ['',''];
var subscriptionGenerated = '';
if(this.props.subscription !== undefined){
subscriptionPeriod = this.props.subscription.period;
subscriptionDue = [this.props.subscription.due.to,this.props.subscription.due.from];
subscriptionGenerated = this.props.subscription.generated;
}
return (
<div >
<h1>Statement</h1>
From: {subscriptionPeriod[0]} - To {subscriptionPeriod[1]} <br />
Due: {subscriptionDue}<br />
Issued:{subscriptionGenerated}
</div>
);
}
在 return 之前的渲染函数中尝试添加以下内容: 如果(this.props.subscription!=未定义){ // 在这里做点什么 }
由于您的数据改变了顶级组件的状态,一旦它具有定义了订阅属性的数据,它将重新触发渲染。
如果我没理解错你可以试试这样的东西
// InitialActions.js
var InitialiseActions = {
initApp: function(){
BillApi.getBill(function(result){
// result from getJson is available here
Dispatcher.dispatch({
actionType: ActionTypes.INITIALISE,
initialData: {
bill: result
}
});
});
}
};
module.exports = InitialiseActions;
//api.js
var BillApi = {
getBillLocal: function() {
console.log(biller);
return biller;
},
getBill: function(callback) {
$.getJSON('https://theurl.com/stuff.json', callback);
}
};
$.getJSON 不会 return 来自 http 请求的值。它使其可用于回调。 此处详细解释了其背后的逻辑:How to return the response from an asynchronous call?
从您的代码看来,预期的流程类似于:
- 某些组件触发初始化操作,
- 初始化动作调用API
- 等待服务器的结果(我认为这里是问题所在:您的组件渲染在服务器返回结果之前开始),
- 然后将结果传递给商店,
- 发出变化
- 触发重新渲染。
在典型的通量设置中,我建议将其结构略有不同:
- 某些组件调用 API(但尚未向调度程序触发操作)
- API 执行
getJSON
并等待服务器结果 - 仅在接收到结果后,API用接收到的数据触发INITIALIZE动作
- store 响应操作,并根据结果更新自身
- 然后发出变化
- 触发重新渲染
我不太熟悉 jquery、promises 和链接,但我认为这将大致转化为代码中的以下更改:
- controller-view 需要一个存储更改侦听器:添加一个
componentDidMount()
函数,将事件侦听器添加到 flux 存储更改。 - 在控制器视图中,事件侦听器触发一个
setState()
函数,该函数从商店中获取最新的 _bill。 - 将
dispatcher.dispatch()
从 actions.js 移动到 api.js(替换return data
);
这样,您的组件最初应该呈现一些 'loading' 消息,并在来自服务器的数据进入时立即更新。
我会将我的 Actions、Stores 和 Views(React 组件)分开。
首先,我会像这样实现我的 Action:
import keyMirror from 'keymirror';
import ApiService from '../../lib/api';
import Dispatcher from '../dispatcher/dispatcher';
import config from '../env/config';
export let ActionTypes = keyMirror({
GetAllBillPending: null,
GetAllBillSuccess: null,
GetAllBillError: null
}, 'Bill:');
export default {
fetchBills () {
Dispatcher.dispatch(ActionTypes.GetAllBillPending);
YOUR_API_CALL
.then(response => {
//fetchs your API/service call to fetch all Bills
Dispatcher.dispatch(ActionTypes.GetAllBillSuccess, response);
})
.catch(err => {
//catches error if you want to
Dispatcher.dispatch(ActionTypes.GetAllBillError, err);
});
}
};
下一个是我的商店,因此我可以跟踪在 api 调用期间可能突然发生的所有更改:
class BillStore extends YourCustomStore {
constructor() {
super();
this.bindActions(
ActionTypes.GetAllBillPending, this.onGetAllBillPending,
ActionTypes.GetAllBillSuccess, this.onGetAllBillSuccess,
ActionTypes.GetAllBillError , this.onGetAllBillError
);
}
getInitialState () {
return {
bills : []
status: Status.Pending
};
}
onGetAllBillPending () {
this.setState({
bills : []
status: Status.Pending
});
}
onGetAllBillSuccess (payload) {
this.setState({
bills : payload
status: Status.Ok
});
}
onGetAllBillError (error) {
this.setState({
bills : [],
status: Status.Errors
});
}
}
export default new BillStore();
最后,你的组件:
import React from 'react';
import BillStore from '../stores/bill';
import BillActions from '../actions/bill';
export default React.createClass({
statics: {
storeListeners: {
'onBillStoreChange': BillStore
},
},
getInitialState () {
return BillStore.getInitialState();
},
onBillStoreChange () {
const state = BillStore.getState();
this.setState({
bills : state.bills,
pending: state.status === Status.Pending
});
},
componentDidMount () {
BillActions.fetchBills();
},
render () {
if (this.state.pending) {
return (
<div>
{/* your loader, or pending structure */}
</div>
);
}
return (
<div>
{/* your Bills */}
</div>
);
}
});
假设您实际上是从 API 中获取数据,但为时已晚并且首先抛出错误,请尝试以下操作: 在您的控制器 -view.js 中,添加以下内容:
componentWillMount: function () {
BillStore.addChangeListener(this._handleChangedBills);
},
componentWillUnmount: function () {
BillStore.removeChangeListener(this._handleChangedBills);
},
_handleChangedBills = () => {
this.setState({bill: BillStore.getAllBill()});
}
然后在您的 getInitialState 函数中,提供一个空对象,该对象具有您的代码所期望的结构(具体而言,其中包含一个 'statement' 对象)。像这样:
getInitialState: function(){
return {
bill: { statement: [] }
};
},
发生的事情是,当您获得初始状态时,它没有正确地从商店中获取,return 未定义的对象也是如此。当您随后请求 this.state.bill.statement 时,bill 已初始化但未定义,因此它找不到任何称为 statement 的东西,因此您需要添加它的原因。在组件有更多时间之后(这是一个异步问题就像其他海报所说的那样),它应该可以从商店中正确获取。这就是为什么我们等待商店为我们发出更改,然后我们从商店中获取数据的原因。