应用connected-react-router和redux-thunk打通react路由孤立

2020-11-20 10:05:16 浏览数 (1)

redux

在我们开发过程中,很多时候,我们需要让组件共享某些数据,虽然可以通过组件传递数据实现数据共享,但是如果组件之间不是父子关系的话,数据传递是非常麻烦的,而且容易让代码的可读性降低,这时候我们就需要一个 state(状态)管理工具。常见的状态管理工具有 redux,mobx,这里选择 redux 进行状态管理。值得注意的是 React 16.3 带来了全新的Context API,我们也可以使用新的 Context API 做状态管理。Redux 是负责组织 state 的工具,但你也要考虑它是否适合你的情况。

在下面的场景中,引入 Redux 是比较明智的:

  • 你有着相当大量的、随时间变化的数据
  • 你的 state 需要有一个单一可靠数据来源
  • 你觉得把所有 state 放在最顶层组件中已经无法满足需要了

的确,这些场景很主观笼统。因为对于何时应该引入 Redux 这个问题,对于每个使用者和每个应用来说都是不同的。

对于 Redux 应该如何、何时使用的更多建议,请看:“您可能不需要Redux” Redux之道,第1部分-实现和意图 Redux之道,第2部分-实践与哲学 Redux 常见问题

Redux 的创造者 Dan Abramov 又补充了一句 "只有遇到 React 实在解决不了的问题,你才需要 Redux 。"

react-redux

react-redux 提供Provider组件通过 context 的方式向应用注入 store,然后组件使用connect高阶方法获取并监听 store,然后根据 store state 和组件自身的 props 计算得到新的 props,注入该组件,并且可以通过监听 store,比较计算出的新 props 判断是否需要更新组件。

代码语言:txt复制
render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)

整合 redux 到 react 应用

合并 reducer

在一个 react 应用中只有一个 store,组件通过调用 action 函数,传递数据到 reducer,reducer 根据数据更改对应的 state。但是随着应用复杂度的提升,reducer 也会变得越来越大,此时可以考虑将 reducer 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。 redux 提供combineReducers辅助函数,将分散的 reducer 合并成一个最终的 reducer 函数,然后在 createStore 的时候使用。

整合 middleware

有时候我们需要多个 middleware 组合在一起形成 middleware 链来增强store.dispatch,在创建 store 时候,我们需要将 middleware 链整合到 store 中,官方提供applyMiddleware(...middleware)将 middleware 链在一起。

整合 store enhancer

store enhancer 用于增强 store,如果我们有多个 store enhancer 时需要将多个 store enhancer 整合,这时候就会用到compose(...functions)。 使用compose合并多个函数,每个函数都接受一个参数,它的返回值将作为一个参数提供给它左边的函数以此类推,最右边的函数可以接受多个参数。compose(funA,funB,funC)可以理解为compose(funA(funB(funC()))),最终返回从右到左接收到的函数合并后的最终函数。

创建 Store

redux 通过createStore创建一个 Redux store 来以存放应用中所有的 statecreateStore的参数形式如下:

代码语言:txt复制
createStore(reducer, [preloadedState], enhancer)

所以我们创建 store 的代码如下:

代码语言:txt复制
import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'

import reducers from '../reducers'

const initialState = {}

const store = createStore(reducers, initialState, applyMiddleware(thunk))

export default store

之后将创建的 store 通过Provider组件注入 react 应用即可将 redux 与 react 应用整合在一起。 注:应用中应有且仅有一个 store

redux与react-router

React Router 与 Redux 一起使用时大部分情况下都是正常的,但是偶尔会出现路由更新但是子路由或活动导航链接没有更新。这个情况发生在:

  1. 组件通过connect()(Comp)连接 redux。
  2. 组件不是一个“路由组件”,即组件并没有像<Route component={SomeConnectedThing} />这样渲染。

这个问题的原因是 Redux 实现了shouldComponentUpdate,当路由变化时,该组件并没有接收到 props 更新。 解决这个问题的方法很简单,找到connect并且将它用withRouter包裹:

代码语言:txt复制
// before
export default connect(mapStateToProps)(Something)
// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))

注意 ! ! : 需要注意:withRouter 只是用来处理数据更新问题的。在使用一些 redux 的connect()或者 mobx的inject()的组件中,如果依赖于路由的更新要重新渲染,会出现路由更新了但是组件没有重新渲染的情况。这是因为 redux 和 mobx 的这些连接方法会修改组件的shouldComponentUpdate。 所以在使用 withRouter 解决更新问题的时候,一定要保证 withRouter 在最外层,比如withRouter(connect()(Component)),而不是 connect()(withRouter(Component))

