Connected-react-router核心思路实现

connected-react-router

免责声明:这只是一篇我自己使用的学习笔记

本文中出现的history不是window.history,而是react-router依赖的一个第三方库

理论知识

作用:连接路由数据跟仓库,将路由数据与redux仓库挂钩(联动),统一在仓库中进行管理。

至此,我们如果需要使用到路由数据,就只从仓库进行获取即可

首先,明确我们要实现的功能是什么?

  1. 当我的路由数据发生变化时,能够触发一个action导致仓库中路由数据同步该变化

  2. 当我需要手动的在方法中进行路由的跳转时,能够通过派发一个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对象里面的方法)

  1. 使用NavLink、Link,在浏览器中输入网址,前进后退等。

    点击之后路由发生变化,变化后被history的listen方法所捕捉,在listen中派发修改仓库数据的action,从而更新仓库中路由的数据

  2. 在方法中手动触发(如点击一个按钮,在回调函数中跳转)

    点击之后,派发一个由特殊的action,这个特殊的action会被routerMiddleware创建的中间件所拦截,中止action的传递,并且会根据这个action的payload信息,使用history对象对路由进行操作,进入第一种方式的处理逻辑

    这里只提供了push和replace两个例子,但是不难想象,connected-react-routetr中还会提供history对象中其他的方法的特殊action创建函数,实现方法与其二者类似。

相关推荐
凯哥爱吃皮皮虾12 小时前
如何给 react 组件写单测
前端·react.js·jest
每一天,每一步15 小时前
react antd点击table单元格文字下载指定的excel路径
前端·react.js·excel
screct_demo1 天前
詳細講一下在RN(ReactNative)中,6個比較常用的組件以及詳細的用法
javascript·react native·react.js
光头程序员1 天前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me1 天前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者1 天前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS2 天前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
某哈压力大2 天前
基于react-vant实现弹窗搜索功能
前端·react.js
傻小胖2 天前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot2 天前
React的响应式
前端·javascript·react.js