使用 Redux Middleware 发送 actions 到服务端
最后我们要做的是把用户数据提交到服务端,这种操作一般发生在用户投票,或选择跳转下一轮投票时发生。
让我们讨论一下投票操作,下面列出了投票的逻辑:
- 当用户进行投票,
VOTE
action 将产生并分派到客户端的 Redux Store 中; VOTE
actions 将触发客户端 reducer 进行hasVoted
状态设置;- 服务端监控客户端通过 socket.io 投递的
action
,它将接收到的 actions 分派到服务端的 Redux Store; VOTE
action 将触发服务端的 reducer,其会创建 vote 数据并更新对应的票数。
这样来说,我们似乎已经都搞定了。唯一缺少的就是让客户端发送 VOTE
action 给服务端。这相当于两端的
Redux Store 相互分派 action,这就是我们接下来要做的。
那么该怎么做呢?Redux 并没有内建这种功能。所以我们需要设计一下何时何地来做这个工作:从客户端发送
action 到服务端。
Redux 提供了一个通用的方法来封装 action: Middleware 。
Redux 中间件是一个函数,每当 action 将要被指派,并在对应的 reducer 执行之前会被调用。它常用来做像日志收集,
异常处理,修整 action,缓存结果,控制何时以何种方式来让 store 接收 actions 等工作。这正是我们可以利用的。
注意,一定要分清 Redux 中间件和 Redux 监听器的差别:中间件被用于 action 将要指派给 store 阶段,它可以修改 action 对
store 将带来的影响。而监听器则是在 action 被指派后,它不能改变 action 的行为。
我们需要创建一个“远程 action 中间件”,该中间件可以让我们的 action 不仅仅能指派给本地的 store,也可以通过
socket.io 连接派送给远程的 store。
让我们创建这个中间件,It is a function that takes a Redux store, and returns another function that takes a “next” callback. That function returns a third function that takes a Redux action. The innermost function is where the middleware implementation will actually go
(译者注:这句套绕口,请看官自行参悟):
//src/remote_action_middleware.js
export default store => next => action => {
}
上面这个写法看着可能有点渗人,下面调整一下让大家好理解:
export default function(store) {
return function(next) {
return function(action) {
}
}
}
这种嵌套接受单一参数函数的写法成为 currying 。
这种写法主要用来简化中间件的实现:如果我们使用一个一次性接受所有参数的函数( function(store, next, action) { }
),
那么我们就不得不保证我们的中间件具体实现每次都要包含所有这些参数。
上面的 next
参数作用是在中间件中一旦完成了 action 的处理,就可以调用它来退出当前逻辑:
//src/remote_action_middleware.js
export default store => next => action => {
return next(action);
}
如果中间件没有调用 next
,则该 action 将丢弃,不再传到 reducer 或 store 中。
让我们写一个简单的日志中间件:
//src/remote_action_middleware.js
export default store => next => action => {
console.log('in middleware', action);
return next(action);
}
我们将上面这个中间件注册到我们的 Redux Store 中,我们将会抓取到所有 action 的日志。中间件可以通过 Redux
提供的 applyMiddleware
函数绑定到我们的 store 中:
//src/components/index.jsx
import React from 'react';
import Router, {Route, DefaultRoute} from 'react-router';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import io from 'socket.io-client';
import reducer from './reducer';
import {setState} from './action_creators';
import remoteActionMiddleware from './remote_action_middleware';
import App from './components/App';
import {VotingContainer} from './components/Voting';
import {ResultsContainer} from './components/Results';
const createStoreWithMiddleware = applyMiddleware(
remoteActionMiddleware
)(createStore);
const store = createStoreWithMiddleware(reducer);
const socket = io(`${location.protocol}//${location.hostname}:8090`);
socket.on('state', state =>
store.dispatch(setState(state))
);
const routes = <Route handler={App}>
<Route path="/results" handler={ResultsContainer} />
<DefaultRoute handler={VotingContainer} />
</Route>;
Router.run(routes, (Root) => {
React.render(
<Provider store={store}>
{() => <Root />}
</Provider>,
document.getElementById('app')
);
});
如果你重启应用,你将会看到我们设置的中间件会抓到应用触发的 action 日志。
那我们应该怎么利用中间件机制来完成从客户端通过 socket.io 连接发送 action 给服务端呢?在此之前我们肯定需要先
有一个连接供中间件使用,不幸的是我们已经有了,就在 index.jsx
中,我们只需要中间件可以拿到它即可。
使用 currying 风格来实现这个中间件很简单:
//src/remote_action_middleware.js
export default socket => store => next => action => {
console.log('in middleware', action);
return next(action);
}
这样我们就可以在 index.jsx
中传入需要的连接了:
//src/index.jsx
const socket = io(`${location.protocol}//${location.hostname}:8090`);
socket.on('state', state =>
store.dispatch(setState(state))
);
const createStoreWithMiddleware = applyMiddleware(
remoteActionMiddleware(socket)
)(createStore);
const store = createStoreWithMiddleware(reducer);
注意跟之前的代码比,我们需要调整一下顺序,让 socket 连接先于 store 被创建。
一切就绪了,现在就可以使用我们的中间件发送 action
了:
//src/remote_action_middleware.js
export default socket => store => next => action => {
socket.emit('action', action);
return next(action);
}
打完收工。现在如果你再点击投票按钮,你就会看到所有连接到服务端的客户端的票数都会被更新!
还有个很严重的问题我们要处理:现在每当我们收到服务端发来的 SET_STATE
action 后,这个 action 都将会直接回传给
服务端,这样我们就造成了一个死循环,这是非常反人类的。
我们的中间件不应该不加处理的转发所有的 action 给服务端。个别 action,例如 SET_STATE
,应该只在客户端做
处理。我们在 action 中添加一个标识位用于识别哪些应该转发给服务端:
//src/remote_action_middleware.js
export default socket => store => next => action => {
if (action.meta && action.meta.remote) {
socket.emit('action', action);
}
return next(action);
}
我们同样应该修改相关的 action creators:
//src/action_creators.js
export function setState(state) {
return {
type: 'SET_STATE',
state
};
}
export function vote(entry) {
return {
meta: {remote: true},
type: 'VOTE',
entry
};
}
让我们重新审视一下我们都干了什么:
- 用户点击投票按钮,
VOTE
action 被分派; - 远程 action 中间件通过 socket.io 连接转发该 action 给服务端;
- 客户端 Redux Store 处理这个 action,记录本地
hasVoted
属性; - 当 action 到达服务端,服务端的 Redux Store 将处理该 action,更新所有投票及其票数;
- 设置在服务端 Redux Store 上的监听器将改变后的状态数据发送给所有在线的客户端;
- 每个客户端将触发
SET_STATE
action 的分派; - 每个客户端将根据这个 action 更新自己的状态,这样就保持了与服务端的同步。
为了完成我们的应用,我们需要实现下一步按钮的逻辑。和投票类似,我们需要将数据发送到服务端:
//src/action_creator.js
export function setState(state) {
return {
type: 'SET_STATE',
state
};
}
export function vote(entry) {
return {
meta: {remote: true},
type: 'VOTE',
entry
};
}
export function next() {
return {
meta: {remote: true},
type: 'NEXT'
};
}
ResultsContainer
组件将会自动关联 action creators 中的 next 作为 props:
//src/components/Results.jsx
import React from 'react/addons';
import {connect} from 'react-redux';
import Winner from './Winner';
import * as actionCreators from '../action_creators';
export const Results = React.createClass({
mixins: [React.addons.PureRenderMixin],
getPair: function() {
return this.props.pair || [];
},
getVotes: function(entry) {
if (this.props.tally && this.props.tally.has(entry)) {
return this.props.tally.get(entry);
}
return 0;
},
render: function() {
return this.props.winner ?
<Winner ref="winner" winner={this.props.winner} /> :
<div className="results">
<div className="tally">
{this.getPair().map(entry =>
<div key={entry} className="entry">
<h1>{entry}</h1>
<div className="voteCount">
{this.getVotes(entry)}
</div>
</div>
)}
</div>
<div className="management">
<button ref="next"
className="next"
onClick={this.props.next()}>
Next
</button>
</div>
</div>;
}
});
function mapStateToProps(state) {
return {
pair: state.getIn(['vote', 'pair']),
tally: state.getIn(['vote', 'tally']),
winner: state.get('winner')
}
}
export const ResultsContainer = connect(
mapStateToProps,
actionCreators
)(Results);
彻底完工了!我们实现了一个功能完备的应用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论