React Router

将 redux 与 react-router 深度整合

有时候我们可能希望将 redux 与 react router 进行更深度的整合,实现:

  • 将 router 的数据与 store 同步,并且从 store 访问
  • 通过 dispatch actions 导航
  • 在 redux devtools 中支持路由改变的时间旅行调试集成好处:1)路由信息可以同步到统一的 store 并可以从中获得 2)可以使用 Redux 的 dispatch action 来导航 3)集成 Redux 可以支持在 Redux devtools 中路由改变的时间履行调试集成的必要性:集成后允许 react router 的路由信息可以存到 redux ,所以就需要路由组件要能访问到 redux store,这样组件就可以使用 store 的 dispatch action,可以使用 dispatch 带上路由信息作为 action 的负载将路由信息存到 store,同时要能将路由信息从 Redux store 里面同步获取出来

这些可以通过 react-router-reduxconnected-react-routerhistory 两个库将 react-routerredux 进行深度整合实现。

官方文档中提到的是 react-router-redux,并且它已经被整合到了 react-router v4 中,但是根据 react-router-redux 的文档,该仓库不再维护,推荐使用 connected-react-router。

create-react-app中使用安装所需中间件:

代码语言:txt复制
yarn add connected-react-router history redux react-redux redux-devtools-extension react-router-dom

然后给 store 添加如下配置:

  • 创建history对象,因为我们的应用是浏览器端,所以使用createBrowserHistory创建
  • 使用connectRouter包裹 root reducer 并且提供我们创建的history对象,获得新的 root reducer
  • 使用routerMiddleware(history)实现使用 dispatch history actions,这样就可以使用push('/path/to/somewhere')去改变路由(这里的 push 是来自 connected-react-router 的)
代码语言:txt复制
history.js

import * as createHistory from 'history'
const history = createHistory.createBrowserHistory()

export default history
代码语言:txt复制
store.js

import thunk from 'redux-thunk'
import { createBrowserHistory } from 'history'
import { createStore, applyMiddleware } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'
import reducers from '../reducers'
export const history = createBrowserHistory()
const initialState = {}
const store = createStore(
  connectRouter(history)(reducers),
  initialState,
  applyMiddleware(thunk, routerMiddleware(history))
)
export default store

在根组件中,我们添加如下配置:

  • 使用ConnectedRouter包裹路由,并且将 store 中创建的history对象引入,作为 props 传入应用
  • ConnectedRouter组件要作为Provider的子组件
代码语言:txt复制
index.js 

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import App from './App'
import store from './redux/store'
import { history } from './redux/store'
render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)

这样我们就将 redux 与 react-router 整合完毕。

使用dispatch切换路由

完成以上配置后,就可以使用dispatch切换路由了:

代码语言:txt复制
import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
最终结果如下:image.pngimage.png

异步任务流管理

实现异步操作的思路

大部分情况下我们的应用中都是同步操作,即 dispatch action 时,state 会被立即更新,但是有些时候我们需要做异步操作。同步操作只要发出一种 Action 即可,但是异步操作需要发出三种 Acion。

  • 操作发起时的 Action
  • 操作成功时的 Action
  • 操作失败时的 Action

为了区分这三种 action,可能在 action 里添加一个专门的status字段作为标记位:

代码语言:txt复制
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

或者为它们定义不同的 type:

代码语言:txt复制
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

所以想要实现异步操作需要做到:

  • 操作开始时,发出一个 Action,触发 State 更新为“正在操作”,View 重新渲染componentDidMount() { store.dispatch(fetchPosts()) }在组件加载成功后,送出一个 Action 用来请求数据,这里的fetchPosts就是 Action Creator。fetchPosts 代码如下:
  • 操作结束后,再发出一个 Action,触发 State 更新为“操作结束”,View 再次重新渲染redux-thunk异步操作至少送出两个 Action,第一个 Action 跟同步操作一样,直接送出即可,那么如何送出第二个 Action 呢? 我们可以在送出第一个 Action 的时候送一个 Action Creator 函数,这样第二个 Action 可以在异步执行完成后自动送出。
代码语言:txt复制
export const SET_DEMO_DATA = createActionSet('SET_DEMO_DATA')
export const fetchPosts = () => async (dispatch, getState) => {
  store.dispatch({ type: SET_DEMO_DATA.PENDING })
  await axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response => store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }))
    .catch(err => store.dispatch({ type: SET_DEMO_DATA.ERROR, payload: err }))
}

