近期 React-Native With Redux 开发的一点心得

正式投入React-Native开发

今年年初的时候,调研了一下RN,写了篇博客 《ReactNative学习技术的三部曲》发布在之前的博客上,主要是RN开发的一些基础入门知识和学习文档。进入4月份,继续进行AppSize瘦身,我们Native的代码已经精简到了一定的地步,可以说是黔驴技穷了,在Native开发层面上,很难再做进一步的瘦身(除非砍功能了,产品拿着刀说:“你砍一个试试” - -)。对于新技术的使用真的到了如饥似渴的地步。

目前使用RN开发iOS主要有以下两点好处(对于我们而言):

  • AppSize 得到迅速下降(在用户体验不受较大影响下);
  • iOS支持热发功能模块(尽管JSPatch真的很棒,但是上架功能模块还是有点费力);

那么为毛Android不那么迫切呢?

对比iOS,Android的AppSize不是那么大,Android已经支持很好的热发功能模块,所有Android的开发不是很急切,但是当RN统一技术栈的时候,就:”是时候表演真正的技术了”。

作为一个Native开发,转战React-Native开发,使用JavaScript来写代码,真的想说:”JavaScript从入门到头疼”。

在对RN有了一定认识和基础下,看了一下React的数据流管理,因为React是使用了组件化和状态机的思想,一切都为状态。state 来操纵 views 的变化。

然而,因为页面的组件化,导致每个组件都必须维护自身的一套状态,对于小型应用还好。但是对于比较大的应用来说,过多的状态显得错综复杂,到最后难以维护,很难清晰地组织所有的状态,在多人开发中也是如此,导致经常会出现一些不明所以的变化,越到后面调试上也是越麻烦,很多时候 state 的变化已经不受控制。

对于组件间通信、页面渲染,我们很需要一套机制来清晰的组织整个应用的状态,这时候Redux应然而生。

Redux

Redux is a predictable state container for JavaScript apps.

官方介绍 Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以跟大部分的 View 层框架配合使用,不限于 React。

Part

Redux 分为三大部分,Store , Action ,Reducer 。

简单理解为:

  • Store:统一存储所有组件的状态;
  • Action:定义操作,和传递操作数据;
  • Reducer:根据Action类型和传递数据改变组件State;

Action

在 Redux 中,action 主要用来传递操作 State 的信息,以 Javascript Plain Object 的形式存在,如

1
2
3
4
5
{
type: 'ByFlightNo_SelectedDate',
netWorkDate:'2016-04-20',
date:'4月20日'
}

在上面的 Plain Object 中,type 属性是必要的,除了 type 字段外,action 对象的结构完全取决于你,建议尽可能简单。type 一般用来表达处理 state 数据的方式。如上面的 ‘ByFlightNo_SelectedDate’ 表达的是航班日期的修改,netWorkDate是用于网络请求的字段,date是用于展示的字段。

上面写法没有任何问题,但细想,这种直接声明的 Plain Object 将越来越多,不好组织。实际上,我们可以通过创建函数来生产 action,这类函数统称为 Action Creator,

1
2
3
4
5
6
7
export function byFlightNoSearchSelectedDate(callback) {
return{
type: ActionConstants.ByFlightNo.SelectedDate,
netWorkDate:callback[0],
date:callback[1]
}
}

这样,通过调用 byFlightNoSearchSelectedDate(callback) 就可以得到对应的 Action,非常直接。

Reducer

有了 Action 来传达需要操作的信息,那么就需要有根据这个信息来做对应操作的方法,这就是 Reducer。 Reducer 一般为简单的处理函数,通过传入旧的 state 和指示操作的 action 来更新 state,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function flightNoSearch(state = {date:"4月18日",netWorkDate:"2016-04-18",flightNumber:"ZH9949"},action) {
switch (action.type){
case ActionConstants.ByFlightNo.SelectedDate:
return {
...state,
date: action.date,
netWorkDate: action.netWorkDate
};
case ActionConstants.ByFlightNo.onChangeTextFlightNo:
return{
...state,
flightNumber:action.flightNumber
};
default:
return state;
}
}

上面代码展示了 Reducer 根据传入的 action.type 来匹配 case 进行不同的 state 更新。
显然,当项目中存在越来越多的 action.type 时,上面的函数( Reducer )将变得越来越大,越来越多的 case 将导致代码不够清晰。所以在代码组织上,通常会将 Reducer 拆分成一个个小的 Reducer,每个 Reducer 分别处理 state 中的一部分数据,最终将处理后的数据合并成为整个 state。

