saga-duck是用来做什么的?

一句话:它是基于ducks模式思想,实现了模块化、可复用、可扩展及可组合特性的redux-saga开发方案。

在使用Redux时,我们发现业务逻辑过于分散,没有模块化,于是决定使用ducks模式来管理代码。

因为业务交互复杂,使用了redux-saga来管理逻辑,这就需要duck能支持redux-saga,并可以扩展和组合使用。extensible-duck 是个很不错的方案,但是它没有考虑支持redux-saga,并且组合使用不太方便,于是我们便重新造一个轮子。

实现可复用

通常Redux的actionType都定义为常量,是固定不变的。但如果要复用,在同一个Redux store中就会冲突了,于是和extensible-duck一样最终的duck实例会根据路径的不同,生成不同的actionType(例如 namespace/route/ADD)。

actionType不同了,理所当然相关的reducer和actionCreator也会不同,都是动态生成。

于是便成了以下模式,reducers和creators都是一个方法,传入当前的duck实例(可通过duck.types.XX访问到最终的actionType),输出最终的结果。

import {Duck} from 'saga-duck'
const duck = new Duck({
    typeList: ['ADD'],
    reducers: ({types}) => ({
        num: (state=0, action)=>{
            switch(action.type){
                case types.ADD:
                    return state+1
                default:
                    return state
            }
        }
    }),
    creators: ({types}) => ({
        add: ()=>({type: types.ADD})
    })
})

支持redux-saga

在参考extensible-duck后,我们添加了可扩展的sagas和selectors配置

sagas

sagas配置很简单,就是saga(generator)数组,在扩展时会自动进行合并,执行时入参为当前duck,通过duck可取得最终的types、selectors、creators等属性

selectors

通常我们都是比较暴力地访问store,而在模块化后,你并不确定Duck被用在哪个位置,所以在saga中访问状态需要通过selector进行

yield select(duck.selector) // 获取当前duck对应的state
yield select(duck.selectors.xxx) // 获取对应的selectors的值

于是完整的带saga逻辑的duck就是这样

const duck = new Duck({
    typeList: ['ADD'],
    reducers: ...,
    creators: ...,
    selectors: {
        num: state => state.num
    },
    sagas: [
        function* ({types, selectors, selector, creators}){
            yield takeEvery(types.ADD, function*(add){
                const state = yield select(selector) // { num: 1 }
                const num = yield select(selectors.num) // 1
                yield put(creators.add())
            })
        }
    ]
})

可扩展

前面都是创建的Duck实例,如果需要一个预定义的类就可以这样做

class MyDuck extends Duck{
    init(){
        super.init();
        this.extend(
            {
                typeList: [...],
                reducers: duck=>({...}),
                selectors: {...},
                creators: duck=>({...}),
                sagas: [...]
            }
        )
    }
}

const duck = new MyDuck()

还可以继续进行扩展

class AnotherDuck extends MyDuck{
    init(){
        super.init();
        this.extend(
            {
                ...
            }
        )
    }

}

可组合

在最终用到Redux store上时,我们需要把ducks按路径拼装,比如我们实现了一个列表交互ListDuck,现在界面上有两个列表,那么可以这样组合成一个Duck

import { DuckMap } from 'saga-duck'

class MyDuck extends DuckMap{
    init(){
        super.init();
        this.extend(
            {
                ducks: {
                    list1: ListDuck,
                    list2: ListDuck
                },
                sagas: [
                    function*({ducks:{list1, list2}}){
                        yield select(list1.selectors.list) // 列表1的数据
                    }
                ]
            }
        )
    }
}

注: DuckMap继承自Duck,有它的一切功能。

使用到React上

最终所有业务可以组合到一个RootDuck上,然后可以通过DuckRuntime来自动执行起来,并便捷地与React组件关联起来

import { Provider } from 'react-redux'
import { DuckRuntime } from 'saga-duck'
const rootDuck = new RootDuck()
const duckRuntime = new DuckRuntime(rootDuck)
const ConnectedContainer = duckRuntime.connectRoot()(Container) // 也可以使用decorator快速定义 @duckRuntime.connectRoot()

ReactDOM.render(
    <Provider store={duckRuntime.store}>
        <ConnectedContainer />
    </Provider>,
    document.getElementById('root')
)

Container中可以这样访问Redux store及duck相关内容

function Container(props){
    const { duck, store, dispatch } = props
    const { selectors, creators, ducks: { list1, list2 } } = duck
    // const xxx = selectors.xxx(store)
    // const list1Data = list1.selectors.list(store)
}

注意:建议所有与duck相关的React组件统一使用约定的 { duck, store, dispatch } 格式来传递到子组件,方便我们后续进行简单的性能优化。详情请看 进阶

支持Typescript

2.0版我们引入了typescript,在VS Code下可以做到比较方便的代码提示及纠错

但需要注意的是,目前只能支持到 typescript 2.6.1,请在VS Code中设置typescript使用工作区版本

type State = number;
enum TYPE{
  INCREMENT,
  INCREMENT_IF_ODD,
  DECREMENT,
  INCREMENT_ASYNC
};
interface Creators{
  increment(step?: number): { type: string; step: number };
}
interface Selectors{
  count: number;
}
interface Options{
  step?: number;
  getStep?: () => number;
}
export default class MyDuck extends Duck<
  State,
  typeof TYPE,
  Creators,
  Selectors,
  Options
> {
  init() {
    super.init();
    this.extend(
      {
        types: TYPE,
        ...
      }
    );
  }
}

results matching ""

    No results matching ""