美文网首页前端大宝剑React.js
05-使用Redux进行状态管理

05-使用Redux进行状态管理

作者: 七玄之主 | 来源:发表于2019-06-26 16:30 被阅读14次

    什么是Redux

    Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux将应用程序所有的状态 State 都保存在 Store 中,因此每个应用程序也只能有一个 Store。State 不能直接改变,必须通过 Reducer 来触发。Reducer 是一个无副作用的纯函数,形式为 (state, action) => state,意思通过传入变化前状态及动作,返回执行该动作后的变化后状态。Action 即描述了如何改变 State。

    整体架构及流程图

    1.在 View 层,用户通过点击等动作触发事件。
    2.将事件转化为对应的 Action 传递给处理的 Middleware。
    3.Middleware处理对应的副作用后,再调用对应的 Reducer ,根据原始 State 和 传递过来的 Action 合成新的 State,此时可以触发异步调用。
    4.将合成后的 State 返回给 View。

    Redux目录构成

    创建如下目录结构来实现Redux状态管理

    src
    └─ src/
       └─ actions                         定义所有的Action
       └─ components                      展示组件
       └─ containers                      容器组件
       └─ services                       管理api
       └─ reducers                        定义所有的Reducer
       └─ store                           管理Store
       └─ types                           管理ts定义的类型
    

    我们预计会使用到以下的包:

    • redux Redux 状态管理核心包
    • react-redux Redux 与 React 的绑定库
    • redux-actions 以FSA标准实现 Action
    • redux-thunk 实现异步 Action 的中间件

    运行安装命令

    yarn add redux react-redux redux-actions redux-thunk
    

    及类型定义

    yarn add -D @types/react-redux @types/redux-actions
    

    接着我们来实现一个简单的列表显示功能

    Store设计

    {
        ...
        novel: {
            isLoading: false,
            data: [
                {
                    id: 1,
                    title: 'Learning Python',
                    author: 'wang3',
                    comment: 'aaaaaaaaaa'
                },
                {
                    id: 2,
                    title: 'Learning Java',
                    author: 'wang4',
                    comment: 'bbbbbbbbbb'
                },
            ]
        }
    
    }
    
    • isLoading 表示请求状态,true 请求中,false 请求完成。
    • data是一个列表,表示一个域数据,是我们主要的业务数据。
    • 实际状态树中可能包含路由,浏览器历史等非业务状态。

    Action设计

    我们采用标准的FSA来实现 Action。FSA全称为Flux Standard Action,是一个实现 Action 的标准化规范。简单来讲,一个 Action 必须只能是一个简单的
    JavaScript 对象,它必须拥有一个字符串常量属性 type 来标识动作类型,它可以拥有 error,payload,meta属性,这3个属性可以是任意类型,除此之外的任何属性都不符合FSA的规范。

    • type 标识动作类型的字符串常量。
    • payload 任何不是 type或 status 等属性的值,都应该属于 payload,如果 error 等于 true的话,那 payload 就应该为一个 error 对象。
    • error 如果 Action 出错,那 error 可以设置为 true,如果 error 被设置成 true 以外的任意值,那该 Action 就不能被认为出错。
    • meta 它包括了不属于 payload 的其他任意属性。

    理解了FSA的概念,我们选择 redux-actions 来辅助我们实现 Action,这有助于我们在项目中维持统一的规范。

    一次异步请求API的过程都需要dispatch至少3种
    Action,首先 constants.ts 中增加 Action 定义:

    // 定义ACTION类型 
    export const ACTION_TYPES = {
        // 发起请求
        FETCH_NOVELS: 'FETCH_NOVELS',
        // 请求成功
        FETCH_NOVELS_OK: 'FETCH_NOVELS_OK',
        // 请求失败
        FETCH_NOVELS_NG: 'FETCH_NOVELS_NG',
    }
    

    新增actions/nobel.ts

    import { ACTION_TYPES } from '../constants'
    import { createActions } from "redux-actions";
    
    // 普通 Action
    const { fetchNovels, fetchNovelsOK, fetchNovelsNG } = createActions(
        ACTION_TYPES.FETCH_NOVELS,
        ACTION_TYPES.FETCH_NOVELS_OK,
        ACTION_TYPES.FETCH_NOVELS_NG
    )
    
    // 异步 Action
    export const searchNovels = (url: string) => (
        (dispatch: any) => {
            dispatch(fetchNovels()); // {type: 'FETCH_NOVELS'}
            callApi(url).then(
                json => dispatch(fetchNovelsOK(json)), // {type: 'FETCH_NOVELS_OK', payload: json}
                error => dispatch(fetchNovelsNG(error)) // {type: 'FETCH_NOVELS_NG', payload: error, error: true}
            )
        }
    );
    

    services/api.ts使用fetch api添加通用api调用

    const callApi = (url: string) => {
        return fetch(url)
            .then(response =>
                response.json().then(json => {
                    if (!response.ok) {
                        return Promise.reject(json)
                    }
    
                    return json
                })
            )
    }
    

    Reducer设计

    新建reducers/novel.ts

    import { ACTION_TYPES } from "../constants";
    import { Novel } from "src/types/novel";
    import { handleActions, Action } from "redux-actions";
    
    // 定义管理状态树的结构类型
    type NovelStateType = {
        isLoading: boolean;
        data: Array<Novel>;
    }
    
    // 初始化状态
    export const initialState: NovelStateType = {
        isLoading: false,
        data: [],
    };
    
    // 根据不同的Action生成Reducer
    const novel = handleActions({
        [ACTION_TYPES.FETCH_NOVELS]: (state: NovelStateType) => {
            return { ...state, isLoading: true };
        },
        [ACTION_TYPES.FETCH_NOVELS_OK]: (state: NovelStateType, action: Action<Array<Novel>>) => {
            return {
                ...state,
                data: action.payload,
                loadingFlag: false,
            };
        },
        [ACTION_TYPES.FETCH_NOVELS_NG]: (state: NovelStateType, action: Action<any>) => {
            return { ...state, loadingFlag: false };
        },
    }, initialState);
    
    export default novel;
    

    Reducer 也和 Action 一样,不包括复杂的处理,这里使用了 redux-actions包的 handleActions方法,省略了case 判断的写法,看起来更加优雅。由此可知,每个 Action 都会对应创建 Reducer,而Reducer 可以更新所管理的状态树的一部分或者全部,这都是根据业务需要来定。

    创建 Store

    新建store/configureStore.ts

    import { createStore, applyMiddleware, combineReducers } from 'redux'
    import thunk from 'redux-thunk'
    import { createBrowserHistory } from 'history'
    import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
    import { NovelStateType, novel } from 'src/reducers/novel';
    
    // 浏览器history对象
    export const history = createBrowserHistory();
    // 定义应用程序状态树的结构类型
    export type AppStateType = {
        // 路由状态
        router: RouterState;
        novel: NovelStateType;
    }
    
    // 将各种reducer合并为一个根reducer
    const rootReducer = combineReducers({
        novel,
        // 将路由与浏览器历史关联
        router: connectRouter(history),
    })
    
    // 创建store
    export const store = createStore(
        // 跟reducer
        rootReducer,
        // 应用中间件
        applyMiddleware(
            thunk,
            routerMiddleware(history)
        )
    )
    
    • 通过 connected-react-router 来完成 redux的绑定路由
    • 通过 combineReducers 工具函数合并reducer。
    • 通过 applyMiddleware 来增强 store,比如增加异步处理及路由管理能力。

    applyMiddleware 完成了Action,Reducer不承担的一些额外的副作用,简单理解applyMiddleware其实就是链式的装饰器模式,通过一步步加入对应的 applyMiddleware 来增强store.dispatch。比如状态变化前后的日志功能等。

    可以参照以下格式来自定义 applyMiddleware,next 参数是store的dispatch方法,可以看到本质上就是在保证原有调用的基础上,增加了别的功能。

    /**
     * 记录所有被发起的 action 以及产生的新的 state。
     */
    const logger = store => next => action => {
      console.group(action.type)
      console.info('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      console.groupEnd(action.type)
      return result
    }
    

    实际使用时

    // 创建store
    export const store = createStore(
        // 跟reducer
        rootReducer,
        // 应用中间件
        applyMiddleware(
            thunk,
            routerMiddleware(history),
            logger
        )
    )
    

    有许多现成的中间件可以使用,这里推荐一个记录状态变化日志的中间件 redux-logger。

    执行以下命令

    yarn add redux-logger 
    
    yarn add -D @types/redux-logger 
    

    修改configureStore.ts

    // 状态变化记录中间件
    const loggerMiddleware = createLogger()
    
    // 创建store
    export const store = createStore(
        // 跟reducer
        rootReducer,
        // 应用中间件
        applyMiddleware(
            thunk,
            routerMiddleware(history),
            loggerMiddleware
        )
    )
    

    创建 React 组件

    实现组件前需要简单了解下展示组件和容器组件的区别。

    • 展示组件:描述页面呈现,不与Redux直接作用,数据来源自props,修改数据调用props里的回调函数
    • 容器组件:描述如何运行(数据获取、状态更新),直接使用Redux,监听 Redux state,向 Redux 派发 actions

    展示组件

    首先创建components/NovelList.tsx的展示组件,根据传入的novels数组显示小说列表。

    import * as React from 'react'
    import { Novel } from '../types/novel';
    
    export interface NovelListProps {
        novels: Novel[];
    }
    
    const NovelList: React.SFC<NovelListProps> = (props) => {
        return (<ul>
            {
                props.novels && props.novels.map((novel, index) => {
                    return (
                        <li key={novel.id}>
                            <span>{index}</span>
                            <span>{novel.title}</span>
                            <span>{novel.author}</span>
                            <span>{novel.summary}</span>
                        </li>
                    );
                })
            }
        </ul>);
    }
    
    export default NovelList;
    

    这里有个需要注意的关于 key 属性,这个涉及到UI更新渲染问题,React 依靠 Diff 算法,会根据这个值来判断是否渲染UI,尽可能减少性能开销。用能够唯一标识数据的属性来做 key 是推荐的做法。

    容器组件

    
    import * as React from 'react'
    import NovelList from '../components/NovelList';
    import { useEffect } from "react";
    import { AppStateType } from '../store/configureStore';
    import { searchNovels } from '../actions/novel';
    import { Novel } from '../types/novel';
    import { connect } from 'react-redux';
    
    export interface NovelContainerProps {
        isLoading: boolean;
        data: Array<Novel>;
        // 通过中间件自动注入
        dispatch: any;
    }
    
    const NovelContainer: React.SFC<NovelContainerProps> = (props) => {
        // componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
        // 保持render的纯净,副作用操作都放到渲染之后执行
        useEffect(() => {
            let { dispatch } = props;
            dispatch(searchNovels('test'))
        }, [])
    
        // 加载中UI
        if (!props.isLoading) {
            return (<div>loading...</div>);
        }
        if (props.data && props.data.length == 0) {
            return <div>No Data...</div>
        }
        // 加载完成后UI
        return (<div>
            <NovelList novels={props.data}></NovelList>
        </div>);
    }
    
    // connect生成容器组件
    export default connect(
        // 将state绑定到容器组件props上
        ({ novel }: AppStateType) => novel,
    )(NovelContainer);
    

    我们这里创建的是属于混合控件,它包含了UI定义及Redux操作。这里使用了React16.8 的新增特性Hook,功能类似于componentDidMount,componentDidUpdate 和 componentWillUnmount,只不过它每次重渲染时都会执行。

    App.tsx更新

    class App extends React.Component {
        render() {
            return (
                <Provider store={store}>
                    <ConnectedRouter history={history}>
                        <Header />
                        <Route exact path={PATHS.TOP} component={Top}></Route>
                        <Suspense fallback={<div>Loading...</div>}>
                            <Route exact path={PATHS.ABOUT} component={About} ></Route>
                            <Route exact path={PATHS.NOVELS} component={NovelContainer} ></Route>
                        </Suspense>
                    </ConnectedRouter>
                </Provider>
            );
        }
    }
    

    通过Provider标签将store注入到所有组件属性中,ConnectedRouter将浏览器历史及路由注入到组件属性中。

    至此,基于 Redux 状态管理的 React 程序可以顺利运行了。

    相关文章

      网友评论

        本文标题:05-使用Redux进行状态管理

        本文链接:https://www.haomeiwen.com/subject/oaxtcctx.html