fetchPosts是一个 Action Creator,执行返回一个函数,该函数执行时dispatch一个 action,表明马上要进行异步操作;异步执行完成后,根据请求结果的不同,分别dispatch不同的 action 将异步操作的结果返回回来。 这里需要说明几点:

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

但是有一个问题,store.dispatch正常情况下,只能发送对象,而我们要发送函数,为了让store.dispatch可以发送函数,我们使用中间件——redux-thunk。 引入 redux-thunk 很简单,只需要在创建 store 的时候使用applyMiddleware(thunk)引入即可。

开发调试工具

开发过程中免不了调试,常用的调试工具有很多,例如 redux-devtools-extensionredux-devtoolsstorybook 等。

注意,从2.7开始,window.devToolsExtension重命名为window.__REDUX_DEVTOOLS_EXTENSION__window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__.

redux-devtools-extension

redux-devtools-extension 是一款调试 redux 的工具,用来监测 action 非常方便。 首先根据浏览器在Chrome Web Store或者Mozilla Add-ons中下载该插件。

  • store高级用法 如果store使用了中间件middleware和增强器enhaners,代码要修改下:
代码语言:txt复制
import { createStore, applyMiddleware, compose } from 'redux';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
    reducer, /* preloadedState, */ 
    composeEnhancers(
        applyMiddleware(...middleware)
  ));
  • 当有特殊扩展选项时,用这么使用:
代码语言:txt复制
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
  // 有指定扩展选项,像name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
    applyMiddleware(...middleware),
    // 其他store增强器(如果有的话)
);
const store = createStore(reducer, enhancer);
  • 使用redux-devtools-extension包 为了简化操作需要安装个npm包
代码语言:txt复制
npm install --save-dev redux-devtools-extension

使用

代码语言:txt复制
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(reducer, 
    composeWithDevTools(
        applyMiddleware(...middleware),
        // 其他store增强器(如果有的话)
));
  • 指定扩展名选项:
代码语言:txt复制
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({
  // 如果需要,在这里指定名称,actionsBlacklist,actionsCreators和其他选项
});
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
  applyMiddleware(...middleware),
  // 其他store增强器(如果有的话)
));
  • 如果你没有包含其它增强器和中间件的话,只需要使用devToolsEnhancer
代码语言:txt复制
import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer(
  // 需要的话,在这里指定名称,actionsBlacklist,actionsCreators和其他选项
));
  • 在生产环境中使用 这个扩展在生产环境也是有用的,但一般都是在开发环境中使用它。 如果你想限制它的使用,可以用 redux-devtools-extension/logOnlyInProduction
代码语言:txt复制
import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction';
const store = createStore(reducer, /* preloadedState, */         devToolsEnhancer(
    // actionSanitizer, stateSanitizer等选项
));
  • 使用中间件和增强器时:
代码语言:txt复制
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
const composeEnhancers = composeWithDevTools({
  // actionSanitizer, stateSanitizer选项
});
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
  applyMiddleware(...middleware),
  // 其它增强器
));

你将不得不在webpack的生产环境打包配置中加上process.env.NODE_ENV': JSON.stringify('production')。如果你用的是create-react-app,那么它已经帮你配置好了

  • 如果你在创建store时检查过process.env.NODE_ENV,那么也包括了生产环境的redux-devtools-extension/logOnly 如果不想在生产环境使用扩展,那就只开启redux-devtools-extension/developmentOnly就好点击文章查看更多细节
代码语言:txt复制
import thunk from "redux-thunk";
import { createBrowserHistory } from "history";
import { createStore, applyMiddleware } from "redux";
  import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
import { connectRouter, routerMiddleware } from "connected-react-router";
import reducers from "../reducers";
export const history = createBrowserHistory();
const initialState = {};
   const composeEnhancers = composeWithDevTools({
    // options like actionSanitizer, stateSanitizer
  });
const store = createStore(
  connectRouter(history)(reducers),
  initialState,
   composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
);

关于怎么使用体系结构的扩展,请参考以下集合链接和博客文章

结尾

Store 跟 Router 必須使用同一个 history 物件,否則会有其中一方不能正常工作,如果以后有遇到必須要先检查一次才行,记录一下。针对以上操作尝试梳理了一个简单demo大家可以查看github。 如果你有任何想法欢迎直接「留言

0 人点赞