React Redux最佳实践

1,406 阅读15分钟

前言

redux作为react的状态管理工具,让很多开发者敬而远之,主要是因为它比较繁杂的用法还有各种组成部分,像StoreReducer等。这次毕设恰好用到了redux来进行项目的状态管理,使得程序变得更加优雅,于是趁此机会总结一下。

什么情况下需要用redux

实际上,大多数情况下,我们不需要用到redux,因为实际的应用场景没有复杂到需要用redux

所以,如果你的UI层很简单,没有很多互动,redux就是没有必要的,用了反而会增加复杂性。

那在什么情况下需要用redux呢?在多交互、多数据源的场景下需要用到redux,例如:

  1. 用户的使用方式复杂
  2. 不同身份的用户有不同的使用方式
  3. 多个用户之间可以协作
  4. 与服务器大量交互,或者使用了WebSocket
  5. View要从多个来源获取数据

在我的项目中,考虑使用redux的场景是这样的:

场景:在诗词页面向后台请求诗词的信息,点击诗词页面的某个按钮,会跳转到另一个页面,这个页面也需要用到这个诗词的信息。

常用的做法是:在两个页面componentWillMount生命周期里向后台请求诗词信息。但这种做法的缺点是多次向后台发送请求,会造成页面加载过慢,性能较差等问题。

此时就可以考虑使用redux:在诗词页面向后台请求到诗词的信息时,将这个信息存储在reduxstore中,跳转到另一个页面时,直接从store里获取这个诗词信息即可。这样可以减少http请求的次数。

因此,我们可以根据自己的实际情况选择是否要使用redux

设计思想

redux的设计思想可以总结为:

  1. Web应用时一个状态机,视图与状态是一一对应的。
  2. 所有的状态,保存在一个对象里面。

视图与状态是一一对应意味着:状态改变会导致视图改变。

所有的状态都保存在一个对象里面,这个对象就是之后会提到的Store。

基本概念与API

此处参照阮一峰老师的博客

Store

Store就是保存数据的地方,可以把它看成一个容器,整个应用只能有一个Store。

redux提供createStore这个函数,用来生成Store

import { createStore } from 'redux';
const store = createStore(fn);

State

Store对象包含所有数据。如果想要得到某个时间节点的数据,就要对Store生成快照。这种时间节点的数据集合,就叫做State

redux规定,一个State对应一个View。只要State相同,View就相同。

Action

State的变化,会导致View的变化。但是用户接触不到State,只能接触到View。所以State的变化必须是View导致的,Action就是ViewState发出的通知,表示State要变化了。

Action必须是一个对象,且其中的type属性必须声明,表示Action的名称。其他属性可以自由设置,详情可见社区

Action Creator

Action是一个对象,我们必须要在一开始就定义好这个Actiontypedata,但一般data是在程序运行过程中才获取到(比如从后台获取到数据),赋值给Action,所以可以定义一个函数来生成Action,这个函数就叫做Action Creator

export const changeAudioInfo = (data) => ({
  type: GET_AUDIO_INFO,
  data: data, // 此处可整行简写为data此处属性写成payload或者data都可以,都表示这个action承载的数据
});

store.dispatch()

store.dispatch()View发出Action的唯一方法。

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

store.dispatch接受一个Action对象作为参数,将它发送出去。

结合Action Creator,代码可以写为:

store.dispatch(changeAudioInfo(data));

Reducer

Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer

Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State

import * as actionTypes from './constants';
import { fromJS } from 'immutable';

const defaultState = fromJS({
  poemInfo: {},
  authorInfo: {},
  audioInfo: {},
  like: false,
  collect: false,
});

export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    default:
      return state;
  }
};

Reducer函数不需要手动调用,store.dispatch方法会触发Reducer的自动执行。因此,Store需要知道Reducer函数,做法就是在生成Store的时候,将Reducer传入createStore方法。

import { createStore } from 'redux';
const store = createStore(reducer);

这个函数之所以叫做Reducer,是因为它可以作为数组的reduce方法的参数:

const defaultState = 0;

