0. 思路整理

React值不值得学习好用不好用基本上就不需要多说了。这篇文章基本上就把我这边的学习心得进行整理,记录。React的学习的难点在于整体的生态系统太过庞大,而React本身提供的功能是相当有限的,就导致了使用者必须整合各种工具和第三方的组件,拔升了学习的曲线。这篇学习笔记的重点就在于将生态系统和各组件之间的关系解释清楚。

1. React

React就只是一个View。提供了JSX语法,用HTML语言或Native的组件来进行展示上的工作。最关键的是一个Component概念,所有的React对象都是一个Component,只要知道这点就可以了。

需要了解的使用语法:

import React, {Component} from "react";

export default class App extends Component<any, any> {
    render() {
        return (
            <View style={\{marginTop: 100\}}>
                <Text>Sample Text.</Text>
            </View>
        );
    }
}

2. Redux

ReduxFlux架构的一种实现,用来进行数据流的处理和控制。简单来说就是类似于MVC中的Controller还有Model两部分的组合。这里我们需要厘清Redux中的几个核心概念 / 组件。

这里也附带提供中文手册的访问地址。

2.1 概念梳理及数据流转

Redux中的核心概念:

  • Store:一个应用程序只有一个的数据中心,其实就只是一个JS对象;所有的数据都存储在这个地方
  • Action:用户发起的一个行为,其实就只是一个JS对象;必须要有一个字符串属性type,用来标识该行为的类型;一般type是一个常量字符串
  • ActionCreator:创造Action的函数,每次该函数的调用都会返回同样的结果;设计上的概念,使用上没有实际意义
  • Reducer:接受一个Action,并依此修改Store中数据的函数;同ActionCreator,每次调用结果必须一致
  • dispatch:store.dispatch(Action),激发Action的触发器;Action本身只是一个对象,并不会自行流转到Reducer,需要Store进行dispatch,才会启动数据流转

流转: Store => store.dispatch => Action created from ActionCreator() => Reducer => Store changed

2.2 Reducer

重申一下,Reducer是一个函数。签名如下:

export type Reducer<S> = <A extends Action>(state: S, action: A) => S;

使用上也很简单,其实就只是一个接受两个参数的函数:

let reducer = function(state, action) {
    let cloned = Object.assign({}, state); // 永远记得需要clone state后修改,而不是直接修改state
    if (action.type === "ACT_UPDATE_TEXT") {
        // 判断类型Action类型是不是当前reducer应该处理的,如果是,则根据业务修改state里的数据
    }
    return cloned;
};

上面的例子只是一个很简单的单ActionReducer,后续我们会介绍如何将大量的Reducer组织起来,让React应用进行统一使用。

2.3 Action

重申一下,Action就只是一个对象。签名如下:

export interface Action {
  type: any;
}

对应的需要有一个创建Action的ActionCreator。签名如下:

export interface ActionCreator<A> {
  (...args: any[]): A;
}

使用上也很简单:

import {ActionCreator} from "redux";

export const ACT_UPDATE_TEXT = "ACT_UPDATE_TEXT"; // 类型常量

export interface ActionUpdateText { // Action结构
    type: string;
    text: string;
}

export let ACTUpdateText: ActionCreator<ActionUpdateText> = function (text: string) {
    return {
        type: ACT_UPDATE_TEXT,
        text: text
    };
};

2.4 Store

重申,只是一个对象。使用者应该在该对象根目录下面创建组件 / 页面对应的namespace,以便进行数据管理:

{
    "ComponentTextInput": {
        "text": "current user input text."
    },
    "ComponentA": { ... },
    "ComponentB": { ... },
    ...
}

创建Store实例:

import {createStore, applyMiddleware, combineReducers} from "redux";

const store = createStore(
    combineReducers({
        "NamespaceOfTheComponent": reducer // 表示Store中,NamespaceOfTheComponent下的所有数据会交由reducer这个函数处理
    })
);

dispatch是Store实例的一个函数,只有这个函数可以进行Action事件的触发。

store.dispatch(ACTUpdateText("New Text to be updated into store."));

3. 问题与思考

介绍到这里我们先暂停下。React负责View的展示,Redux则负责数据的处理和消息流转。看上去这两者结合的很好,非常匹配,但实际使用中我们会发现,其实还有很多问题等待解决。这两者都在自己的领域上发挥了很好的作用,但是要让他们合起来一起好好工作,还有很多细节需要调整。

