connected-react-router
免责声明:这只是一篇我自己使用的学习笔记
本文中出现的history不是window.history,而是react-router依赖的一个第三方库
理论知识
作用:连接路由数据跟仓库,将路由数据与redux仓库挂钩(联动),统一在仓库中进行管理。
至此,我们如果需要使用到路由数据,就只从仓库进行获取即可
首先,明确我们要实现的功能是什么?
-
当我的路由数据发生变化时,能够触发一个action导致仓库中路由数据同步该变化
-
当我需要手动的在方法中进行路由的跳转时,能够通过派发一个action去实现
其实直接用history.push也能实现路由的跳转,只不过如果你使用了这个库,意味着你将路由数据交给了仓库进行管理,那么我们就只注焦于仓库,而不去使用原生
参照npm官网中connected-react-router - npm的用法,我们可以知道该库为我们提供了3个东西
- connectRouter :是一个reducer创建函数,返回一个处理路由行为的reducer
- routerMiddleware :是一个中间件创建函数,返回一个中间件
- ConnectedRouter :是一个组件,作用是为子组件们提供一个特定的history对象
这三者有一个共同点:就是它们的都需要同一个history对象作为参数。 至于为什么,后面会说。
下面我说说它们三者的作用是什么?
-
connectRouter
当我们想将路由数据添加到仓库中时,势必需要一个对应的reducer去处理与路由相关的action,connectRouter的作用就是你将一个history对象作为参数传递进去后,它会给你返回一个用于处理仓库路由数据的reducer。
-
routerMiddleware
根据flux的规范,我们改变仓库中数据的流程如下
某个操作->触发了某个action->由仓库将其交给处理器reducer进行统一的处理->得到一个新的状态->更新仓库中的状态
而在这个流程中,我们发现没有哪个流程可以去实现第二个功能,因为导致浏览器路由变化的行为是一个有副作用的操作,而reducer函数和action创建函数又都必须是一个纯函数,因此我们需要想办法去处理这个副作用操作,因此不能直接将action传递给reducer 。这就是routerMiddleware的作用,它会返回一个中间件,通过该中间件拦截特定的action,处理副作用。
-
ConnectedRouter
它本质是对Router进行二次封装,之所以需要它,最重要的原因是我们需要指定Router的history对象,如果直接使用如BrowserRouter,它内部会创建一个新的history对象,这个不是我们想要的结果。
具体代码实现
在开始之前,我们先定义两个action的type类型
js
//actionTypes.js
//"当地址变化后产生的action" 的type类型
export const LOCATION_CHANGE = "@@router/LOCATION_CHANGE";
//"当我们需要手动去跳转时(在方法中实现跳转),会导致调用history对应方法的action" 的type类型
export const CALL_HISTORY_METHOD = "@@router/CALL_HISTORY_METHOD";
然后我们需要一些action创建函数
js
// actionCreators.js
import { CALL_HISTORY_METHOD, LOCATION_CHANGE } from "./actionTypes"
/**
* 创建一个用于地址变化后改变仓库的action
* @param {*} action
* @param {*} location
*/
export function createLocationChangeAction(action, location) {
return {
type: LOCATION_CHANGE,
payload: {
action,
location
}
}
}
//创建一个特殊的action,它对应history中的push方法
export function push(...args) {
return {
type: CALL_HISTORY_METHOD,
payload: {
method: "push",
args
}
}
}
//创建一个特殊的action,它对应history中的replace方法
export function replace(...args) {
return {
type: CALL_HISTORY_METHOD,
payload: {
method: "replace",
args
}
}
}
由于需要使用同一个history对象,我们可以创建一个文件专门用于处理它
js
//history.js
import { createBrowserHistory } from "history"; //这是一个第三方库
export default createBrowserHistory(); // 这是我们所需的history对象
1. connectRouter它的核心实现如下
根据以下代码不难看出,history对象的作用只是为了给仓库提供初始值
js
//connectRouter.js
import { LOCATION_CHANGE } from "./actionTypes";
export default function (history) {
const initial = {
action: history.action, //"POP" "PUSH" "REPLACE"
payload: history.location,
};
return (state = initial, { type, payload }) => {
switch (type) {
case LOCATION_CHANGE:
return payload;
//处理type为CALL_HISTORY_METHOD的action的操作因为带有副作用,因此不在reducer中处理
default:
return state;
}
};
}
ConnectedRouter组件的核心代码如下
这段代码其实也解释了为什么需要使用同一个history对象,因为它使用到了history.listen方法去监听路由的变化,只有是同一个history才能被监听得到。以下代码就是第一个功能的核心实现了。
js
//ConnectedRouter.js
import React, { Component } from "react";
import { Router } from "react-router-dom";
import { ReactReduxContext } from "react-redux";
import { createLocationChangeAction } from "./actionCreators";
// 用法: <ConnectedRouter history={history}>...</ConnectedRouter>
export default class ConnectedRouter extends Component {
static contextType = ReactReduxContext;
componentDidMount = () => {
let history = this.props.history;
history.listen((location, action) => {
//? 当路由发生变化时,触发action更新仓库里面的数据
//* 拿出上下文中的dispatch进行派发action,更新仓库中的数据
// ps1:之所以能从上下文中拿到store的东西,是因为在APP.js那里使用了react-redux的Provider,给上下文注入了store
// ps2: history.listen是后置监听,路由变化后才运行回调函数
let dispatch = this.context.store.dispatch;
dispatch(createLocationChangeAction(action, location));
});
};
render() {
//想理解这下面这部分得去看Router的原码
//react-router-dom中BrowserRouter,HashRouter等的实现核心就是下面这句代码
return <Router history={this.props.history}>{this.props.children}</Router>;
}
}
routerMiddleware的核心代码如下
js
import { CALL_HISTORY_METHOD } from "./actionTypes"
export default history => store => next => action => {
//判断这个action是否是一个特殊的action
if (action.type === CALL_HISTORY_METHOD) {
//如果是,调用history对应的方法
const { method, args } = action.payload;
history[method](...args); //副作用操作
//不再向下传递action
} else {
//如果不是,继续往下传递
next(action);
}
}
下面是其他页面
js
// 使用派发action的方式进行手动跳转,代码如下
import React from 'react'
import { push } from "../../connected-react-router"
import { connect } from "react-redux"
function StudentAdd({ onClick }) {
return (
<div>
<h1>添加学生页</h1>
<button onClick={() => {
onClick && onClick()
}}>点击跳转到课程列表</button>
</div>
)
}
const mapDispatchToProps = dispatch => ({
onClick: () => {
dispatch(push("/courses"))
}
})
export default connect(null, mapDispatchToProps)(StudentAdd)
js
//App.js
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import { Route, Switch } from "react-router-dom";
import { ConnectedRouter } from "./connected-react-router";
import Admin from "./pages/Admin";
import Login from "./pages/Login";
import history from "./store/history";
export default function App() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/login" component={Login} />
<Route path="/" component={Admin} />
</Switch>
</ConnectedRouter>
</Provider>
);
}
总结:
导致仓库中路由数据更新的整体思路如下:
导致路由跳转的方式一般有两种(本质上都是触发了history对象里面的方法)
-
使用NavLink、Link,在浏览器中输入网址,前进后退等。
点击之后路由发生变化,变化后被history的listen方法所捕捉,在listen中派发修改仓库数据的action,从而更新仓库中路由的数据
-
在方法中手动触发(如点击一个按钮,在回调函数中跳转)
点击之后,派发一个由特殊的action,这个特殊的action会被routerMiddleware创建的中间件所拦截,中止action的传递,并且会根据这个action的payload信息,使用history对象对路由进行操作,进入第一种方式的处理逻辑
这里只提供了push和replace两个例子,但是不难想象,connected-react-routetr中还会提供history对象中其他的方法的特殊action创建函数,实现方法与其二者类似。