// state可以看作reduce回调函数的acc,action可以看作reduce回调函数的cur
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const actions = [
  { type: 'ADD', payload: 0 },
  { type: 'ADD', payload: 1 },
  { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

Reducer函数最重要的特征是,它是一个纯函数,同样的输入,必定得到同样的输出。由于Reducer是纯函数,就可以保证同样的State,必定得到同样的View。但也因为这一点,Reducer函数里面不能改变State,必须返回一个全新的对象:

// State 是一个对象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state, action) {
  return [...state, newItem];
}

// 使用immutable数据流的话,会返回新的state对象
export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    default:
      return state;
  }
};

Reducer的拆分

在实际应用中,通常每个组件有自己的Reducer,然后全局将这些Reducer合并后再传入createStore方法:

import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';

export default combineReducers({
  search: searchReducer,
  player: playerReducer,
  poem: poemReducer,
  record: recordReducer,
});

combineReducers()做的就是产生一个整体的Reducer函数。该函数根据Statekey去执行相应的子Reducer,并将返回结果合并成一个大的State对象。

获取全局Store的数据也会根据key值来获取:

const mapStateToProps = (state) => ({
  poemInfo: state.getIn(['poem', 'poemInfo']),
  authorInfo: state.getIn(['poem', 'authorInfo']),
  like: state.getIn(['poem', 'like']),
  collect: state.getIn(['poem', 'collect']),
});

Redux的工作流程

image.png

首先,用户发出Action——> 然后,Store自动调用Reducer,并传入两个参数:当前State和收到的Action——>Reducer会返回新的State——>State一旦有变化,Store就会调用监听函数(store.subscribe(listener))重新渲染View

中间件和异步操作

如果程序中涉及异步操作的话,我们需要使用中间件使得Reducer在异步操作结束后自动执行。

image.png

中间件就是一个函数,对store.dispatch方法进行了改造,在发出Action和执行Reducer这两步之间,添加了其他功能。

中间件的用法

比如添加输出日志功能:

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

这里有两点要注意:

  1. createStore方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware就是第三个参数:
const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);
  1. 中间件的次序有讲究
const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

logger要放在最后。

applyMiddlewaresRedux的原生方法,作用是将所有中间件组成一个数组,依次执行。

redux-thunk中间件

我们先看一个用dispatch发出异步请求的例子:

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    dispatch(fetchPosts(selectedPost))
  }

// ...

这个组件在componentDidMount生命周期里执行dispatch操作,向服务器请求数据fetchPosts(selectedPost)。这里的fetchPosts就是Action Creator

这个fetchPosts``Action Creator是这样的:

