12 OCTOBER 2016
这是翻译版本,原文请见
简单的Redux Saga 模板
在这个文章中,我们将完成完整的React/Redux/Redux Saga app,并且来看看为什么要这样做.
我已经创建了一个app的模板作为本文的起点,我们没有必要关注一些开发的细节,因为这些细节不是本系列文章的重点(我假设你已经了解React,Redux以及与此相关的开发工具.)但是我仍然会简单强调一些内容,以便于你对项目依赖包和配置有一些基础的了解.你可能是个高手,或者是个不折不扣的菜鸟(是菜鸟也没有关系)如果你不关心这些基础内容,直接跳到那副图片,看看后面的内容.
第一步,克隆repo,并且安装依赖包:
//原文的repo不能运行了,下面的repo是验证过的
git clone https://github.com/granmoe/redux-saga-clock-tutorial.git
cd redux-saga-clock-tutorial
npm i
好了做完上面的工作,使用你喜欢的编辑器打开项目,让我们先看看里面有些什么内容.
在我们的package.json文件中每个元素都是非常标准的,但是要注意,如果要对付不支持ES2015标砖的浏览器,需要引入babel-polyfill包.这个包必须在redux-saga之前引入(译者:redux-saga使用了ES2015的技术,所以要先获得支持才可以).
你也可以注意到,在package.json中有ESLint依赖包,因为我发现这个依赖包是开发中的无价之宝.
下面是我们的babel配置,在.babelrc文件中:
{
"presets": ["es2015", "react", "stage-2"]
}
我已经决定使用es2015,react和stage-2.
我还想讲讲.eslintrc文件,但是我实在是不想让你看到想睡觉.
webpack和index.html文件没讲,但是这里估计没有人会对这两个文件感兴趣.
开始进入正题吧
app的入口文件是main.jsx:
import 'babel-polyfill' // generator support
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from 'app.jsx'
import initStore from 'store'
const store = initStore()
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>,
这里我们导入一些依赖项(包括babel-polyfill),导入根组件,redux store的配置,实例化store,然后在经过Provider class包装的”app”div的中使用ReactDOM渲染出根组件,这样以来,在app中所有组件树种的react组件都可以很容易的接入到我们的store实例.
查看store.js,代码中我们使用saga middleware来配置我们的store:
import createSagaMiddleware from 'redux-saga'
export default function () {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
return store
}
首先我们使用createSagaMiddleware
方法来创建middleware实例.接下来,把根reducer和middleware传递到createStore
,这就创建了一个redux store.然后把我们app的root saga传递进saga middleware.这一步一定要在redux store实例化以后再执行.rootSaga
是顶级generator,这个generator负责代理其他所有的generators的工作(马上会看到.)
上面都是些什么见鬼的代码,你个王八蛋(译者:原意直接翻译啊)
其实我们已经有了有趣的东西,我们的代码基本上依赖两个文件.”app.jsx”是一个react组件,可以根据app的state和基于DOM事件系统的actions来返回渲染的html标记.”duck.js”包含单纯对象actions和reducer,这两个函数一起工作描述出怎么修改state.其中也包含了所有的控制流代码,控制流代码描述了整个app的处理过程.如果你很熟悉标准的鸭子模型,我仅仅修改了鸭子模型,让他很容易包含saga代码.让我们使用鸭子模块来工作吧.
我们将会创建一个可以控制的时钟.开始来想想app需要的最精简的sate结构.在任何时间我们要询问app的状态是”现在几点了?”所有我们需要存储的就是单个的数字.现在让我们来看看怎么改变这个状态.好的,我们我们将制作一个时钟,用户可以向前,向后,暂停和重置.这里的构想意味着我们表征时间的代码逻辑需要增,减,什么也不做,重置到0.什么事也不做意味着不需要sate发生改变,所以我们留下增加,减少,重置.我们要显示时间的毫秒数,因此app的state就定为”毫秒数”.
正如上面所讲的,我们在redux代码中使用鸭子模型,如果你不喜欢这样做,可以分割成三个文件.
让我们看看duck.js中的第一部分,saga actions
.
import { takeLatest } from 'redux-saga'
const initialState = {
milliseconds: 0
}
export default function reducer (currentState = initialState, action) {
switch (action.type) {
case 'reset-clock':
return {
...currentState,
milliseconds: 0
}
case 'increment-milliseconds':
return {
...currentState,
milliseconds: currentState.milliseconds + 100
}
case 'decrement-milliseconds':
if (!currentState.milliseconds) { return currentState }
return {
...currentState,
milliseconds: currentState.milliseconds - 100
}
default:
return currentState
}
}
export const resetClock = () => ({ type: 'reset-clock' })
export const incrementMilliseconds = () => ({ type: 'increment-milliseconds' })
export const decrementMilliseconds = () => ({ type: 'decrement-milliseconds' })
上面这段代码很简单.首先由我们需求字段的起始state,接着有一个reducer,reducer实际上操作actions,它基于action type对state做出合适的修饰,之后创建新的state。最后我们export(模块模式)一些可以在其他地方调用的单纯action对象
.(马上我们会在saga中导入action对象之一).示例代码总是这么这么的整洁.
现在我们需要实现一下app的流程.在处理过程中,什么状态需要输入?这个问题的另一个问法是:app在某个特定的时间应该做什么工作?我们的时钟可以向前,向后,暂停.为了在这几个过程中相互转变,我们需要三个action,开始时钟,拨回时间,暂停时钟.
从代码//saga actions
开始,看看duck模块的剩余部分.我们已经创建了三个actions,我们的root saga在收到某个action的时候,会启动一个傻瓜处理流程.现在在代码里傻瓜处理流程只是打印一下action的名字.后续我们会开始根据action type处理具体的增,减,休眠流程.这里是duck.js的saga代码.
// saga actions
export const startClock = () => ({ type: 'start-clock' })
export const pauseClock = () => ({ type: 'pause-clock' })
export const rewindClock = () => ({ type: 'rewind-clock' })
// saga
export function* rootSaga () {
yield takeLatest(['start-clock', 'pause-clock', 'rewind-clock'], handleClockAction)
}
function* handleClockAction ({ type }) {
console.log('Pushed this action to handleClockAction: ', type)
}
actions(严格上讲,根据术语来说应该是叫“action creators”,但是无所谓,只要你理解具体的意义就可以)应该看起来和其他的redux actions类似.但是这些action在我们的reducer中不能得到处理.如果保持仅仅在saga代码附近保留这些actions,这里的acions仅仅触发saga.做到这一点,会避免action和根据这些action做出的state修改的代码混杂在一起.显而易见,saga action仍然通脱connect
函数绑定到store实例,并且输入到组件里.
现在解释一下这个文件里奇怪的saga.你还记得rootSaga
被传递到saga中间件,对吗?坦率讲,你可能也不知道,但是这也没关系.每次我们发出一个action,action会被推送到经过sagaMiddleware.run(generator)
包装的generator.这就意味着,每个generator都有机会响应action,在我们的实例中,rootSaga
遇到匹配的action type的时候才会做出响应.我们正在使用从Redux Saga获取的takeLatest
助手函数完成这个工作.takeLatest
接收任何与action type数组匹配的action,然后接着传递他,启动一个handleClockAcion
流程,传递进action.takeLatest
意思是直接收最新的action,如果现在还有正在运行的handleClockAction
的话,在新的action开始之前,当前的这个处理流程需要先退出.handleClockAction
,本质上是在后台启动,允许rootSaga
保持运行状态,即使handleClockAction
仍在运行,也可以接受下一个匹配的action.
注意我们使用的yield
关键词,回想一下,yield
在generator中发出和接收值.在任何时间,我们yield
一个Redux Saga助手或者effect的时候,我们就正在和Saga middleware进行通讯.在我们的上面的实例中,Redux Saga等待匹配发送到saga的action.后面我们还会更进一步深入讨论.
我希望你至少对这个流程有一点感觉.我认为可能在测试过程中(译者:这里的意思是实际运行代码的过程,并不是代码的测试过程)你对这个流程更清楚一点.所以让我们看看React组件中怎么和用户进行交互的过程.
在组件这一点看,“app.jsx”是非常简单的react组件.让我们看个仔细.
import React from 'react'
import { connect } from 'react-redux'
import { incrementMilliseconds, decrementMilliseconds, resetClock, startClock, pauseClock, rewindClock } from 'duck'
class Clock extends React.Component {
render () {
const {
milliseconds,
incrementMilliseconds,
decrementMilliseconds,
resetClock,
startClock,
pauseClock,
rewindClock
} = this.props
return (
<div>
<svg onClick={ incrementMilliseconds } onDoubleClick={ resetClock } onMouseLeave={ decrementMilliseconds }
className="clock" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="500">
<circle cx="50" cy="50" r={ 30 } stroke={ 'rgba(1,1,1,1)' } fill="orange" />
</svg>
<p>{ milliseconds }</p>
<p>
<button type="button" onClick={ startClock }>Start Clock</button>
<button type="button" onClick={ pauseClock }>Pause Clock</button>
<button type="button" onClick={ rewindClock }>Rewind Clock</button>
</p>
</div>
)
}
}
export default connect(state => ({
milliseconds: state.milliseconds
}), ({
incrementMilliseconds,
decrementMilliseconds,
resetClock,
startClock,
pauseClock,
rewindClock
}))(Clock)
通过使用connect高内聚组件,我们可以从store的state获取一个字段,并作为props传递进入组件.我们也通过一个对象传递四个action creators.Redux把这个对象绑定到store实例中,确保我们在组件中调用这几个action的时候,他们可以正确的被dispatch.
在我们的渲染中,我们返回一个<div>
,这个元素中有一个SVG(后续中将会比较关键).SVG有一些事件操作句柄,这些操作句柄将会dispatch state修饰actions.接下来,会有一个<p>
元素依据app的state来显示当前时间.最后我们有几个<button>
s 连接到saga的actions.
上面的代码都就位以后,我们就试着运行一下app,验证一下基础构架和actions的工作情况.
到底能工作吗?
回到你的终端,运行npm start
.现在输入localhost:8080,在浏览器中打开devtools,检查一下js 终端.当你点击buttons的时候,会看到saga actions的日志输出.现在试着在SVG上点击,鼠标一定,双击action.你可以看到毫秒文本的更新.
真好啊,我们创建了一个Redux Saga app的模板结构,了解了怎么使用takeLatest
.还可以在终端中输出一些日志信息.棒!
在下一篇文章中,我们会完成整个时钟的实施,得到一些非常酷的内容.