我们可以把所有action.type都写在一个Reducer,但是一个项目那么多操作,写一个Reducer,这是要累死的节奏。我们可以写不同的Reducer,然后通过combineReducers()这个函数,将几个Reducer组合起来,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function flightNoSearch(state = {date:"4月18日",netWorkDate:"2016-04-18",flightNumber:"ZH9949"},action) {
switch (action.type){
case ActionConstants.ByFlightNo.SelectedDate:
return {
...state,
date: action.date,
netWorkDate: action.netWorkDate
};
case ActionConstants.ByFlightNo.onChangeTextFlightNo:
return{
...state,
flightNumber:action.flightNumber
};
default:
return state;
}
}
function flightCitySearch(state = {date: "4月18日", netWorkDate: "2016-04-18", depCity: "北京", arrCity: "上海"}, action) {
switch (action.type) {
case ActionConstants.ByDepArrCity.SelectedDate:
return {
...state,
date: action.date,
netWorkDate: action.netWorkDate
};
case ActionConstants.ByDepArrCity.SelectedDepCity:
return {
...state,
depCity: action.depCity
};
case ActionConstants.ByDepArrCity.SelectedArrCity:
return {
...state,
arrCity: action.arrCity
};
default:
return state;
}
}
let rootReducers = combineReducers({
flightNoSearch,
flightCitySearch,
})
export default rootReducers;

上面的 rootReducer 将不同部分的 state 传给对应的 reducer 处理,最终合并所有 Reducer 的返回值,组成整个state。

在 Redux 中,一个 action 可以触发多个 reducer,一个 reducer 中也可以包含多种 action.type 的处理。属于多对多的关系。

Store

在 Redux 项目中,Store 是单一的。维护着一个全局的 State,并且根据 Action 来进行事件分发处理 State。可以看出 Store 是一个把 Action 和 Reducer 结合起来的对象。

Redux 提供了 createStore() 方法来 生产 Store,并提供三个 API,如

1
var store = createStore(rootReducer); //rootReducer 为根 Reducer

store.getState()用来获取 state 数据。

store.subscribe(listener) 用于注册监听函数。每当 state 数据更新时,将会触发监听函数。

store.dispatch(action)是用于将一个 action 对象发送给 reducer 进行处理。

store 对象使得我们可以通过 store.dispatch(action) 来减少对 reducer 的直接调用,并且能够更好地对 state 进行统一管理。没有 store,可能会出现 reducer(currentState, action) 这样的频繁地传入 state 参数的更新形式。

bindActionCreators

从上面的 Action 相关介绍中可知,我们使用了 ActionCreator 来生产 action。所以在实际的 store.dispatch(action) 中,我们需要这样调用 store.dispatch(actionCreator(…args))。
借鉴 Store 对 reducer 的封装(减少传入 state 参数)。可以对 store.dispatch 进行再一层封装,将多参数转化为单参数的形式。 Redux 提供的 bindActionCreators 就做了这件事。如

1
var actionCreators = bindActionCreators(actionCreators, dispatch)

现在,经 bindActionCreators 包装过后的 action Creator 形成了具有改变全局 state 数据的多个函数,将这些函数分发到各个地方,即能通过调用这些函数来改变全局的 state。

React-Redux

Redux 并不依赖于 React,它支持多种框架 Ember、Angular、jQuery 甚至纯 JavaScript。但实际上,它更合适由 数据更新 UI 的框架。

上面最终通过 bindActionCreators 得到具有操作全局 state 的函数集合,在与 React 搭配时,就会将这些函数分发到各个对应的组件中,从而组件具备了操作全局的 state 的功能。在上节中可以得到,调用操作全局 state 的函数,最终将更新 state。当 redux 与 react 结合,在更新 state 时,将会触发 重新渲染 组件的函数,进而组件得到更新。
React-Redux 主要提供两个组件来实现上述功能。

Connect

Connect 组件主要为 React 组件提供 store 中的部分 state 数据 及 dispatch 方法,这样 React 组件就可以通过 dispatch 来更新全局 state。在 React 组件中,如果你希望让组件通过调用函数来更新 state,可以通过使用 const actions = bindActionCreators(actions, dispatch); 将 actions 和 dispatch 揉在一起,成为具备操作 store.state 的 actions。最终将 actions 和 state 以 props 形式传入子组件中。如:

1
2
3
4
5
6
7
8
9
10
11
function mapStateToProps(state) {
return state;
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch)
};
}
// 最终暴露 经 connect 处理后的组件
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);

这样子组件就不必关心自己的state,只需要从Props中读取对应的Reducer函数中的state即可,弱化了组件自身维护state的机制。将双数据流Props和State整合为了单一数据流Props。

备注

  • Props理解为父组件向子组件传递的参数,子组件不可修改Props;
  • state理解为子组件自身用与展示或者自己可以修改的数据;

Provider

Connect 组件需要 store。这个需求由 Redux 提供的另一个组件 Provider 来提供。源码中,Provider 继承了 React.Component,所以可以以 React 组件的形式来为 Provider 注入 store,从而使得其子组件能够在上下文中得到 store 对象。如

1
2
3
<Provider store={store}>
<MainPageContainer />
</Provider>

Redux-thunk

Redux 中使用 Redux-thunk 做异步操作(Async Actions)。

首先理解一下”中间件(middleware)”的概念。

通常来说中间件是在某个应用中 A 和 B 部分中间的那一块,
中间件可以把 A 发送数据到 B 的形式从

A -----> B

变成:

A ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> B