const fetchPosts = postTitle => (dispatch, getState) => {
  dispatch(requestPosts(postTitle)); // 先发出一个Action,表示操作开始
  return fetch(`/some/API/${postTitle}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(postTitle, json))); // 再发出一个Action表示操作结束
  };
};

// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

在上面代码中,fetchPosts是一个Action Creator,返回一个函数。然后再函数内部执行异步操作(fetch),获取到异步操作结果后,再通过dispatch发出Action,更新store中变量的值。

上面的代码中,有几点要注意:

  1. fetchPosts返回了一个函数,而普通的Action Creator默认返回一个对象。
  2. 返回的函数的参数是dispatchgetState这两个Redux方法,普通的Action Creator的参数是Action的内容。
  3. 在返回的函数之中,先发出一个Action表示操作开始。
  4. 异步操作结束之后,再发出一个Action表示操作结束。

我们知道,Action是由store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。 因此,这时就要使用中间件redux-thunk

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

使用redux-thunk中间件,改造store.dispatch使得后者可以接受函数作为参数。

React-Redux的用法

React-Redux将所有组件分成两大类:UI组件和容器组件。

UI组件

  • 只负责UI的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何ReduxAPI

因为不含有状态,UI组件又称为“纯组件”,即它和纯函数一样,纯粹由参数决定它的值(参数相同返回的结果也相同)。

容器组件

  • 负责管理数据和业务逻辑,不负责UI的呈现
  • 带有内部状态
  • 使用Redux的API

可以说:UI组件负责UI的呈现,容器组件负责管理数据和逻辑。

如果一个组件既有UI又有业务逻辑的话,我们的做法是,将其拆分成外面是一个容器组件,里面包着UI组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

React-Redux规定,所有的UI组件都由用户提供,容器组件则是由React-Redux自动生成,用户负责视觉层,状态管理则全部交给它。

connect()

React-Redux提供connect方法,用于从UI组件生成容器组件。其完整API如下:

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了UI组件的业务逻辑:

  • mapStateToProps负责输入逻辑,将state映射到UI组件的参数(props
  • mapDispatchToProps负责输出逻辑,即将用户对UI组件的操作映射成Action

具体用法如下:

const mapStateToProps = (state) => ({
  poemInfo: state.getIn(['poem', 'poemInfo']),
  authorInfo: state.getIn(['poem', 'authorInfo']),
  like: state.getIn(['poem', 'like']),
  collect: state.getIn(['poem', 'collect']),
});

const mapDispatchToProps = (dispatch) => {
  return {
    getPoem(poem_id, category) {
      return dispatch(getPoemInfo(poem_id, category)); // dispatch Action Creator
    },
    getAuthor(author_id, category) {
      dispatch(getAuthorInfo(author_id, category));
    },
    getAudio(poem_id, category) {
      return dispatch(getAudioInfo(poem_id, category));
    },
    getDynamic(poem_id, category) {
      dispatch(getDynamicInfo(poem_id, category));
    },
    changeLikeStatus(status) {
      dispatch(changeLike(status));
    },
    changeCollectStatus(status) {
      dispatch(changeCollect(status));
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));

定义好mapStateToProps将其作为参数传入connect以后,在组件Poem中,我们可以从props获取poemInfoauthorInfolikecollect数据,这就体现了输入逻辑。

与此同时,我们也可以在组件Poem中从props获取getPoemgetAuthor等方法,当组件内通过事件触发这些方法时,就可以通过dispatch执行对应的Action,改变store中变量的值,从而进一步从props中获取改变之后的变量值。

  • mapStateToProps是一个函数,建立一个从(外部的)state对象到(UI组件的)props对象的映射关系。它返回一个对象,里面的每一个键值对就是一个映射。
  • mapDispatchToProps可以是函数也可以是对象,一般作为函数来使用,它返回一个对象,该对象的每个键值对都是一个映射,定义了每个方法对应发出怎样的Action。(可以写成键值对的形式,也可以写成上面代码的形式)

Provider组件

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

React-Redux 提供Provider组件,可以让容器组件拿到state

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

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

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。它的原理是React组件的context属性。

React Redux实践

项目中想要使用React-Redux一般遵循以下几个步骤:

安装项目依赖

npm install redux redux-thunk redux-immutable react-redux immutable --save

如果项目中使用到immutable.js中的数据结构,则需要安装redux-immutable,在合并不同模块的reducer的时候需要用到redux-immutable中的方法。

创建store

src目录或者app目录下创建store文件夹,并在其中新建index.jsreducer.js文件。

//reducer.js
import { combineReducers } from 'redux-immutable';

export default combineReducers ({
// 之后开发具体功能模块的时候添加 reducer
});
//index.js
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore (reducer, composeEnhancers (
  applyMiddleware (thunk)
));

export default store;

在项目中注入store

import React from 'react'
import { Provider } from 'react-redux'
import store from './store/index'
import routes from './routes/index.js'

function App () {
  return (
    <Provider store={store}>
        ...
    </Provider>
  )
}

export default App;

为需要使用redux的组件创建各自的store

image.png

// constants.ts
// 定义各种Action
export const GET_CURRENT_POEM = 'poem/GET_CURRENT_POEM';
export const GET_AUTHOR_INFO = 'poem/GET_AUTHOR_INFO';
export const GET_AUDIO_INFO = 'poem/GET_AUDIO_INFO';
export const GET_LIKE = 'poem/GET_LIKE';
export const GET_COLLECT = 'poem/GET_COLLECT';
// actionCreators.ts
// 定义各种Action和Action Creator
import {
  GET_CURRENT_POEM,
  GET_AUTHOR_INFO,
  GET_AUDIO_INFO,
  GET_LIKE,
  GET_COLLECT,
} from './constants';
import { fromJS } from 'immutable';
import {
  getPoemDetail,
  getAuthorDetail,
  getAudio,
  getDynamic,
} from '@servers/servers';

export const changePoemInfo = (data) => ({
  type: GET_CURRENT_POEM,
  data: fromJS(data),
});

export const changeAuthorInfo = (data) => ({
  type: GET_AUTHOR_INFO,
  data: fromJS(data),
});

export const changeAudioInfo = (data) => ({
  type: GET_AUDIO_INFO,
  data: fromJS(data),
});

export const changeLike = (data) => ({
  type: GET_LIKE,
  data: fromJS(data),
});

export const changeCollect = (data) => ({
  type: GET_COLLECT,
  data: fromJS(data),
});

export const getPoemInfo = (poem_id, category) => {
  return (dispatch) => {
    return getPoemDetail(poem_id, category)
      .then((res) => {
        const curPoem = res[0];

        if (category === '0') {
          curPoem.dynasty = 'S';
        } else if (category === '1') {
          curPoem.dynasty = 'T';
        }

        dispatch(changePoemInfo(curPoem));
      })
      .catch((err) => {
        console.error(err);
      });
  };
};

export const getAuthorInfo = (author_id, category) => {
  return (dispatch) => {
    if(author_id === undefined) {
      dispatch(changeAuthorInfo({}));
    }
    getAuthorDetail(author_id, category)
      .then((res) => {
        if (res) {
          dispatch(changeAuthorInfo(res[0]));
        }
      })
      .catch((err) => {
        console.error(err);
      });
  };
};

export const getAudioInfo = (poem_id, category) => {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      getAudio(poem_id, category)
        .then((data: any) => {
          if (data.length > 0) {
            dispatch(changeAudioInfo(data[0]));
            resolve(true);
          } else {
            resolve(false);
          }
        })
        .catch((err) => {
          console.error(err);
        });
    });
  };
};

export const getDynamicInfo = (poem_id, category) => {
  return (dispatch) => {
    getDynamic(poem_id, category)
      .then((res: any) => {
        const { like, collect } = res;
        dispatch(changeLike(like));
        dispatch(changeCollect(collect));
      })
      .catch((err) => {
        console.error(err);
      });
  };
};

// reducer.ts
// 定义defaultState和reducer
import * as actionTypes from './constants';
import { fromJS } from 'immutable';

const defaultState = fromJS({
  poemInfo: {},
  authorInfo: {},
  audioInfo: {},
  like: false,
  collect: false,
});

export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    case actionTypes.GET_AUDIO_INFO:
      return state.set('audioInfo', action.data);
    case actionTypes.GET_LIKE:
      return state.set('like', action.data);
    case actionTypes.GET_COLLECT:
      return state.set('collect', action.data);
    default:
      return state;
  }
};
// index.ts
// 将reducer、actionCreators export出去
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';

export { reducer, actionCreators, constants };

export出去之后,要在全局的store文件夹下的reducer内导入每个组件的reducer,将所有组件的reducer进行合并,才能起到作用:

// store/reducer.ts
import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';

export default combineReducers({
  search: searchReducer,
  player: playerReducer,
  poem: poemReducer,
  record: recordReducer,
});

组件中使用

...
import { connect } from 'react-redux';
...

function Poem(props) {
  ...
  const { poemInfo: poem, authorInfo: author, like, collect } = props; // 获取从mapStateToProps传入的外部(store)的state对象
  const {
    getPoem,
    getAuthor,
    getAudio,
    getDynamic,
    changeLikeStatus,
    changeCollectStatus,
  } = props; // 获取从mapDispatchToProps传入的方法

  let poemInfo = poem ? poem.toJS() : {}; // 对象类型数据需要进行进一步的toJS操作
  let authorInfo = author ? author.toJS() : {};
  ...
}

const mapStateToProps = (state) => ({
  poemInfo: state.getIn(['poem', 'poemInfo']),
  authorInfo: state.getIn(['poem', 'authorInfo']),
  like: state.getIn(['poem', 'like']),
  collect: state.getIn(['poem', 'collect']),
});

const mapDispatchToProps = (dispatch) => {
  return {
    getPoem(poem_id, category) {
      return dispatch(getPoemInfo(poem_id, category));
    },
    getAuthor(author_id, category) {
      dispatch(getAuthorInfo(author_id, category));
    },
    getAudio(poem_id, category) {
      return dispatch(getAudioInfo(poem_id, category));
    },
    getDynamic(poem_id, category) {
      dispatch(getDynamicInfo(poem_id, category));
    },
    changeLikeStatus(status) {
      dispatch(changeLike(status));
    },
    changeCollectStatus(status) {
      dispatch(changeCollect(status));
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));

遇到的复杂场景

之前遇到的场景是这样的:我要通过异步请求获取音频,这个音频想让它存储在全局store中的,所以要通过dispatch操作来发起请求。与此同时,我想在请求完成后,通过获取的数据是否为空来判断这个音频是否存在,存在的话跳转至播放器Player页面,不存在的话就弹出toast提示。

一开始想到的解决方案是:在store中存放一个status变量来表示数据是否为空,获取到数据就dispatch这个status为true。但可能因为dispatch是异步更新的原因,在组件中获取这个从props中传来的变量的值因延迟而有误,无法实现效果。

因为想要在请求完成后去判断数据是否为空,很自然会想到用.then。但如果不做任何改写,直接在方法后添加.then,会报错store.dispatch(...).then is not a function

综上所述,问题可以归结为两点:1. 如何传递请求获取的数据为空这个信息;2. 如何解决store.dispatch(...).then的报错

先看第二个问题,有.then的报错,说明这个方法它不是thenable的,那什么是.thenable的呢?很自然我们会想到Promise。所以我们可以将原本的Action Creator

export const getAudioInfo = (poem_id, category) => {
  return (dispatch) => {
      getAudio(poem_id, category)
        .then((data) => {
          if (data.length > 0) {
            dispatch(changeAudioStatus(true));
            dispatch(changeAudioInfo(data[0]));
            resolve(true);
          } else {
            dispatch(changeAudioStatus(false));
            resolve(false);
          }
        })
        .catch((err) => {
          console.error(err);
        });
  };
};

改写成:

export const getAudioInfo = (poem_id, category) => {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      getAudio(poem_id, category)
        .then((data) => {
          if (data.length > 0) {
            dispatch(changeAudioStatus(true));
            dispatch(changeAudioInfo(data[0]));
          } else {
            dispatch(changeAudioStatus(false));
          }
        })
        .catch((err) => {
          console.error(err);
        });
    });
  };
};

然后在mapDispatchToProps中,将dispatch(xxx)改写成return dispatch(xxx)

const mapDispatchToProps = (dispatch) => {
  return {
    getAudio(poem_id, category) {
      return dispatch(getAudioInfo(poem_id, category));
    },
  };
};

这么写完后,就不会报.then的错误了。(参考:store.dispatch(...).then is not a function

我们再来看第一个问题。我们现在不能够通过在store中存储status变量来传递数据是否存在的信息,因为dispatch异步更新有延迟,在Promise中,我们会使用resolvereject来传递信息,可以在之后的.then回调中获取这个信息。

所以,我们可以通过resolve来传递数据是否存在的信息,存在则resolve(true),不存在则reject(false)

export const getAudioInfo = (poem_id, category) => {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      getAudio(poem_id, category)
        .then((data: any) => {
          if (data.length > 0) {
            dispatch(changeAudioInfo(data[0]));
            resolve(true); // 数据存在
          } else {
            resolve(false); // 数据不存在
          }
        })
        .catch((err) => {
          console.error(err);
        });
    });
  };
};

前端部分通过.then回调传入的参数status来判断是跳转页面还是弹出提示:

const handleListen = () => {
    getAudio(id, category).then((status) => {
      if (status) {
        Taro.navigateTo({
          url: '/pages/Player/index',
        });
      } else {
        setShowToast(true);
      }
    });
  };

总结

React-ReduxReact的状态管理工具,它主要是为了解决在大型项目中,一些状态的共享问题。如果不使用React-Redux,组件之间共享状态只能通过路由或者context来实现,很麻烦。有了React-Redux,一些会在多个组件中使用的变量就可以存储在store中,组件内可以从store中获取这个变量来使用,这么做代码整体结构更优雅,代码可读性也更好。