1.生成项目 #

create-react-app zhufeng_connected_router
cd zhufeng_connected_router
npm i redux react-redux  react-router-dom redux-first-history -S

2.跑通项目 #

2.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Routes, Link } from "react-router-dom";
import { HistoryRouter } from "./redux-first-history/rr6";
import { Provider } from 'react-redux';
import { store, history } from "./store";
import Home from './components/Home';
import Counter from './components/Counter';
ReactDOM.render(
  <Provider store={store}>
    <HistoryRouter history={history}>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/counter">Counter</Link></li>
      </ul>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/counter" element={<Counter />} />
      </Routes>
    </HistoryRouter>
  </Provider >,
  document.getElementById('root')
);

2.2 history.js #

src\history.js

import { createBrowserHistory } from 'history';
import { createReduxHistoryContext } from "./redux-first-history";
const history = createBrowserHistory();
const { routerReducer, routerMiddleware, createReduxHistory } = createReduxHistoryContext({ history });
export {
    routerReducer,
    routerMiddleware,
    createReduxHistory
}

2.3 store\index.js #

src\store\index.js

import { createStore, applyMiddleware } from 'redux';
import combinedReducer from './reducers';
import { routerMiddleware, createReduxHistory } from '../history';
//routerMiddleware 可以拦截到 push('/counter') 这个action,调用history进行路径的跳转
export const store = applyMiddleware(routerMiddleware)(createStore)(combinedReducer);
window.store = store;
export const history = createReduxHistory(store);

2.4 action-types.js #

src\store\action-types.js

export const ADD = 'ADD';
export const MINUS = 'MINUS';

2.5 counter.js #

src\store\reducers\counter.js

import * as actionTypes from '../action-types';
function counter(state = { number: 0 }, action) {
    switch (action.type) {
        case actionTypes.ADD:
            return { number: state.number + 1 };
        case actionTypes.MINUS:
            return { number: state.number - 1 };
        default:
            return state;
    }
}
export default counter;

2.6 reducers\index.js #

src\store\reducers\index.js

import { combineReducers } from 'redux';
import counter from './counter';
import { routerReducer } from '../../history';
const reducers = {
    counter,
    router: routerReducer
}
export default combineReducers(reducers);

2.7 Home.js #

src\components\Home.js

import React from 'react';
import { useDispatch } from 'react-redux';
import { push } from "../redux-first-history";
function Home() {
    //const navigate = useNavigate();
    const dispatch = useDispatch();
    const gotoCounter = () => {
        // navigate('/counter');
        dispatch(push('/counter'));
    }
    return (
        <div>
            <p>Home</p>
            <button onClick={gotoCounter}>跳转到/counter</button>
        </div>
    )
}
export default Home;

2.9 Counter.js #

src\components\Counter.js

function Counter() {
    return (
        <div>
            <p>Counter</p>
        </div>
    )
}
export default Counter;

3.实现 #

3.1 redux-first-history\index.js #

src\redux-first-history\index.js

export {
    push,
    CALL_HISTORY_METHOD,
    LOCATION_CHANGE,
} from './actions';
export { createReduxHistoryContext } from './create';

3.2 actions.js #

src\redux-first-history\actions.js

export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD';
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE';
export const locationChangeAction = (location, action) => ({
    type: LOCATION_CHANGE,
    payload: { location, action },
});

function updateLocation(method) {
    return (...args) => ({
        type: CALL_HISTORY_METHOD,
        payload: { method, args },
    });
}

export const push = updateLocation('push');

3.3 create.js #

src\redux-first-history\create.js

import { push, locationChangeAction } from './actions';
import { createRouterMiddleware } from './middleware';
import { createRouterReducer } from './reducer';
export const createReduxHistoryContext = ({ history }) => {
    const routerReducer = createRouterReducer();
    const routerMiddleware = createRouterMiddleware({ history });
    const createReduxHistory = (store) => {
        let registeredCallback = [];
        store.dispatch(locationChangeAction(history.location, history.action));
        history.listen((location, action) => {
            store.dispatch(locationChangeAction(location, action));
            registeredCallback.forEach(listener =>
                listener(location, action),
            );
        });
        return {
            push: (...args) => store.dispatch(push(...args)),
            createHref: history.createHref,
            listen: listener => {
                registeredCallback.push(listener);
                return () => {
                    registeredCallback = registeredCallback.filter(c => c !== listener);
                };
            },
            get location() {
                return store.getState().router.location;
            },
            get action() {
                return store.getState().router.action;
            },
        };
    };

    return { routerReducer, routerMiddleware, createReduxHistory };
};

3.4 middleware.js #

src\redux-first-history\middleware.js

import { CALL_HISTORY_METHOD } from './actions';

export const createRouterMiddleware = ({ history }) =>
    () => (next) => (action) => {
        if (action.type !== CALL_HISTORY_METHOD) {
            return next(action);
        }
        const method = action.payload.method;
        const args = action.payload.args;
        switch (method) {
            case 'push':
                history.push(...(args));
                break;
            default:
                break;
        }
    };

3.5 reducer.js #

src\redux-first-history\reducer.js

import { LOCATION_CHANGE } from './actions';
export const createRouterReducer = () => {
    const initialState = {
        location: null,
        action: null,
    };
    return (state = initialState, { type, payload } = {}) => {
        if (type === LOCATION_CHANGE) {
            const { location, action } = payload || {};
            return { ...state, location, action };
        }
        return state;
    };
};

3.6 rr6\index.js #

src\redux-first-history\rr6\index.js

import React from 'react';
import { Router } from 'react-router';
export function HistoryRouter({ history, children }) {
    const [state, setState] = React.useState({
        action: history.action,
        location: history.location,
    });
    React.useLayoutEffect(() => history.listen(setState), [history]);
    return (
        <Router
            children={children}
            location={state.location}
            navigationType={state.action}
            navigator={history}
        />
    )
}