12 OCTOBER 2016
這是翻譯版本,原文請見
第一部分譯文請見,
第二部分譯文請見吐辙,
第三部分譯文請見.
簡單的Redux Saga 模板
在這個文章中,我們將完成完整的React/Redux/Redux Saga app,并且來看看為什么要這樣做.
我已經(jīng)創(chuàng)建了一個app的模板作為本文的起點,我們沒有必要關(guān)注一些開發(fā)的細(xì)節(jié),因為這些細(xì)節(jié)不是本系列文章的重點(我假設(shè)你已經(jīng)了解React,Redux以及與此相關(guān)的開發(fā)工具.)但是我仍然會簡單強調(diào)一些內(nèi)容,以便于你對項目依賴包和配置有一些基礎(chǔ)的了解.你可能是個高手,或者是個不折不扣的菜鳥(是菜鳥也沒有關(guān)系)如果你不關(guān)心這些基礎(chǔ)內(nèi)容,直接跳到那副圖片,看看后面的內(nèi)容.
第一步,克隆repo,并且安裝依賴包:
//原文的repo不能運行了,下面的repo是驗證過的
git clone https://github.com/granmoe/redux-saga-clock-tutorial.git
cd redux-saga-clock-tutorial
npm i
好了做完上面的工作,使用你喜歡的編輯器打開項目,讓我們先看看里面有些什么內(nèi)容.
在我們的package.json文件中每個元素都是非常標(biāo)準(zhǔn)的,但是要注意,如果要對付不支持ES2015標(biāo)磚的瀏覽器,需要引入babel-polyfill包.這個包必須在redux-saga之前引入(譯者:redux-saga使用了ES2015的技術(shù)宣决,所以要先獲得支持才可以).
你也可以注意到,在package.json中有ESLint依賴包,因為我發(fā)現(xiàn)這個依賴包是開發(fā)中的無價之寶.
下面是我們的babel配置,在.babelrc文件中:
{
"presets": ["es2015", "react", "stage-2"]
}
我已經(jīng)決定使用es2015,react和stage-2.
我還想講講.eslintrc文件,但是我實在是不想讓你看到想睡覺.
webpack和index.html文件沒講,但是這里估計沒有人會對這兩個文件感興趣.
開始進(jìn)入正題吧
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>,
這里我們導(dǎo)入一些依賴項(包括babel-polyfill),導(dǎo)入根組件,redux store的配置,實例化store,然后在經(jīng)過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
方法來創(chuàng)建middleware實例.接下來,把根reducer和middleware傳遞到createStore
,這就創(chuàng)建了一個redux store.然后把我們app的root saga傳遞進(jìn)saga middleware.這一步一定要在redux store實例化以后再執(zhí)行.rootSaga
是頂級generator,這個generator負(fù)責(zé)代理其他所有的generators的工作(馬上會看到.)
上面都是些什么見鬼的代碼,你個王八蛋(譯者:原意直接翻譯啊)
其實我們已經(jīng)有了有趣的東西,我們的代碼基本上依賴兩個文件.”app.jsx”是一個react組件,可以根據(jù)app的state和基于DOM事件系統(tǒng)的actions來返回渲染的html標(biāo)記.”duck.js”包含單純對象actions和reducer,這兩個函數(shù)一起工作描述出怎么修改state.其中也包含了所有的控制流代碼,控制流代碼描述了整個app的處理過程.如果你很熟悉標(biāo)準(zhǔn)的鴨子模型,我僅僅修改了鴨子模型,讓他很容易包含saga代碼.讓我們使用鴨子模塊來工作吧.
我們將會創(chuàng)建一個可以控制的時鐘.開始來想想app需要的最精簡的sate結(jié)構(gòu).在任何時間我們要詢問app的狀態(tài)是”現(xiàn)在幾點了?”所有我們需要存儲的就是單個的數(shù)字.現(xiàn)在讓我們來看看怎么改變這個狀態(tài).好的,我們我們將制作一個時鐘,用戶可以向前,向后,暫停和重置.這里的構(gòu)想意味著我們表征時間的代碼邏輯需要增,減,什么也不做,重置到0.什么事也不做意味著不需要sate發(fā)生改變,所以我們留下增加,減少,重置.我們要顯示時間的毫秒數(shù),因此app的state就定為”毫秒數(shù)”.
正如上面所講的,我們在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做出合適的修飾,之后創(chuàng)建新的state昏苏。最后我們export(模塊模式)一些可以在其他地方調(diào)用的單純action對象
.(馬上我們會在saga中導(dǎo)入action對象之一).示例代碼總是這么這么的整潔.
現(xiàn)在我們需要實現(xiàn)一下app的流程.在處理過程中,什么狀態(tài)需要輸入尊沸?這個問題的另一個問法是:app在某個特定的時間應(yīng)該做什么工作?我們的時鐘可以向前,向后,暫停.為了在這幾個過程中相互轉(zhuǎn)變,我們需要三個action,開始時鐘,撥回時間,暫停時鐘.
從代碼//saga actions
開始,看看duck模塊的剩余部分.我們已經(jīng)創(chuàng)建了三個actions,我們的root saga在收到某個action的時候,會啟動一個傻瓜處理流程.現(xiàn)在在代碼里傻瓜處理流程只是打印一下action的名字.后續(xù)我們會開始根據(jù)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(嚴(yán)格上講,根據(jù)術(shù)語來說應(yīng)該是叫“action creators”,但是無所謂,只要你理解具體的意義就可以)應(yīng)該看起來和其他的redux actions類似.但是這些action在我們的reducer中不能得到處理.如果保持僅僅在saga代碼附近保留這些actions,這里的acions僅僅觸發(fā)saga.做到這一點,會避免action和根據(jù)這些action做出的state修改的代碼混雜在一起.顯而易見,saga action仍然通脫connect
函數(shù)綁定到store實例,并且輸入到組件里.
現(xiàn)在解釋一下這個文件里奇怪的saga.你還記得rootSaga
被傳遞到saga中間件,對嗎捷雕?坦率講,你可能也不知道,但是這也沒關(guān)系.每次我們發(fā)出一個action,action會被推送到經(jīng)過sagaMiddleware.run(generator)
包裝的generator.這就意味著,每個generator都有機(jī)會響應(yīng)action,在我們的實例中,rootSaga
遇到匹配的action type的時候才會做出響應(yīng).我們正在使用從Redux Saga獲取的takeLatest
助手函數(shù)完成這個工作.takeLatest
接收任何與action type數(shù)組匹配的action,然后接著傳遞他,啟動一個handleClockAcion
流程,傳遞進(jìn)action.takeLatest
意思是直接收最新的action椒丧,如果現(xiàn)在還有正在運行的handleClockAction
的話,在新的action開始之前,當(dāng)前的這個處理流程需要先退出.handleClockAction
,本質(zhì)上是在后臺啟動,允許rootSaga
保持運行狀態(tài),即使handleClockAction
仍在運行,也可以接受下一個匹配的action.
注意我們使用的yield
關(guān)鍵詞,回想一下,yield
在generator中發(fā)出和接收值.在任何時間,我們yield
一個Redux Saga助手或者effect的時候,我們就正在和Saga middleware進(jìn)行通訊.在我們的上面的實例中,Redux Saga等待匹配發(fā)送到saga的action.后面我們還會更進(jìn)一步深入討論.
我希望你至少對這個流程有一點感覺.我認(rèn)為可能在測試過程中(譯者:這里的意思是實際運行代碼的過程,并不是代碼的測試過程)你對這個流程更清楚一點.所以讓我們看看React組件中怎么和用戶進(jìn)行交互的過程.
在組件這一點看,“app.jsx”是非常簡單的react組件.讓我們看個仔細(xì).
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高內(nèi)聚組件,我們可以從store的state獲取一個字段,并作為props傳遞進(jìn)入組件.我們也通過一個對象傳遞四個action creators.Redux把這個對象綁定到store實例中,確保我們在組件中調(diào)用這幾個action的時候,他們可以正確的被dispatch.
在我們的渲染中,我們返回一個<div>
,這個元素中有一個SVG(后續(xù)中將會比較關(guān)鍵).SVG有一些事件操作句柄,這些操作句柄將會dispatch state修飾actions.接下來,會有一個<p>
元素依據(jù)app的state來顯示當(dāng)前時間.最后我們有幾個<button>
s 連接到saga的actions.
上面的代碼都就位以后,我們就試著運行一下app,驗證一下基礎(chǔ)構(gòu)架和actions的工作情況.
到底能工作嗎救巷?
回到你的終端,運行npm start
.現(xiàn)在輸入localhost:8080,在瀏覽器中打開devtools,檢查一下js 終端.當(dāng)你點擊buttons的時候,會看到saga actions的日志輸出.現(xiàn)在試著在SVG上點擊,鼠標(biāo)一定,雙擊action.你可以看到毫秒文本的更新.
真好啊,我們創(chuàng)建了一個Redux Saga app的模板結(jié)構(gòu),了解了怎么使用takeLatest
.還可以在終端中輸出一些日志信息.棒壶熏!
在下一篇文章中,我們會完成整個時鐘的實施,得到一些非常酷的內(nèi)容.