翻译|Redux Saga:完成时钟演示App

翻译版本,原文请见
14 NOVEMBER 2016

实现Saga代码

这篇文章是三篇关于Redux Saga系列文章的第三篇,也是最后一篇.在第一部分我们先对有关的概念进行了热身.在第二部分,我们和Redux关系推进到了另一个高度.我们把Saga付诸实施.在最后一篇文章,我们把最后一部分内容写完.如果你没有克隆app repo,可以在这个地址克隆,如果不想看这个repo,想直接跳到最终的app代码,可以看这个repo. live demo看这里.


第一部分译文请见,
第二部分译文请见,
第三部分译文请见.


What’s my name agian?

在上一篇文章中,我们设计了基础但是有点幼稚的需求,创建了很初级时钟,可以对用户的交互操作做出一些响应.(译者:后面有几句似乎和技术没有太大关系,我就没有翻译了.请参见原文).

我们决定只使用最小化的redux state值有一个字段“milliseconds”来代表时钟的时间.因为我们的时钟需要向前,向后,重置,所以创建了相应的redux action去执行增,减,重置actions,这些actions可以改变state.
当我们进入某些saga code代码,创建一个saga监听三个actions(开始,暂停,回拨),只有收到相应action的时候,输出日志内容.最后,我们的目标是把日志输出替换成时钟实际运行的流程,例如每一秒种发送一个递减的action,因此app state的异步操作需要好好的设计.最后我们创建一个简单的React组件,可以让我们根据期望的saga代码的工作流程测试redux和saga action,观察state的更新.

现在我们来完成这件事,我们想给朋友留下深刻的印象,看上去有意思一点,所以会使用一些非常酷的SVG技术,当时间增加的时候,时钟可以画出一些图形.但是最重要的是使用Saga可以是app唱歌,跳舞.开始编码.

实现Saga

我们的duck.js是非常小的,实际上仅仅需要很少几行代码去完成saga/redux.目前为止我们的duck代码是这样的:

 export function* rootSaga () {  
  yield takeLatest(['start-clock', 'pause-clock', 'rewind-clock'], handleClockAction)
}

function* handleClockAction ({ type }) {  
  console.log('Pushed this action to handleClockAction: ', type)
}

正如我们上一篇文章所讨论的,takeLatest将会监听action type数组中的任何一个action type,当每次发送的action和action type之一匹配的时候,会运行另一个流程(在我们的app中,是handleClockAction).takeLatest也会取消之前的运行流程,他也会传递整个action对象,我们可以在handleClockAction使用这个对象.

现在让我们考虑一下整个app应该怎么工作.所有的实际流程都是在handelClockAction中,在三个saga actions中,每一个action我们都做一些不同的工作.深入到代码的基础构架中.使用if/else if代码块替换handleClockAction的单行代码.这个代码块为每个action type提供分支判断逻辑,并且输出一些文本日志,验证代码的正确性.

给你几秒钟,耐心等待一下.

好了,下面的是我的handelClockAction的实际样子:

//if else if代码块进行分支判断执行流程
function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    console.log(`Received ${type}. We need to run the clock forward here.`)
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  } else if (type === 'pause-clock') {
    console.log(`Received ${type}. Guess what needs to be done here?`)
  }
}

现在运行app(npm start),转到localhost:8080,打开javascript终端,测试一些saga actions(他们和SVG图下面的按钮是连接在一起的).我们可以看到每次action执行时,有相应的日志输出.

当我们开始clock action,我们想让时钟每100毫秒递增一次.为了这样做,从saga导入助手函数”delay”.编辑redux-saga文件.

  import { delay, takeLatest } from 'redux-saga'  

delay非常简单,功能不言自明.它接收一个参数,毫秒数,然后暂停执行对应的时间.例如yield delay(50).在if/else if代码块中添加一些逻辑代码,每1000ms,输出日志.请记住,你需要这个过程无限制的重复.我们可以使用非常简单的原生javascript代码控制流来实现.我想让你先猜猜怎么实现.

不要偷看.

花点时间考虑一下.

好了,下面是我的代码

 function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {//就是这个while(true)语句保持始终运行
      yield delay(1000)
      console.log(`Received ${type}. We need to run the clock forward here.`)
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  } else if (type === 'pause-clock') {
    console.log(`Received ${type}. Guess what needs to be done here?`)
  }
}

在这一点上,打开js终端,查看日志输出,可以得到直观的信息.你将看到,start clock action暂停100毫秒,然后输出一些日志信息,之后无限地重复这个过程.注意一个关键事实,无论什么时间我们的takeLatest函数接收到匹配的action的时候,它会退出当前运行的handleClockAction.如果是暂停的action,什么事情也不做,但是还是要退出当前任何的action,猜猜我们应该怎么做?删除一点代码!暂停的时钟将会自动开始工作,因为takeLatest会退出当前的handleClockAction,不管时钟是向前还是向后运行,时钟都会停止.
确信你理解takeLatest的关键特性.在handleClockAction中做出合适的改变.应该想下面这样做:

 function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      console.log(`Received ${type}. We need to run the clock forward here.`)
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  }
}

