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创建函数,实现方法与其二者类似。

相关推荐
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
不是鱼6 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
飞翔的渴望9 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿12 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
哑巴语天雨1 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情1 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起1 天前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱1 天前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js