路由相关的库还有一个connected-react-router,这个库做到了以下几点

  1. 将 router 的数据与 store 同步,并且从 store 访问
  2. 通过 dispatch actions 导航
  3. 在 redux devtools 中支持路由改变的时间旅行调试

使用

配置仓库

import { applyMiddleware, compose, createStore } from 'redux'
// import { routerMiddleware } from 'connected-react-router'
import { routerMiddleware } from '../connected-router-self'
import history from './history'
import reducers from './reducers'
console.log(history) // 一个对象,拥有push、go、back、forward listen等方法
export default function configureStore(preloadedState) {
  const store = createStore(
    reducers, // root reducer with router state
    preloadedState,
    compose(
      applyMiddleware( // appliMiddleware返回一个函数,参数为createStore,返回的函数的作用是将多个中间件串联调用。
        routerMiddleware(history), // for dispatching history actions
        // ... other middlewares ...
      ),
    ),
  )

  return store
}

生成Router

import React from 'react'
import {Provider} from 'react-redux'
import ReactDOM from 'react-dom'
// import {ConnectedRouter} from 'connected-react-router'
import {ConnectedRouter} from './connected-router-self'
import {Route, Link} from 'react-router-dom'
import configureStore from './store/configureStore'
import history from './store/history'

import Counter from './components/Counter'
import Home from './components/Home'
import './app.css'
const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
      <> { /* your usual react-router v4/v5 routing */ }
          <Link to="/">Home</Link>
          <br></br>
          <Link to="/counter">Counter</Link>
          <Route exact={true} path="/" component={Home} />
          <Route path="/counter" component={Counter} />
      </>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
)

action creator

{
  increment(){
      return {type:types.INCREMENT}
  },
  decrement(){
      return {type:types.DECREMENT}
  },
  go(path){
      return push(path)
  }
}

在组件中调用action

onClick = () => {
  this.props.go('/')
}

实现思路

当组件中dispatch action的时候,进入routerMiddleware中间件。该中间件的作用是识别路由操作相关action,执行history暴露的方法,比如push,在hash路由中就会执行window.location.hash = to,进而改变路由。React-Router内部会监听hash变化,重新渲染React组件。

import {CALL_HISTORY_METHOD} from './constant'

export default function routerMiddleware (history) {
  return function ({dispatch, getState}) {
    return function (next) { // 可能是dispatch,也可能是其他中间件最内层的函数
      return function (action) { 
        // 如果是第一个中间件的最内层,会被applyMiddleware执行后作为洋葱的最外层(第一个执行)
        // 如果是最后一个中间件的最内层,会调用next(action)相当于store.dispatch(action)
        // 和上面的dispatch不同的是,如果调用上面的dispatch,相当于再次调用了用中间件增强后的dispatch,会重新执行一遍
        // 所有中间件。上面的dispatch常用于redux-thunk和redux-promise对异步action的处理。
        if (action.type !== CALL_HISTORY_METHOD) {
          return next(action)
        }
        const {method, path} = action.payload
        history[method](path)
      }
    }
  }
}

ConnectedRouter是一个组件,它的作用是在组件渲染完成后监听hash变化,然后将路由数据以及变更的形式存到redux 仓库中。

import React, { Component } from 'react'
import { Router } from 'react-router'
import { LOCATION_CHANGE } from './constant'
import { ReactReduxContext } from 'react-redux'

export default class ConnectedRouter extends Component {
  static contextType = ReactReduxContext
  componentDidMount() {
    // listen的回调函数会在hash变化的时候执行
    this.unlisten = this.props.history.listen((location, action) => {
      this.context.store.dispatch({
        type: LOCATION_CHANGE,
        payload: {
          location,
          action
        }
      })
    })
  }

  componentWillUnmount() {
    this.unlisten()
  }

  render() {
    const { history, children } = this.props
    return (
      <Router history={history}>
        {children}
      </Router>
    )
  }
}

connectRouter是一个函数,返回一个函数,该函数作为redux仓库中路由数据的reducer,进行更新仓库操作。

import {LOCATION_CHANGE} from './constant'

export default function connectRouter (history) {
  let initState = {
    location: history.location,
    action: history.action
  }

  return function (state=initState, action) {
      if (action.type === LOCATION_CHANGE) {
          return {
              location: action.payload.location,
              action: action.payload.action
          }
      }
      return state
  }
}

参考

掘金文章
源码