现在让我们在循环内真正的dispatch一个action.
(旁白:我们没有实际dispatch一个action,仅仅请求中间件来做这件事,暂时不要担心,我后面会解释清楚的.)

在Saga里面dispatch一个action,我们要使用put side effects,添加一行代码到duck文件里.

 import { put } from 'redux-saga/effects'  

通过put请求一个dispatch的action,简单的使用yield put({ type: 'hi', data: 'I am an action' })就可以了.意思是在put里,可以调用一个action creator 返回一个单纯对象,像这样yield put(someAction()).(在javascript中函数的参数会立即求值).还记得我们在开始创建的那些redux action吗?我们要使用一个,转到while循环,试试put.

  function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      yield put(incrementMilliseconds())//注意这一句
    }
  } else if (type === 'rewind-clock') {
    console.log(`Received ${type}. We need to run the clock backwards here.`)
  }
}

在你的浏览器里试试.注意在SVG下面的时钟时间是递增的.

为了让时钟后退,可以重用start 时钟的大部分代码.

下面是回退的代码:

  function* handleClockAction ({ type }) {  
  if (type === 'start-clock') {
    while (true) {
      yield delay(1000)
      yield put(incrementMilliseconds())
    }
  } else if (type === 'rewind-clock') {
    while (true) {
      yield delay(1000)
      yield put(decrementMilliseconds())//递减
    }
  }
}


现在测试一下所有的saga actions,确保每件事情都工作正常.如果工作不正常,那绝对是你的错,不是我的错.开个玩笑.(译者:后面几句没有翻译)

激动人心的时刻是运行saga代码并且通过实验学到的了一些东西.一个提示:delay是阻塞的,其他的effect语句都是非阻塞的.试着使用for循环.要么把while条件改为false.我们已经放弃了promise的世界,现在我们可以使用这些简单的,可预测的,原生的javascript来表现出所有的控制流语句.

好了,把saga代码恢复到上面的样子.

做好了上面的恢复,我的saga代码已经完成,可以操作了(注意事项:在最终版本上,我已经把代码重新组织了一下,以便于阅读).

Saga’s的方式处理异步操作

好了.现在让我们再做一些事情,让我们谈谈代码是怎么工作的.Redux Saga中核心的概念是:我们实际没有像dispatch一个action到store一样执行异步操作.替代方法是:我们yield异步操作到saga middleware,然后middleware执行他们.简单的说,我们给Saga一个指示,告诉他我们希望发生什么.这些异步操作描述是遵循标准generator输出的单纯对象:{value:any,done:boolean},这和我们在第一部分看到的完全一样.
例如,yield put发送一个对象到中间件,这个中间件包含一个action对象,可以dispatch一些内容给我们(译者:这个地方有点绕,其实是在异步操作完成以后,需要再次dispatch返回的结果,从而改变state,这样我们的组件才可以根据state的改变来渲染效果,例如从远程资源请求数据,通常都需要把整个操作分成几步来完成,每一步要改变不同的state,在dispatch中再dispatch)这样定义异步数据流有很多好处,其中之一是可测试性.我们可以很简单的在每一个side effect返回中间件的时候创建断言(继续关注后续的内容,有测试的深入介绍)

我们应该遵循redux-saga的最佳实践,做些小小的更新,从redux-saga/effects中导入call,把yield delay(100)改为yield call(delay,100).因为我们只想返回side-effects到saga中间件,saga提供了calleffect把non-effects转变为effects.与仅仅yielddelay自己(promise对象)不同,我们通过calldelay转变为effect,然后yield它.所有的non-effect都应该做相同的处理.call的语法调用一个函数,并且传递参数列表.例如:yield call(fetch,url,options).最后,注意,call可以被promise对象,generators,或者函数使用.

代码不要太丑陋

我们需要创建一个文件作为app的配置,包含一些常量.为什么不叫做“config.js”在/src目录下创建,粘贴下面的内容.

 const MAX_RADIUS = 40

let hands = [  
 { ms: 144000, maxTicks: 1 },
 { ms: 36000, maxTicks: 4 },
 { ms: 12000, maxTicks: 3 },
 { ms: 2000, maxTicks: 6 },
 { ms: 400, maxTicks: 5 },
 { ms: 100, maxTicks: 4 }
]

export const STROKE_WIDTH = MAX_RADIUS / hands.length

hands = hands.map((hand, idx) => {  
 const radius = STROKE_WIDTH * (hands.length - idx)

 return {
   ...hand,
   radius,
   circumference: 2 * Math.PI * radius,
   alpha: 1 - idx / hands.length
 }
})

export const CLOCK_HANDS = hands

export const MINIMUM_MS = 100  

这里面都有什么内容?让我解释一下,它是一组常量和计算量,描述SVG的外观.
我不会详细讲这个内容,不是这里的重点.

好了,现在我们的saga代码已经就绪,我们也定义了所有SVG需要的配置文件.再更新一下SVG内容,好完成app.jsx的代码.