3.1 Store的全局性

在上面我们说到Store的时候,可以看到,Store是需要创建出实例的,并且有且唯一只有一个实例。而且,所有的事件触发都需要从store.dispatch开始。那么问题就来了,在进行组件封装的时候,组件、甚至是子组件,如何能够或得到store实例以进行事件和数据处理?为了解决这个问题,React官方提供了一个组件react-redux。这个组件使用依赖注入的方式,将React和Redux进行了很好的绑定,将Redux的Store和自定义的Action输送到了React的Component内部,方便使用。

3.2 异步调用

在上面说到Action和Reducer的时候,我们说过,这两者都是很纯粹的JS对象和函数,ActionCreator也是很纯粹的函数。但实际在进行应用程序制作的时候,我们不可避免的需要使用到异步函数,来进行外部IO、API调用等等。那么,应对这种需求,我们如何进行处理呢。这里就不得不提到一个第三方的组件:redux-thunk。具体的概念可以看官方文档,在本文中我们不会具体涉及到该组件的使用,它是一个进阶的技巧,本文主旨在于梳理React整体的生态和组件整合。

3.3 组件整合

编写React我们必然会编写大量的组件,为了代码复用,也为了代码管理。那么我们创建出来的大量Action及Reducer都需要管理。Action会映射到本组件内,所以管理上问题还不大。而Reducer则是需要统一绑定到Store上,这部分就需要小心处理了。后续我们会讲解这部分工作如何处理。

4. react-redux

这个组件一共提供了两个部分,都非常简洁,但很重要。

4.1 JSX组件Provider

这个JSX组件我们需要应用在根节点的组件上,将根节点组件的所有JSX都包裹在其中即可:

import React, {Component} from "react";
import {View} from "react-native";
import {createStore, applyMiddleware, combineReducers} from "redux";
import {Provider} from "react-redux";

const store = createStore(
    combineReducers({
        "NamespaceOfTheComponent": reducer // 表示Store中,NameSpaceOfTheComponent下的所有数据会交由reducer这个函数处理
    })
);

export class App extends Component<any, any> {
    render() {
        return (
            <Provider store={store}>
                <Router>
                    <View> ... </View>
                </Router>
            </Provider>
        );
    }
}

4.2 connect函数

该函数是用来将Action和State注入到组件内所用的一个函数。

import React, {Component} from "react";
import {bindActionCreators} from "redux";
import {connect} from "react-redux";

export class App extends Component<any, any> {
    ...
}

export default connect(
    // bind state
    (state) => ({
        // Store["NamespaceOfTheComponent"] 会被注入到 App 的 this.props 里面
        // 可以通过 this.props["NamespaceOfTheComponent"] 进行访问,并且会侦听Reducer对这部分数据的修改
        "NamespaceOfTheComponent": state["NamespaceOfTheComponent"]
    }),
    // bind dispatch action
    (dispatch) => ({
        actions: bindActionCreators({
            // 这两个 ActionCreator 会被注入到 App 的 this.props.actions 里面
            // 可以通过 this.props.actions.ACTUpdateText("Updated") 进行使用
            // 注意看,被注入进去的 ActionCreator 是不需要主动dispatch的,在进行调用的同时就自动dispatch出去了
            ACTUpdateText, 
            ACTClearText
        }, dispatch),
        // 最后加的这个dispatch,表示dispatch函数会被注入到 App 的 this.props 里面
        // 可以通过 this.props.dispatch(ActionCreator) 进行使用
        dispatch
    })
)(App);

5. 组件整合

现在整理下思路:

  • React负责展示
  • Redux负责数据处理和事件处理
  • react-redux负责将数据和事件注入到组件或子组件内,便于在组件内进行使用,且能够侦听到外部Reducer对数据的修改

到这步为止,一个组件的独立运行已经是没有任何问题了。那么接下来我们需要看下如何从应用层面,将整个系统整合起来。这里有两个地方需要梳理:

  1. 组件的Action
  2. 组件的Reducer

5.1 Action

在之前的connect函数教程中,我们提到了,组件内使用到的ActionCreator都需要使用connect再注入到组件内,转完成一个能直接使用的类dispatch的函数。因此在每个组件对外暴露的地方,我们简单将所有的Action归并到一个对象内即可:

import React, {Component} from "react";
import {bindActionCreators} from "redux";
import {connect} from "react-redux";

