阅读 2017

React、Redux与复杂业务组件的复用

All State In Redux

在上一篇文章【Redux的副作用处理与No-Reducer开发模式】中,我们介绍了如何使用Redux/Redux-Saga来进行组件的状态共享,以及副作用处理。

在随后的开发中,我们所有的页面,以及业务逻辑组件都使用了这一套开发模式。举一个例子,我们有一个App搜索的AutoComplete组件,这个组件会做如下事情:

  1. 从Redux的State中读取用户token。
  2. 通过用户的token,请求后台服务,获取这个用户有权限看到的App列表。
  3. 将App信息载入组件,根据用户输入返回对应的搜索项。

由于这个组件需要读取存放在Redux State中的用户token,并且包含异步请求,将它的状态放入Redux中管理,并且使用Redux-Saga处理异步请求是非常合适的。

组件复用

但是在组件的复用性上,我们遇到一个难题,由于Redux本身并不提供模块化功能,我们想要复用使用了Redux/Redux-Saga的组件时:

  1. 我们需要为这个组件注册一个全局不冲突的reducerKey。
  2. 我们需要修改组件所关注的action类型,因为全局会有多个组件实例,如果action类型重复,会引起错误的组件状态改变。
  3. 在组件被卸载(Umountained)后,由于保存在Redux中的state不会被自动销毁,我们需要手动清理组件的app列表信息。
  4. 组件被卸载后,reducer继续存在,会轻微的损失一些执行性能。

针对这种情形,我们开发了Redux-ArenaRedux-Arena会将Redux/Redux-Saga的代码与React组件导出成一个React高阶组件以供复用:

  1. 在高阶组件被挂载(Mount)时,会自动初始化Redux-Saga的任务,初始化组件的reducer并在Redux维护的State上注册自己的节点。
  2. 在高阶组件被卸载(Unmout)时,会自动取消Redux-Saga的任务,销毁组件的reducer并从Redux维护的State上删除自己的节点。
  3. 提供组件信道机制,组件发送的Action默认只能被组件自己的reducer接收。也可以通过配置放弃信道,接收全局的action。
  4. 提供vReducerKey机制,Redux中如果组件间想共享state信息,需要知道知道真实的节点名称,在可复用的Redux组件中很容易引发冲突,Redux-Arena提供vReducerKey机制保证了state节点真实名称永远不会冲突。vReducerKey在同名时,下层组件会覆盖掉上层组件的vReducerKey信息。
  5. 提供单向的(类似flux的 one-way data flow)组件状态和actions的共享方案,下层组件可以通过vReducerKey获取上层组件的state和actions。
  6. 与Redux-Saga深度整合,在Redux-Saga中也可以选择只发送和接收组件自己的action。

构造可复用的高阶组件(Scene)

我们将每一个导出的Redux/React绑定的高阶组件称为Scene。首先我们要为Scene构造actions和reducer,然后使用bundleToComponent,导出高阶组件。

import { bundleToComponent } from "redux-arena/helper";
import * as actions from "./actions";
import state from "./actions";
import reducer from "./reducer";
import ComponentA from "./ComponentA";

export default bundleToComponent({
  Component: ComponentA,
  actions,
  state,
  reducer
});
复制代码

现在我们导出的这个组件,就可以和普通的组件一样直接在React中使用了。

需要注意的是,Redux-Arena增强了Redux的Store,需要使用createArenaStore创建Store:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createArenaStore } from "redux-arena";
import ComponentA from "./ComponentA";//上面导出的高阶组件

const store = createArenaStore();

const app = document.getElementById("app");
ReactDOM.render(
  <Provider store={store}>
    <ComponentA />
  </Provider>,
  app
);
复制代码

非常的简单。

通讯隔离

每个Scene默认只会接受自己所发送的action,其他Scene或者原生Redux绑定的action,将会被忽略。

举个例子:

import { bundleToComponent } from "redux-arena/helper";
import * as actions from "./actions";
import reducer from "./reducer";
import ComponentA from "./ComponentA";

export const ComponentA1 = bundleToComponent({
  Component: ComponentA,
  actions,
  reducer
});

export const ComponentA2 = bundleToComponent({
  Component: ComponentA,
  actions,
  reducer
});
复制代码

ComponentA1和ComponentA2的代码来源都是相同的,但是他们的reducer只会接收自己的Scene内部发送的Action,其他组件发送的Action即使type相同,也会被忽略。

State与Actions共享

原生的Redux中,如果要实现state的共享,需要为组件注册一个全局唯一的reducerKey,然后使用mapStateToProps方法,将对应state传入props。

Redux-Arena使用了Vitural ReducerKey(vReducerKey),vReducerKey不要求全局唯一,当子组件的vReducerKey与父组件的vReducerKey相同时,子组件的vReducerKey会覆盖掉父组件的vReducerKey的信息。

为Scene指定vReducerKey:

import { bundleToComponent } from "redux-arena/helper";
import * as actions from "./actions";
import reducer from "./reducer";
import ComponentA from "./ComponentA";

export default bundleToComponent({
  Component: ComponentA,
  actions,
  reducer,
  options: {
    vReducerKey: "a1"
  }
});
复制代码

和mapStateToProps相似,子组件使用propsPicker取出所需要的state和actions:

import { bundleToComponent } from "redux-arena/helper";
import state from "./state";
import actions from "./actions";
import ComponentAChild from "./ComponentAChild";

export default bundleToComponent({
  Component: ComponentA,
  state,
  actions,
  propsPicker:(state,actions,allState,{ a1 })=>({
    name: state.name,  //Scene的state
    actions,  //Scene的actions
    a1Name: allState[a1.reducerKey].name,  //ComponentA的state
    a1Actions: a1.actions   //ComponentA的actions
  })
});
复制代码

这样,我们就实现了组件间的state与actions的共享。

需要注意的是,这种共享模式类似Flux的one-way data flow,传递方向是单向的,反向的信息传递不能使用state,只能使用action。

如果是兄弟组件间的state共享,需要在这些兄弟组件间的某一个父组件上增加一个数据层,使用这个统一的父组件数据层共享状态。

Redux-Saga的整合

Redux-Arena提供了一系列的Redux-Saga方法实现通讯的隔离,使在Redux-Saga中可以只接收当前Scene所派发的action。

使用setSceneState,可以方便的设置当前Scene的State。

import { setSceneState, takeLatestSceneAction } from "redux-arena/sagaOps";

function * doSomthing({ payload }){
  yield* setSceneState({ payload })
}

export function* saga (){
  yield takeLatestSceneAction("DO_SOMETHING", doSomthing)
}
复制代码


Redux-Arena的Github地址:github.com/hapood/redu…,具体使用可以参考项目目录下的example,使用文档陆续补完中。


文章分类
前端