Redux 并不能自动处理 action creator 中返回的异步函数。
但如果在 action creator 和 reducer 之间增加一个中间件,就可以把这个函数转成
适合 Redux 处理的内容:

action ---> dispatcher ---> middleware 1 ---> middleware 2 ---> reducers

每当一个 action(或者其他诸如异步 action creator 中的某个函数)被分发时,中间件就会被调用,并且在需要的时候协助 action creator 分发真正的 action。

异步 action creator 提供的中间件叫 thunk middleware 即Redux-thunk这个库干的事儿。

为了让 Redux 知道我们有一个或多个中间件,我们使用 Redux 的辅助函数:applyMiddleware
applyMiddleware 接收所有中间件作为参数,返回一个供 Redux createStore 调用的函数。
当最后这个函数被调用时,它会产生一个 Store 增强器,用来将所有中间件应用到 Store 的 dispatch 上。

下面就是如何将一个中间件应用到 Redux store:

1
2
3
4
5
6
7
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
export default function configureGenericStore(reducer, initialState) {
let createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore);
return createStoreWithMiddleware(reducer, initialState);
}

如何使用的呢?

在我们写ActionCreator时,如果返回一个正常的包含type的字面量对象时,就认为这是一个同步的。例如:

1
2
3
4
5
6
7
8
export function byFlightNoSearchSelectedDate(callback) {
return{
type: ActionConstants.ByFlightNo.SelectedDate,
netWorkDate:callback[0],
// 展示使用的日期
date:callback[1]
}
}

如果我们我们返回了一个函数,那么就认为这是一个异步的Action,而且只要返回的是函数,那么就回异步下去,直到返回了一个包含type的字面量对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
export function myFocusConfirmUnfocusFlight(itemData) {
return dispatch => {
Alert.alert(
null,
'确定要取消关注吗?',
[
{text: '确定', onPress: () => dispatch(myFocusBeginUnfocusFlight(itemData))},
{text: '取消', onPress: () => {}},
]
)
};
}

这里又返回了一个异步action

1
2
3
4
5
6
7
8
9
10
11
12
13
export function myFocusBeginUnfocusFlight(itemData) {
return dispatch => {
// 网络请求返回,传入
return dispatch(myFocusUnfocusFlight(itemData));
}
}
export function myFocusUnfocusFlight(itemData) {
return {
type: ActionConstants.MyFocus.UnfocusFlight,
data: itemData,
}
}

直到返回了包含type的字面量对象,这个异步操作就算完成了。

完整的 Redux 数据流

最后串一下完整的Redux数据流图,清晰的控制了数据流,毕竟数据驱动界面,妈妈再也不用担心数据流乱了。

从 MVC 到 Redux 开发

基于 MVC 的目录结构 (主目录按照模块分类,内目录按照业务分类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── 业务目录
| |
| ├──业务1
| | |
| | ├──Model
| | ├──View
| | ├──Controller
| | |
| ├──业务2
| | |
| | ├──Model
| | ├──View
| | ├──Controller
| |
| ├──Expand(扩展)
| |
| ├──Vender(第三方)
| |
| ├──Resource(资源)
. .
. .
└──

在刚开始学习Redex时,我总是试图将这种数据流思想映射到Native开发的MVC或者MVVM上,之前我们Native开发大概是这么个流程:

与后端定义接口 -----> 封装网络Model -----> 写界面-----> Controller -----> View -----> ViewModel -----> Action操作 -----> ViewModel -----> Model -----> 网络请求 or 本地存储 -----> ...

这么个流程中Action其实不是核心,或是Action并不像是进攻发动机 - - 。

基于Redux的RN目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── src #开发目录
| |
| ├──constants #ActionTypes和Urls
| |
| ├──actions #actions的文件
| |
| ├──components #内部组件
| |
| ├──containers #容器组件
| |
| ├──reducers #reducer文件
| |
| ├──stores #store配置文件
| |
| └──utils #工具
|
├── node_modules #包文件夹
├── .gitignore
├── index.js #入口文件
└── package.json

在RN开发中,我们会省去封装网络Model这个过程,在新增模块的时候,也会按部就班的来:在ActionTypes中添加动作定义,在actions中定义Action,在reducers中定义reducer,然后在containers中写好容器外壳,最后在components中写组件。

与后端定义接口 -----> 定义ActionType -----> ActioCreator函数 -----> reducer函数 -----> container 容器外壳 -----> components组件 -----> ...

这样看来,Action 位置提到了进攻发动机的位置 - -。

最后

其实总体感觉下来,Redux 和 MVC 根本目的是类似的,都是为了解耦。

从一个Native开发人员转向web开发确实略显痛苦,前端东西很多,而且很多思维方式和Native开发不同,主要有两个:

  • 由静态类型语言转型动态类型语言,代码规范,和调试方面需要过渡;
  • 由MVC or MVVM 思想 过渡到Redux这类 数据流管理思想;

RN目前是解决跨平台和迅速迭代的一种方案,值得深入研究。

李剑飞 wechat
欢迎订阅我的微信公众号"剑飞说"!
坚持原创技术分享,您的支持将鼓励我继续创作!