export class App extends Component<any, any> {
    ...
}

let ACTUpdateText = ...;
let ACTClearText = ...;

export default connect(
    // bind state
    (state) => ({
        "NamespaceOfTheComponent": state["NamespaceOfTheComponent"]
    }),
    // bind dispatch action
    (dispatch) => ({
        actions: bindActionCreators({
            ACTUpdateText, 
            ACTClearText
        }, dispatch),
        dispatch
    })
)(App);

5.2 Reducer

Reducer的整合更麻烦点,因为Reducer最终是要整合到唯一的Store上的,在代码上反应在createStore函数的使用上。因此所有的Reducer我们需要手动整理到一个单独的文件内进行暴露。

// file Reducers.ts
import * as TextInputReducer from "../component/text_input/Reducer";
...

export default {
    [TextInputReducer.StateName]: TextInputReducer.Reducers,
    ...
};

// file App.ts 根节点组件
import Reducers from "../app/Reducers";

const store = createStore(
    combineReducers(Reducers)
);

6. 最终代码范例

文件结构:

src / 
    | App.tsx #根节点组件,应用的入口
    | app / Reducers.ts
    | component /
                | text_input /
                             | Action.ts
                             | App.tsx
                             | Reducer.ts
                             | State.ts

File App.tsx

import React, {Component} from "react";
import {View} from "react-native";
import {createStore, combineReducers} from "redux";
import {Provider} from "react-redux";

import Reducers from "./app/Reducers";

const store = createStore(
    combineReducers(Reducers)
);

export class App extends Component<any, any> {
    render() {
        return (
            <Provider store={store}>
                <View>
                    <TextInputApp />
                </View>
            </Provider>
        );
    }
}

File app / Reducers.ts

import * as TextInputReducer from "../component/text_input/Reducer";

export default {
    [TextInputReducer.StateName]: TextInputReducer.Reducers
};

File component / text_input / Action.ts

import {ActionCreator} from "redux";

export const ACT_UPDATE_TEXT = "ACT_UPDATE_TEXT";
export const ACT_CLEAR_TEXT = "ACT_CLEAR_TEXT";

export interface ActionUpdateText {
    type: string;
    text: string;
}

export interface ActionClearText {
    type: string;
}

export let ACTUpdateText: ActionCreator<ActionUpdateText> = function (text: string) {
    return {
        type: ACT_UPDATE_TEXT,
        text: text
    };
};

export let ACTClearText: ActionCreator<ActionClearText> = function () {
    return {
        type: ACT_CLEAR_TEXT
    };
};

export let Actions = {
    ACTUpdateText,
    ACTClearText
};

File component / text_input / App.tsx

import React, {Component} from "react";
import {View} from "react-native";

import {Actions} from "./Action";
import {StateName} from "./State";

export class Application extends Component<any, any> {
    render() {
        return (
            <View style=>
                ...
            </View>
        );
    };
}

export const App = connect(
   // bind state
   (state) => ({
       StateName: state[StateName]
   }),
   // bind dispatch action
   (dispatch) => ({
       actions: bindActionCreators(Actions, dispatch),
       dispatch
   })
)(Application);

File component / text_input / Reducer.ts

import {State} from "./State";
import {
    ACT_UPDATE_TEXT, ACT_CLEAR_TEXT,
    ActionUpdateText, ActionClearText
} from "./Action";

export {StateName} from "./State";

export interface ReducerInterface<S> {
    action: string;
    reducer: Reducer<S>;
}

export let RDCUpdateText = {
    action: ACT_UPDATE_TEXT,
    reducer: function (state: State, action: ActionUpdateText) {
        state.text = action.text;
        return state;
    }
} as ReducerInterface<State>;

export let RDCClearText = {
    action: ACT_CLEAR_TEXT,
    reducer: function (state: State, action: ActionClearText) {
        state.text = "";
        return state;
    }
} as ReducerInterface<State>;

export let Reducers = function (state, action) {
    let cloned = typeof state === 'undefined' ? {} : Object.assign({}, state);

    let reducers = [
        RDCUpdateText,
        RDCClearText
    ]
    for (let reducerInterface of reducers) {
        if (action.type == reducerInterface.action) {
            cloned = reducerInterface.reducer(cloned, action);
            break;
        }
    }

    return cloned;
}

File component / text_input / State.ts

export interface State {
    text: string;
}

export let StateName = "ComponentTextInput";