完成 HIM

下面是目前的app.jsx代码

 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 (
     

{ milliseconds }

) } } export default connect(state => ({ milliseconds: state.milliseconds }), ({ incrementMilliseconds, decrementMilliseconds, resetClock, startClock, pauseClock, rewindClock }))(Clock)

我们需要导入conifg内容,通过props传递到组件.删除一些不必要的导入文件.不再直接调用特定的actions,因为我们仅仅在saga中发出action.更新一下代码.

 import React from 'react'  
import { connect } from 'react-redux'

import { startClock, rewindClock, pauseClock, resetClock } from 'duck'  
import { CLOCK_HANDS, STROKE_WIDTH } from 'config'

现在我们有了数据,可以在SVG内画出漂亮的圆环.简单点的说我们使用同心圆代表小时/分钟/秒.时间流逝的时候,改变每一个圆环的长度.为了做SVG图片,使用了一些小技巧CSS Trick blog post

我们不再通过props手段传递millseconds字段.每次更新milliseconds时,我们计算每一个时钟指针的位置.这像是烦人的数学计算.基本上我们遍历每一个指针,然后在下一个指针重复同样的过程,(时针就减掉小时的整倍好秒数,分钟和秒一样处理),把剩余的时间传递给下去.最小的指针对应的最小的精度单位,100ms(config.js中的MAXIMUM_MS),更新你的connect

 export default connect(state => {  
 const currentTime = state.milliseconds
 let remainingTime = currentTime

 const getTicks = (hands, timeRemaining) => {
   let [hand, ...tailHands] = hands
   hand.ticks = Math.floor(timeRemaining / hand.ms)
   return tailHands.length ? [hand, ...getTicks(tailHands, timeRemaining % hand.ms)] : [hand]
 }

 const hands = getTicks(CLOCK_HANDS, remainingTime)
   .map((hand, idx) => {
     const offset = state.milliseconds >= hand.ms ? 1 : 0
     const position = hand.circumference - ((hand.ticks + offset) / hand.maxTicks * hand.circumference)

     return {
       ...hand,
       position
     }
   })

 return {
   hands
 }
}, ({
 startClock,
 rewindClock,
 resetClock,
 pauseClock
}))(Clock)
 

现在我们需要在组件中导入新的指针属性.在渲染方法中给props解构赋值:

 render () {  
 const {
   hands,
   startClock,
   rewindClock,
   resetClock,
   pauseClock
 } = this.props

为了最终版本,我们做一下最终的改变.我们更新SVG,启动onMouseEnter,回退onMouseLeave,暂停onClick.添加一下这些变化.

(译者:这一段也不翻了,原作者要喝伏特加).

做好了吧,现在我们在SVG内创建同心圆,遍历hands并且创建SVG元素,使用我们的计算值设置每个指针的半径,位置和透明度.

这是最终的更新,下面是render()函数返回的内容:

 return (  

  { hands.map((hand, index) => {
    const { radius, circumference, position, alpha } = hand
    return (
      
    )
  }) }

)

忘了件事情,返回duck.js文件,从config导入MINIMUM_MS:

 import { MINIMUM_MS } from 'config'  

现在在delay函数调用时使用MINIMUM_MS替换1000.
时钟每100ms就会滴答一下:

 function* handleClockAction ({ type }) {  
 if (type === 'start-clock') {
   while (true) {
     yield delay(MINIMUM_MS)
     yield put(incrementMilliseconds())
   }
 } else if (type === 'rewind-clock') {
   while (true) {
     yield delay(MINIMUM_MS)
     yield put(decrementMilliseconds())
   }
 }
}

看看在saga中控制流程的逻辑有多容易?

给你的时钟上发条

现在给这段代码拧发条,npm start,浏览器中打开localhost:8080.

翻译|Redux Saga:完成时钟演示App_第1张图片
redux-saga-clock.gif

这是一个能暂停,恢复,向前,向后和重置.看上去也很酷.

结束语

感谢阅读这个教程,我们涉及到了js generator和Redux saga的有关内容.我希望能帮助你理解怎么使用Redux Saga,同时还有使用的好处和背后的合理性.

如果你想了解更多的Redux Saga的内容,这里还有一些很好的教程.下面是我开始学习redux saga的时候帮助我入门的内容.

  • Redux的官方文档是无价之宝
  • Niels Gerritsen's的Redux Saga入门教程.这是我最开始学习Redux saga的教程之一,结构很清晰,帮助我跨国一些概念障碍.
  • Joel Hooks的教程
  • Jack Hsu的文章.我的实例实际是这个教程的扩展.

我也喜欢下面的文章,解密generators,激励我写这篇文章,没有这两个文章,我都不知道generators讲的是什么玩意.

  • David Walsh的博客,关于ES6 generator的内容,作者是kyle Simpson
  • Axel Rauschmeyer的ES6 generators的文章

感谢阅读!

你可能感兴趣的:(翻译|Redux Saga:完成时钟演示App)