内容大纲 #

1. React路由原理 #

1.1 HashRouter #

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #root{
            border:1px solid red;
        }
    </style>
</head>
<body>
    <div id="root"></div>
    <ul>
        <li><a href="#/a">/a</a></li>
        <li><a href="#/b">/b</a></li>
    </ul>
    <script>
        window.addEventListener('hashchange',()=>{
                        //把最前面的那个# 删除 
            let pathname = window.location.hash.slice(1);
            root.innerHTML = pathname;
        });
    </script>
</body>
</html>

1.2 BrowserRouter #

1.2.1 history #

1.2.1.1 pushState #
1.2.1.2 replaceState #
1.2.1.3 onpopstate #
1.2.1.4 案例 #
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>hash路由</title>
</head>

<body>
  <div id="root"></div>
  <ul>
    <li><a onclick="go('/a')">/a</a></li>
    <li><a onclick="go('/b')">/b</a></li>
    <li><a onclick="go('/c')">/c</a></li>
    <li><a onclick="forward()">前进</a></li>
    <li><a onclick="back()">后退</a></li>
  </ul>
  <script>

    function render() {
      root.innerHTML = window.location.pathname;
    }
    //只有当你前进后退的时候会触发,pushState不会触发
    window.onpopstate = render;
    let historyObj = window.history;
    let oldPushState = historyObj.pushState;
    historyObj.pushState = function (state, title, url) {
      oldPushState.apply(history, arguments);
      render();
    }
    function go(path) {
      historyObj.pushState({}, null, path);
    }
    function forward() {
      historyObj.go(1);
      //historyObj.forward();
    }
    function back(path) {
      historyObj.go(-1);
      //historyObj.back();
    }

  </script>
</body>

</html>

2.使用基本路由 #

2.1 安装 #

npm i react-router-dom --save

2.2 src\index.js #

import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} />
            <Route path="/profile" element={<Profile />} />
        </Routes>
    </BrowserRouter>);

2.3 Home.js #

src\components\Home.js

import React from 'react';
function Home(props) {
    console.log(props);
    return (
        <div>Home</div>
    )
}
export default Home;

2.4 User.js #

src\components\User.js

import React from 'react';
function User() {
    return (
        <div>User</div>
    )
}
export default User;

2.5 Profile.js #

src\components\Profile.js

import React from 'react';
function Profile() {
    return (
        <div>Profile</div>
    )
}
export default Profile;

3.实现基本路由 #

3.1 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router } from '../react-router';
import { createHashHistory, createBrowserHistory } from "@remix-run/router";
export { Routes, Route } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

3.2 react-router\index.js #

src\react-router\index.js

import { Route, Router, Routes } from "./lib/components";
export {
    Route,
    Routes,
    Router
}

3.3 components.js #

src\react-router\lib\components.js

import React from 'react'
import { LocationContext, NavigationContext } from "./context";
import { useRoutes } from "./hooks";
export function Router({ children, location, navigator }) {
    const navigationContext = { navigator };
    const locationContext = { location };
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider children={children} value={locationContext} />
        </NavigationContext.Provider>
    );
}
export function Route() {

}
export function Routes({ children }) {
    let routes = createRoutesFromChildren(children)
    return useRoutes(routes);
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, (element) => {
        let route = {
            element: element.props.element,
            path: element.props.path
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}

3.4 context.js #

src\react-router\lib\context.js

import * as React from "react";
export const NavigationContext = React.createContext(null);
export const LocationContext = React.createContext(null);
export const RouteContext = React.createContext({
    outlet: null,
    matches: []
});

3.5 hooks.js #

src\react-router\lib\hooks.js

import React from 'react'
import { LocationContext } from "./context";

export function useLocation() {
    return React.useContext(LocationContext).location;
}

export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
    for (let i = 0; i < routes.length; i++) {
        let route = routes[i];
        if (route.path === pathname) {
            return (
                route.element
            )
        }
    }
}

4.实现history #

4.1 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router } from '../react-router';
+import { createHashHistory, createBrowserHistory } from "../router";
export { Routes, Route } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

4.2 router\index.js #

src\router\index.js

export * from "./history";

4.3 history.js #

src\router\history.js

export const Action = {
    Pop: "POP",
    Push: "PUSH"
}
const PopStateEventType = "popstate";
let action = Action.Pop;
export function createHashHistory() {
    if (!window.location.hash) {
        window.location.hash = '/';
    }
    function createHashLocation(window, globalHistory) {
        let pathname = window.location.hash.substr(1);
        let state = globalHistory.state || {};
        return { pathname, state: state.usr };
    }
    function createHashHref(to) {
        let url = window.location.href;
        let hashIndex = url.indexOf("#");
        let href = hashIndex === -1 ? url : url.slice(0, hashIndex);
        return href + "#" + to;
    }
    return getUrlBasedHistory(createHashLocation, createHashHref);
}
export function createBrowserHistory() {
    function createBrowserLocation(window, globalHistory) {
        let { pathname } = window.location;
        let state = globalHistory.state || {};
        return { pathname, state: state.usr };
    }
    function createBrowserHref(to) {
        return to;
    }
    return getUrlBasedHistory(createBrowserLocation, createBrowserHref);
}
export function getUrlBasedHistory(getLocation, createHref) {
    let globalHistory = window.history;
    let listener = null;
    let index = getIndex();
    if (index == null) {
        index = 0;
        globalHistory.replaceState({
            ...globalHistory.state,
            idx: index
        }, "");
    }
    function getIndex() {
        let state = globalHistory.state || {
            idx: null
        };
        return state.idx;
    }
    function handlePop() {
        action = Action.Pop;
        let nextIndex = getIndex();
        index = nextIndex;
        if (listener) {
            listener({
                location: history.location
            });
        }
    }
    function push(to, state) {
        action = Action.Push;
        index = getIndex() + 1;
        const url = createHref(to);
        globalHistory.pushState({ usr: state, idx: index }, "", url);
        if (listener) {
            listener({
                location: history.location
            });
        }
    }
    let history = {
        get action() {
            return action;
        },
        get location() {
            return getLocation(window, globalHistory);
        },
        listen(fn) {
            window.addEventListener(PopStateEventType, handlePop);
            listener = fn;
            return () => {
                window.removeEventListener(PopStateEventType, handlePop);
                listener = null;
            };
        },
        push,
        go(n) {
            return globalHistory.go(n);
        }
    };
    return history;
}

5. path-to-regexp #

5.1 /home结束 #

let pathToRegExp = require('path-to-regexp');
let regxp = pathToRegExp('/home',[],{end:true});
console.log(regxp);//   /^\/home\/?$/i
console.log(regxp.test('/home'));
console.log(regxp.test('/home/2'));

homereg

5.2 /home非结束 #

let pathToRegExp = require('path-to-regexp');
let regExp = pathToRegExp('/home',[],{end:false});
console.log(regExp);//   /^\/home\/?(?=\/|$)/i
console.log(regExp.test('/home'));
console.log(regExp.test('/home/'));
console.log(regExp.test('/home//'));
console.log(regExp.test('/home/2'));

homereg2

5.3 路径参数 #

let params = [];
let regExp = pathToRegExp('/user/:id',params,{end:true});
console.log(regExp,params);
/**
/^\/user\/(?:([^\/]+?))\/?$/i
[ { name: 'id', optional: false, offset: 7 } ]
**/

uerreg

5.4 正则匹配 #

表达式 含义
() 表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容)
(?:) 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来
(?...) 表示命名捕获分组,反向引用一个命名分组的语法是 \k,在 replace() 方法的替换字符串中反向引用是用 $
//分组获取
console.log('1ab'.match(/1[a-z]([b-c])/));
//分组不捕获
console.log('1ab'.match(/1[a-z](?:[a-z])/));
//?<x> 表示命名捕获分组
console.log('11-22'.replace(/(?<x>\d{2})-(?<y>\d{2})/,"$<y>-$<x>"));
表达式 含义
(?=pattern) 正向肯定查找(前瞻),后面必须跟着什么
(?!pattern) 正向否定查找(前瞻),后面不能跟着什么
(?<=pattern) 反向肯定条件查找(后顾),不捕获
(?<!pattern) 反向否定条件查找(后顾)
//会消耗掉字符的
//console.log('1a'.match(/\d[a-z][a-z]/));
//?= 正向肯定查找 不消费字符 正向前瞻
//console.log('1a'.match(/\d(?=[a-z])[a-z]/));

//正向肯定前瞻
console.log('1a'.match(/\d(?=[a-z])[a-z]/));
//正向否定前瞻
console.log('1a'.match(/\d(?![A-Z])[a-z]/));
//反向肯定前瞻
console.log('1a'.match(/(?<=[a-z])\d[a-z]/));
//反向否定前瞻
console.log('1a'.match(/(?<![A-Z])\d[a-z]/));

let array = ['1ab'];
array.index = 0;
array.input = '1ab';
array.groups = undefined;
console.log(array);

6. 路径参数 #

6.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, BrowserRouter, Routes, Route } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
+import Post from './components/Post';
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <Routes>
+            <Route path="/post/:id" element={<Post />} />
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} />
            <Route path="/profile" element={<Profile />} />
        </Routes>
    </BrowserRouter>);

6.2 Post.js #

src\components\Post.js

import React from 'react';
function Post() {
    return (
        <div>Post</div>
    )
}
export default Post;

6.3 hooks.js #

src\react-router\lib\hooks.js

import React from 'react'
import { LocationContext } from "./context";
import { matchRoutes } from "../../router";

export function useLocation() {
    return React.useContext(LocationContext).location;
}

export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
+    let match = matchRoutes(routes, { pathname });
+    console.log(match);
+    return match.route.element
}

6.4 router\index.js #

src\router\index.js

export * from "./history";
export { matchRoutes } from "./utils";

6.5 utils.js #

src\router\utils.js

export function matchRoutes(routes, location) {
    let { pathname } = location;
    let match = null;
    for (let i = 0; i < routes.length; ++i) {
        match = matchPath(routes[i].path, pathname);
        if (match) {
            match.route = routes[i];
            return match;
        }
    }
}
export function matchPath(pattern, pathname) {
    let [matcher, paramNames] = compilePath(pattern,true);
    let match = pathname.match(matcher);
    if (!match) return null;
    let captureGroups = match.slice(1);
    let params = paramNames.reduce((memo, paramName, index) => {
        memo[paramName] = captureGroups[index];
        return memo;
    }, {});
    return {
        params
    };
}

function compilePath(path,end) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/\/*\*?$/, "")
        .replace(/^\/*/, "/")
        .replace(/\/:(\w+)/g, (_, paramName) => {
            paramNames.push(paramName);
            return "/([^\\/]+)";
        });
    if (end) {
        regexpSource += "$";
    }    
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}

7. Link导航 #

7.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import { HashRouter, BrowserRouter, Routes, Route, Link } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import Post from './components/Post';
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
+        <ul>
+            <li><Link to="/">首页</Link></li>
+            <li><Link to="/user" >用户管理</Link></li>
+            <li><Link to="/profile" >个人中心</Link></li>
+        </ul>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/post/:id" element={<Post />} />
        </Routes>
    </BrowserRouter>);

7.2 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
+import { Router, useNavigate } from '../react-router';
import { createHashHistory, createBrowserHistory } from "../router";
export { Routes, Route } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

+export const Link = function (props, ref) {
+    const { to, state, ...rest } = props;
+    let navigate = useNavigate();
+    function handleClick(event) {
+        event.preventDefault();
+        navigate(to, state);
+    }
+    return (
+        <a
+            {...rest}
+            onClick={handleClick}
+        />
+    );
+};

7.3 hooks.js #

src\react-router\lib\hooks.js

import React from 'react'
+import { LocationContext, NavigationContext } from "./context";
import { matchRoutes } from "../../router";

export function useLocation() {
    return React.useContext(LocationContext).location;
}

export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
    let match = matchRoutes(routes, { pathname });
    console.log(match);
    return match.route.element
}

+export function useNavigate() {
+    let { navigator } = React.useContext(NavigationContext);
+    let navigate = React.useCallback((to, state) => {
+        navigator.push(to, state);
+    }, [navigator]);
+    return navigate;
+}

7.4 react-router\index.js #

src\react-router\index.js

import { Route, Router, Routes } from "./lib/components";
+import { useNavigate } from "./lib/hooks";
export {
    Route,
    Routes,
    Router,
+    useNavigate
}

8. 使用嵌套路由 #

8.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import { HashRouter, BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
+import UserAdd from './components/UserAdd';
+import UserList from './components/UserList';
+import UserDetail from './components/UserDetail';
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <ul>
            <li><Link to="/">首页</Link></li>
            <li><Link to="/user" >用户管理</Link></li>
            <li><Link to="/profile" >个人中心</Link></li>
        </ul>
        <Routes>
            <Route path="/" element={<Home />} />
+            <Route path="/user" element={<User />} >
+                <Route path="add" element={<UserAdd />} />
+                <Route path="list" element={<UserList />} />
+                <Route path="detail/:id" element={<UserDetail />} />
+            </Route>
        </Routes>
    </BrowserRouter>);

8.2 User.js #

src\components\User.js

import React from 'react';
+import { Link, Outlet } from 'react-router-dom';
function User() {
    return (
+        <div>
+            <ul>
+                <li><Link to="/user/list">用户列表</Link></li>
+                <li><Link to="/user/add">添加用户</Link></li>
+            </ul>
+            <Outlet />
+        </div>
    )
}
export default User;

8.3 utils.js #

src\utils.js

export const UserAPI = {
    list() {
        let usersStr = localStorage.getItem('users');
        let users = usersStr ? JSON.parse(usersStr) : [];
        return users;
    },
    add(user) {
        let users = UserAPI.list();
        users.push(user);
        localStorage.setItem('users', JSON.stringify(users));
    },
    find(id) {
        let users = UserAPI.list();
        return users.find((user) => user.id === id);
    }
}

8.4 UserList.js #

src\components\UserList.js

import React from 'react'
import { Link } from 'react-router-dom';
import { UserAPI } from '../utils';
export default function User() {
    const [users, setUsers] = React.useState([]);
    React.useEffect(() => {
        let users = UserAPI.list();
        setUsers(users);
    }, []);
    return (
        <ul>
            {
                users.map((user, index) => (
                    <li key={index}>
                        <Link to={`/user/detail/${user.id}`} state={user}>{user.name}</Link>
                    </li>
                ))
            }
        </ul>
    )
}

8.5 UserAdd.js #

src\components\UserAdd.js

import React from 'react'
import { useNavigate } from 'react-router-dom';
import { UserAPI } from '../utils';
export default function UserAdd() {
    const navigate = useNavigate();
    const nameRef = React.useRef();
    const handleSubmit = (event) => {
        event.preventDefault();
        let name = nameRef.current.value;
        UserAPI.add({ id: Date.now() + "", name });
        navigate('/user/list');
    }
    return (
        <form onSubmit={handleSubmit}>
            <input type="text" ref={nameRef} />
            <button type="submit">添加</button>
        </form>
    )
}

8.6 UserDetail.js #

src\components\UserDetail.js

import React from 'react'
import { useLocation, useParams } from 'react-router-dom';
import { UserAPI } from '../utils';
export default function UserDetail(props) {
    const location = useLocation();
    const { id } = useParams();
    const [user, setUser] = React.useState({});
    React.useEffect(() => {
        let user = location.state;
        if (!user) {
            if (id) {
                user = UserAPI.find(id);
            }
        }
        if (user) setUser(user);
    }, []);
    return (
        <div>
            {user.id}:{user.name}
        </div>
    )
}

9. 实现嵌套路由 #

9.1 数据结构 #

9.1.1 routes #

[
  {
    "path": "/user",
    "element": "User",
    "children": [
      {
        "path": "add",
        "element": "UserList"
      }
    ]
  }
]

9.1.1 branches #

function flattenRoutes(routes, branches = []) {
    routes.forEach(route => {
        if (route.children && route.children.length > 0) {
            flattenRoutes(route.children, branches);
        }
        branches.push({ name: route.name });
    });
    return branches;
}
let routes = [
    {
        name: 'A',
        children: [
            {
                name: 'B',
                children: [
                    {
                        name: 'C'
                    }
                ]
            }
        ]
    }
]
console.log(flattenRoutes(routes));
[
  {
    path: "/",
    routesMeta: [
      {
        relativePath: "/",
        childrenIndex: 0,
        route: {
          path: "/",
          element: {
            $$typeof: Symbol(react.element),
            type: Home,
            props: {}
          }
        }
      }
    ],
    score: 4
  },
  {
    path: "/profile",
    routesMeta: [
      {
        relativePath: "/profile",
        childrenIndex: 1,
        route: {
          path: "/profile",
          element: {
            $$typeof: Symbol(react.element),
            type: Profile,
            props: {}
          }
        }
      }
    ],
    score: 13
  },
  {
    path: "/user/add",
    routesMeta: [
      {
        relativePath: "user",
        childrenIndex: 2,
        route: {
          path: "user",
          element: {
            $$typeof: Symbol(react.element),
            type: User,
            props: {}
          },
          children: [
            {
              path: "add",
              element: {
                $$typeof: Symbol(react.element),
                type: UserAdd,
                props: {}
              }
            },
            {
              path: "list",
              element: {
                $$typeof: Symbol(react.element),
                type: User,
                props: {}
              }
            },
            {
              path: "detail/:id",
              element: {
                $$typeof: Symbol(react.element),
                type: UserDetail,
                props: {}
              }
            }
          ]
        }
      },
      {
        relativePath: "add",
        childrenIndex: 0,
        route: {
          path: "add",
          element: {
            $$typeof: Symbol(react.element),
            type: UserAdd,
            props: {}
          }
        }
      }
    ],
    score: 24
  },
  {
    path: "/user/list",
    routesMeta: [
      {
        relativePath: "user",
        childrenIndex: 2,
        route: {
          path: "user",
          element: {
            $$typeof: Symbol(react.element),
            type: User,
            props: {}
          },
          children: [
            {
              path: "add",
              element: {
                $$typeof: Symbol(react.element),
                type: UserAdd,
                props: {}
              }
            },
            {
              path: "list",
              element: {
                $$typeof: Symbol(react.element),
                type: User,
                props: {}
              }
            },
            {
              path: "detail/:id",
              element: {
                $$typeof: Symbol(react.element),
                type: UserDetail,
                props: {}
              }
            }
          ]
        }
      },
      {
        relativePath: "list",
        childrenIndex: 1,
        route: {
          path: "list",
          element: {
            $$typeof: Symbol(react.element),
            type: User,
            props: {}
          }
        }
      }
    ],
    score: 24
  },
  {
    path: "/user/detail/:id",
    routesMeta: [
      {
        relativePath: "user",
        childrenIndex: 2,
        route: {
          path: "user",
          element: {
            $$typeof: Symbol(react.element),
            type: User,
            props: {}
          },
          children: [
            {
              path: "add",
              element: {
                $$typeof: Symbol(react.element),
                type: UserAdd,
                props: {}
              }
            },
            {
              path: "list",
              element: {
                $$typeof: Symbol(react.element),
                type: User,
                props: {}
              }
            },
            {
              path: "detail/:id",
              element: {
                $$typeof: Symbol(react.element),
                type: UserDetail,
                props: {}
              }
            }
          ]
        }
      },
      {
        relativePath: "detail/:id",
        childrenIndex: 2,
        route: {
          path: "detail/:id",
          element: {
            $$typeof: Symbol(react.element),
            type: UserDetail,
            props: {}
          }
        }
      }
    ],
    score: 28
  },
  {
    path: "/user",
    routesMeta: [
      {
        relativePath: "user",
        childrenIndex: 2,
        route: {
          path: "user",
          element: {
            $$typeof: Symbol(react.element),
            type: User,
            props: {}
          },
          children: [
            {
              path: "add",
              element: {
                $$typeof: Symbol(react.element),
                type: UserAdd,
                props: {}
              }
            },
            {
              path: "list",
              element: {
                $$typeof: Symbol(react.element),
                type: User,
                props: {}
              }
            },
            {
              path: "detail/:id",
              element: {
                $$typeof: Symbol(react.element),
                type: UserDetail,
                props: {}
              }
            }
          ]
        }
      }
    ],
    score: 13
  },
  {
    path: "/profile",
    routesMeta: [
      {
        relativePath: "/profile",
        childrenIndex: 3,
        route: {
          path: "/profile",
          element: {
            $$typeof: Symbol(react.element),
            type: Protected,
            props: {
              component: Profile
            }
          }
        }
      }
    ],
    score: 13
  },
  {
    path: "/login",
    routesMeta: [
      {
        relativePath: "/login",
        childrenIndex: 4,
        route: {
          path: "/login",
          element: {
            $$typeof: Symbol(react.element),
            type: Login,
            props: {}
          }
        }
      }
    ],
    score: 13
  },
  {
    path: "/*",
    routesMeta: [
      {
        relativePath: "*",
        childrenIndex: 5,
        route: {
          path: "*",
          element: {
            $$typeof: Symbol(react.element),
            type: NotFound,
            props: {}
          }
        }
      }
    ],
    score: 1
  }
]
const branches=[
    {path:'/user/*/add',routesMeta:[user*Meta,addMeta]},
    {path:'/user/*/list',routesMeta:[user*Meta,listMeta]},
    {path:'/user/*/detail',routesMeta:[user*Meta,detailMeta]},
    {path:'/user/*',routesMeta:[user*Meta]},
]

9.1.2 matches #

[
    {
        "pathname": "/user",
        "route": {
            "element": {
                "type": User
            },
            "path": "/user"
        }
    },
    {
        "pathname": "/user/list",
        "route": {
            "element": {
                "type": UserList
            },
            "path": "list"
        }
    }
]

9.1.3 renderMatches #

let element = {
    $$typeof: Symbol(react.element),
    type: {
        $$typeof: Symbol(react.provider)
    },
    props: {
        value: {
            outlet: {
                $$typeof: Symbol(react.element),
                type: {
                    $$typeof: Symbol(react.provider)
                },
                props: {
                    value: {
                        outlet: null,
                        matches: [
                            {
                                params: {},
                                pathname: "/user",
                                route: {
                                    element: {
                                        $$typeof: Symbol(react.element),
                                        type: User,
                                        props: {}
                                    },
                                    path: "/user"
                                }
                            },
                            {
                                params: {},
                                pathname: "/user/list",
                                route: {
                                    element: {
                                        $$typeof: Symbol(react.element),
                                        type: User,
                                        props: {}
                                    },
                                    path: "list"
                                }
                            }
                        ]
                    }
                }
            },
            matches: [
                {
                    params: {},
                    pathname: "/user",
                    route: {
                        element: {
                            $$typeof: Symbol(react.element),
                            type: User,
                            props: {}
                        },
                        path: "/user"
                    }
                }
            ]
        }
    }
}

9.2 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router, useNavigate } from '../react-router';
import { createHashHistory, createBrowserHistory } from "../router";
+export { Routes, Route, Outlet, useLocation, useParams, useNavigate } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    window.browserHistory = history;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    window.hashHistory = history;
    let [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} />;
}

export const Link = function (props) {
    const { to, state, ...rest } = props;
    let navigate = useNavigate();
    function handleClick(event) {
        event.preventDefault();
        navigate(to, state);
    }
    return (
        <a
            {...rest}
            onClick={handleClick}
        />
    );
};

9.3 react-router\index.js #

src\react-router\index.js

+import { Route, Router, Routes, Outlet } from "./lib/components";
+import { useNavigate, useLocation, useParams } from "./lib/hooks";
export {
    Route,
    Routes,
    Router,
    useNavigate,
+    Outlet,
+    useLocation,
+    useParams
}

9.4 components.js #

src\react-router\lib\components.js

import React from 'react'
import { LocationContext, NavigationContext } from "./context";
+import { useRoutes, useOutlet } from "./hooks";
+export function Outlet() {
+    return useOutlet();
+}
export function Router({ children, location, navigator }) {
    const navigationContext = { navigator };
    const locationContext = { location };
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider children={children} value={locationContext} />
        </NavigationContext.Provider>
    );
}
export function Route() {

}
export function Routes({ children }) {
    let routes = createRoutesFromChildren(children)
    return useRoutes(routes);
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, (element) => {
        let route = {
            element: element.props.element,
            path: element.props.path
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}

9.5 hooks.js #

src\react-router\lib\hooks.js

import React from 'react'
import { matchRoutes } from "../../router";
+import { LocationContext, NavigationContext, RouteContext } from "./context";

export function useLocation() {
    return React.useContext(LocationContext).location;
}

export function useRoutes(routes) {
    let location = useLocation();
    let pathname = location.pathname || "/";
+    let matches = matchRoutes(routes, { pathname });
+    if (matches)
+        return renderMatches(matches);
}
+// 导出一个名为 renderMatches 的函数,参数为 renderedMatches
+export function renderMatches(renderedMatches) {
+    // 使用 reduceRight 方法从右到左遍历 renderedMatches 数组,累计器初始值为 null
+    return renderedMatches.reduceRight((outlet, match, index) => {
+        // 获取当前元素之前的子数组,包含当前元素
+        let matches = renderedMatches.slice(0, index + 1);
+        // 返回一个 RouteContext.Provider 组件,将累计值和子数组作为值传递给组件
+        return (
+            <RouteContext.Provider value={{ outlet, matches }}>
+                {match.route.element} //渲染当前 match 对象的 route.element
+            </RouteContext.Provider>
+        );
+    }, null);
+}
export function useNavigate() {
    let { navigator } = React.useContext(NavigationContext);
    let navigate = React.useCallback((to, state) => {
        navigator.push(to, state);
    }, [navigator]);
    return navigate;
}
+export function useOutlet() {
+    let { outlet } = React.useContext(RouteContext);
+    return outlet;
+}
+export function useParams() {
+    let { matches } = React.useContext(RouteContext);
+    let routeMatch = matches[matches.length - 1];
+    return routeMatch ? routeMatch.params : {};
+}

9.6 src\router\utils.js #

src\router\utils.js

+export const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
export function matchRoutes(routes, location) {
    let { pathname } = location;
+    let branches = flattenRoutes(routes);
+    let matches = null;
+    for (let i = 0; matches == null && i < branches.length; ++i) {
+        matches = matchRouteBranch(branches[i], pathname);
+    }
+    return matches;
}
+// 定义一个函数,将嵌套的路由数组扁平化
+function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "") {
+    // 定义一个内部函数,处理单个路由对象
+    let flattenRoute = (route, index) => {
+        // 定义一个元数据对象,存储路由相关信息
+        let meta = {
+            relativePath: route.path,
+            childrenIndex: index,
+            route
+        };
+        // 使用 joinPaths 函数将父路径与当前相对路径组合,生成完整路径
+        let path = joinPaths([parentPath, meta.relativePath]);
+        // 将当前路由元数据添加到父级元数据数组中
+        let routesMeta = parentsMeta.concat(meta);
+        // 如果当前路由对象有子路由,则递归处理
+        if (route.children && route.children.length > 0) {
+            flattenRoutes(route.children, branches, routesMeta, path);
+        }
+        // 将路径和元数据对象添加到结果数组中
+        branches.push({ path, routesMeta });
+    };
+    // 遍历路由数组,调用内部函数处理每个路由对象
+    routes.forEach((route, index) => {
+        flattenRoute(route, index);
+    });
+    // 返回扁平化后的路由数组
+    return branches;
+}
+/**
+ * 定义matchRouteBranch函数,用于匹配路由分支和路径名
+ * @param {*} branch 分支
+ * @param {*} pathname 路径名
+ * @returns 
+ */
+function matchRouteBranch(branch, pathname) {
+    // 从branch中解构routesMeta
+    let { routesMeta } = branch;
+    // 初始化匹配参数、匹配路径名、匹配数组
+    let matchedParams = {};
+    let matchedPathname = "/";
+    let matches = [];
+    // 遍历routesMeta
+    // 遍历routesMeta
+    for (let i = 0; i < routesMeta.length; ++i) {
+        // 获取当前元素的meta
+        let meta = routesMeta[i];
+        // 判断是否为最后一个元素
+        let end = i === routesMeta.length - 1;
+        // 获取剩余路径名
+        let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
+        // 获取当前路径匹配结果
+        let match = matchPath({ path: meta.relativePath, end }, remainingPathname);
+        // 若没有匹配结果,返回null
+        if (!match) return null;
+        // 将当前匹配参数合并到matchedParams
+        Object.assign(matchedParams, match.params);
+        // 获取当前路由
+        let route = meta.route;
+        // 将匹配结果添加到matches数组
+        matches.push({
+            params: matchedParams,
+            pathname: joinPaths([matchedPathname, match.pathname]),
+            route
+        });
+        // 更新匹配路径名
+        matchedPathname = joinPaths([matchedPathname, match.pathname]);
+    }
+    // 返回匹配结果数组
+    return matches;
+}
+export function matchPath({ path, end }, pathname) {
+    let [matcher, paramNames] = compilePath(path, end);
    let match = pathname.match(matcher);
    if (!match) return null;
+    let matchedPathname = match[0];
    let captureGroups = match.slice(1);
    let params = paramNames.reduce((memo, paramName, index) => {
        memo[paramName] = captureGroups[index];
        return memo;
    }, {});
    return {
        params,
+        pathname: matchedPathname
    };
}

+function compilePath(path, end = true) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/\/*\*?$/, "")
        .replace(/^\/*/, "/")
        .replace(/\/:(\w+)/g, (_, paramName) => {
            paramNames.push(paramName);
            return "/([^\\/]+)";
        });
+    if (end) {
+        regexpSource += "$";
+    }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}

10.1 index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
+    <style>
+        .active {
+            color: red;
+        }
+    </style>
</head>
<body>
    <div id="root"></div>
</body>
</html>

10.2 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import { BrowserRouter, Routes, Route, NavLink } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
+const activeStyle = { backgroundColor: 'green' };
+const activeClassName = 'active';
+const activeNavProps = {
+    style: ({ isActive }) => isActive ? activeStyle : {},
+    className: ({ isActive }) => isActive ? activeClassName : ''
+}
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <ul>
+            <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
+            <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
+            <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
        </ul>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} >
                <Route path="add" element={<UserAdd />} />
                <Route path="list" element={<UserList />} />
                <Route path="detail/:id" element={<UserDetail />} />
            </Route>
        </Routes>
    </BrowserRouter>);

10.3 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
+import { Router, useNavigate, useLocation } from '../react-router';
import { createHashHistory, createBrowserHistory } from "../router";
export { Routes, Route, Outlet, useLocation, useParams, useNavigate } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export const Link = function (props, ref) {
    const { to, state, ...rest } = props;
    let navigate = useNavigate();
    function handleClick(event) {
        event.preventDefault();
        navigate(to, state);
    }
    return (
        <a
            {...rest}
            onClick={handleClick}
        />
    );
};

/**
 * @param {*} className 类名 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} end 是否结束
 * @param {*} style 行内样式 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} to 点击导航跳转的路径
 * @param {*} children 子组件
 */
+export function NavLink({ className: classNameProp = '', end = false, style: styleProp = {}, to, children, ...rest }) {
+    let location = useLocation();
+    let path = { pathname: to };
+    let locationPathname = location.pathname;//当前的路径
+    let toPathname = path.pathname;//当前导航想要跳转的路径
+    //如果路径一样,或者 不结束,并且当前的路径是以to开头的,并且下一个字符/,也就是路径路径分隔符
+    let isActive = locationPathname === toPathname || (!end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === '/')
+    let className;
+    if (typeof classNameProp === 'function') {
+        className = classNameProp({
+            isActive
+        });
+    }
+    let style;
+    if (typeof styleProp === 'function') {
+        style = styleProp({
+            isActive
+        });
+    }
+    return (
+        <Link {...rest} to={to} className={className} style={style}>{children}</Link>
+    )
+}

11. 跳转和重定向 #

11.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import { BrowserRouter, Routes, Route, NavLink, Navigate } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
+import Profile from './components/Profile';
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
    style: ({ isActive }) => isActive ? activeStyle : {},
    className: ({ isActive }) => isActive ? activeClassName : ''
}
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <ul>
            <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
            <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
            <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
        </ul>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} >
                <Route path="add" element={<UserAdd />} />
                <Route path="list" element={<UserList />} />
                <Route path="detail/:id" element={<UserDetail />} />
            </Route>
            <Route path="/profile" element={<Profile />} />
+            <Route path="*" element={<Navigate to="/" />} />
        </Routes>
    </BrowserRouter>);

11.2 Home.js #

src\components\Home.js

import React from 'react';
+import { useNavigate } from '../react-router-dom';
function Home(props) {
+    let navigate = useNavigate();
+    function navigateTo() {
+        navigate('/profile');
+    };
    console.log(props);
    return (
+        <div>
+            <p>Home</p>
+            <button onClick={navigateTo}>跳转到/profile</button>
+        </div>
    )
}
export default Home;

11.3 react-router\index.js #

src\react-router\index.js

+import { Route, Router, Routes, Outlet, Navigate } from "./lib/components";
import { useNavigate, useLocation, useParams } from "./lib/hooks";
export {
    Route,
    Routes,
    Router,
    Outlet,
+    Navigate,
    useNavigate,
    useLocation,
    useParams
}

11.4 components.js #

src\react-router\lib\components.js

import React from 'react'
import { LocationContext, NavigationContext } from "./context";
+import { useRoutes, useOutlet, useNavigate } from "./hooks";
export function Router({ children, location, navigator }) {
    const navigationContext = { navigator };
    const locationContext = { location };
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider children={children} value={locationContext} />
        </NavigationContext.Provider>
    );
}
export function Route() {

}
export function Routes({ children }) {
    const routes = createRoutesFromChildren(children);
    return useRoutes(routes);
}
export function createRoutesFromChildren(children) {
    let routes = [];
    React.Children.forEach(children, (element) => {
        let route = {
            element: element.props.element,
            path: element.props.path
        };
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }
        routes.push(route);
    });
    return routes;
}

export function Outlet() {
    return useOutlet();
}
+export function Navigate({ to, state }) {
+    let navigate = useNavigate();
+    React.useEffect(() => {
+        navigate(to, state)
+    });
+    return null;
+}

11.5 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router, useNavigate, useLocation } from '../react-router';
import { createHashHistory, createBrowserHistory } from "../router";
+export { Routes, Route, Outlet, useLocation, useParams, useNavigate, Navigate } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export const Link = function (props, ref) {
    const { to, state, ...rest } = props;
    let navigate = useNavigate();
    function handleClick(event) {
        event.preventDefault();
        navigate(to, state);
    }
    return (
        <a
            {...rest}
            onClick={handleClick}
        />
    );
};

/**
 * @param {*} className 类名 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} end 是否结束
 * @param {*} style 行内样式 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} to 点击导航跳转的路径
 * @param {*} children 子组件
 */
export function NavLink({ className: classNameProp = '', end = false, style: styleProp = {}, to, children, ...rest }) {
    let location = useLocation();
    let path = { pathname: to };
    let locationPathname = location.pathname;//当前的路径
    let toPathname = path.pathname;//当前导航想要跳转的路径
    //如果路径一样,或者 不结束,并且当前的路径是以to开头的,并且下一个字符/,也就是路径路径分隔符
    let isActive = locationPathname === toPathname || (!end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === '/')
    let className;
    if (typeof classNameProp === 'function') {
        className = classNameProp({
            isActive
        });
    }
    let style;
    if (typeof styleProp === 'function') {
        style = styleProp({
            isActive
        });
    }
    return (
        <Link {...rest} to={to} className={className} style={style}>{children}</Link>
    )
}

11.6 utils.js #

src\router\utils.js

export const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
//import { stringifyWithWhitelist } from "stringify-with-whitelist";
export function matchRoutes(routes, location) {
    let { pathname } = location;
    let branches = flattenRoutes(routes);
    //console.log(stringifyWithWhitelist(branches, ['*.path,', '*.routesMeta.*.relativePath', '*.routesMeta.*.route.element.type', '*.routesMeta.*.route.path', '*.routesMeta.*.childrenIndex']));
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; ++i) {
        matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
// 定义一个函数,将嵌套的路由数组扁平化
function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "") {
    // 定义一个内部函数,处理单个路由对象
    let flattenRoute = (route, index) => {
        // 定义一个元数据对象,存储路由相关信息
        let meta = {
            relativePath: route.path,
            childrenIndex: index,
            route
        };
        // 使用 joinPaths 函数将父路径与当前相对路径组合,生成完整路径
        let path = joinPaths([parentPath, meta.relativePath]);
        // 将当前路由元数据添加到父级元数据数组中
        let routesMeta = parentsMeta.concat(meta);
        // 如果当前路由对象有子路由,则递归处理
        if (route.children && route.children.length > 0) {
            flattenRoutes(route.children, branches, routesMeta, path);
        }
        // 将路径和元数据对象添加到结果数组中
        branches.push({ path, routesMeta });
    };
    // 遍历路由数组,调用内部函数处理每个路由对象
    routes.forEach((route, index) => {
        flattenRoute(route, index);
    });
    // 返回扁平化后的路由数组
    return branches;
}
/**
 * 定义matchRouteBranch函数,用于匹配路由分支和路径名
 * @param {*} branch 分支
 * @param {*} pathname 路径名
 * @returns 
 */
function matchRouteBranch(branch, pathname) {
    // 从branch中解构routesMeta
    let { routesMeta } = branch;
    // 初始化匹配参数、匹配路径名、匹配数组
    let matchedParams = {};
    let matchedPathname = "/";
    let matches = [];
    // 遍历routesMeta
    // 遍历routesMeta
    for (let i = 0; i < routesMeta.length; ++i) {
        // 获取当前元素的meta
        let meta = routesMeta[i];
        // 判断是否为最后一个元素
        let end = i === routesMeta.length - 1;
        // 获取剩余路径名
        let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
        // 获取当前路径匹配结果
        let match = matchPath({ path: meta.relativePath, end }, remainingPathname);
        // 若没有匹配结果,返回null
        if (!match) return null;
        // 将当前匹配参数合并到matchedParams
        Object.assign(matchedParams, match.params);
        // 获取当前路由
        let route = meta.route;
        // 将匹配结果添加到matches数组
        matches.push({
            params: matchedParams,
            pathname: joinPaths([matchedPathname, match.pathname]),
            route
        });
        // 更新匹配路径名
        matchedPathname = joinPaths([matchedPathname, match.pathname]);
    }
    // 返回匹配结果数组
    return matches;
}
export function matchPath({ path, end }, pathname) {
    let [matcher, paramNames] = compilePath(path, end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let matchedPathname = match[0];
    let captureGroups = match.slice(1);
    let params = paramNames.reduce((memo, paramName, index) => {
        memo[paramName] = captureGroups[index];
        return memo;
    }, {});
    return {
        params,
        pathname: matchedPathname
    };
}

function compilePath(path, end = true) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/\/*\*?$/, "")
        .replace(/^\/*/, "/")
        .replace(/\/:(\w+)/g, (_, paramName) => {
            paramNames.push(paramName);
            return "/([^\\/]+)";
        });
+    if (path === "*") {
+        paramNames.push("*");
+        regexpSource += "(.*)$";
+    } if (end) {
        regexpSource += "$";
    }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}

12. 受保护路由 #

12.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route, NavLink, Navigate } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
+import Protected from './components/Protected';
+import Login from './components/Login';
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
    style: ({ isActive }) => isActive ? activeStyle : {},
    className: ({ isActive }) => isActive ? activeClassName : ''
}
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <ul>
            <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
            <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
            <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
        </ul>
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/user" element={<User />} >
                <Route path="add" element={<UserAdd />} />
                <Route path="list" element={<UserList />} />
                <Route path="detail/:id" element={<UserDetail />} />
            </Route>
+            <Route path="/profile" element={<Protected component={Profile} path="/profile" />} />
+            <Route path="/login" element={<Login />} />
            <Route path="*" element={<Navigate to="/" />} />
        </Routes>
    </BrowserRouter>);

12.2 Protected.js #

src\components\Protected.js

import React from 'react';
import { Navigate } from '../react-router-dom';
function Protected(props) {
    let { component: RouteComponent, path } = props;
    return localStorage.getItem('login') ? <RouteComponent /> :
        <Navigate to='/login' state={{ from: path }} />
}
export default Protected;

12.3 Login.js #

src\components\Login.js

import React from 'react';
import { useNavigate, useLocation } from '../react-router-dom';
function Login() {
    let navigation = useNavigate();
    let location = useLocation();
    const login = () => {
        localStorage.setItem('login', 'true');
        let to = '/';
        if (location.state) {
            to = location.state.from || '/';
        }
        navigation(to);
    }
    return (
        <button onClick={login}>登录</button>
    )
}
export default Login;

13. 配置式路由和懒加载 #

13.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
+import { BrowserRouter, Routes, Route, NavLink, Navigate, useRoutes } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import UserAdd from './components/UserAdd';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
import Protected from './components/Protected';
import Login from './components/Login';
+import routesConfig from './routesConfig';
+const LazyPost = React.lazy(() => import('./components/Post'));
+function App() {
+    let [routes, setRoutes] = React.useState(routesConfig);
+    const addRoute = () => {
+        setRoutes([...routes, {
+            path: '/post', element: (
+                <React.Suspense fallback={<div>loading...</div>}>
+                    <LazyPost />
+                </React.Suspense>
+            )
+        }]);
+    }
+    return (
+        <div>
+            {useRoutes(routes)}
+            <button onClick={addRoute}>addRoute</button>
+        </div>
+    )
+}
const activeStyle = { backgroundColor: 'green' };
const activeClassName = 'active';
const activeNavProps = {
    style: ({ isActive }) => isActive ? activeStyle : {},
    className: ({ isActive }) => isActive ? activeClassName : ''
}
ReactDOM.createRoot(
    document.getElementById('root')
).render(
    <BrowserRouter >
        <ul>
            <li><NavLink end={true} to="/" {...activeNavProps}>首页</NavLink></li>
            <li><NavLink to="/user/list" {...activeNavProps}>用户管理</NavLink></li>
            <li><NavLink to="/profile" {...activeNavProps}>个人中心</NavLink></li>
+            <li><NavLink to="/post" {...activeNavProps}>post</NavLink></li>
        </ul>
+        <App />
    </BrowserRouter>);

13.2 routesConfig.js #

src\routesConfig.js

import React from 'react';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import UserAdd from './components/UserAdd';
import UserDetail from './components/UserDetail';
import UserList from './components/UserList';
import NotFound from './components/NotFound';
import Login from './components/Login';
import Protected from './components/Protected';
const routes = [
    { path: '/', element: <Home /> },
    { path: '/profile', element: <Profile /> },
    {
        path: 'user',
        element: <User />,
        children: [
            { path: 'add', element: <UserAdd /> },
            { path: 'list', element: <UserList /> },
            { path: 'detail/:id', element: <UserDetail /> }
        ]
    },
    { path: '/profile', element: <Protected component={Profile} /> },
    { path: '/login', element: <Login /> },
    { path: '*', element: <NotFound /> }
];
export default routes;

13.3 NotFound.js #

src\components\NotFound.js

import React from 'react';
function NotFound(props) {
    return (
        <div>NotFound</div>
    )
}
export default NotFound;

13.4 react-router-dom\index.js #

src\react-router-dom\index.js

import React from 'react'
import { Router, useNavigate, useLocation } from '../react-router';
import { createHashHistory, createBrowserHistory } from "../router";
+export { Routes, Route, Outlet, useLocation, useParams, useNavigate, Navigate, useRoutes } from '../react-router';
export function BrowserRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createBrowserHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export function HashRouter({ children }) {
    let historyRef = React.useRef();
    if (historyRef.current == null) {
        historyRef.current = createHashHistory();
    }
    let history = historyRef.current;
    let [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} />;
}

export const Link = function (props, ref) {
    const { to, state, ...rest } = props;
    let navigate = useNavigate();
    function handleClick(event) {
        event.preventDefault();
        navigate(to, state);
    }
    return (
        <a
            {...rest}
            onClick={handleClick}
        />
    );
};

/**
 * @param {*} className 类名 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} end 是否结束
 * @param {*} style 行内样式 可以是固定的字符串,也可以是一个函数,函数的参数是isActive
 * @param {*} to 点击导航跳转的路径
 * @param {*} children 子组件
 */
export function NavLink({ className: classNameProp = '', end = false, style: styleProp = {}, to, children, ...rest }) {
    let location = useLocation();
    let path = { pathname: to };
    let locationPathname = location.pathname;//当前的路径
    let toPathname = path.pathname;//当前导航想要跳转的路径
    //如果路径一样,或者 不结束,并且当前的路径是以to开头的,并且下一个字符/,也就是路径路径分隔符
    let isActive = locationPathname === toPathname || (!end && locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === '/')
    let className;
    if (typeof classNameProp === 'function') {
        className = classNameProp({
            isActive
        });
    }
    let style;
    if (typeof styleProp === 'function') {
        style = styleProp({
            isActive
        });
    }
    return (
        <Link {...rest} to={to} className={className} style={style}>{children}</Link>
    )
}

13.5 react-router\index.js #

src\react-router\index.js

import { Route, Router, Routes, Outlet, Navigate } from "./lib/components";
+import { useNavigate, useLocation, useParams, useRoutes } from "./lib/hooks";
export {
    Route,
    Routes,
    Router,
    Outlet,
    Navigate,
    useNavigate,
    useLocation,
    useParams,
+    useRoutes
}

13.6 utils.js #

src\router\utils.js

export const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
+//如果路径中有*就减少2
+const splatPenalty = -2;
+const indexRouteValue = 2;
+const paramRegexp = /^:\w+$/;
+const dynamicSegmentValue = 3;
+const emptySegmentValue = 1;
+const staticSegmentValue = 10;
+const isSplat = s => s === '*';//console.log(computeScore('/user/*', 1));
+function computeScore(path, index) {
+    // /user/add
+    let segments = path.split('/');//['','user','*']//3
+    let initialScore = segments.length;//3
+    if (segments.some(isSplat)) {
+        initialScore += splatPenalty;//1
+    }
+    if (typeof index !== 'undefined') {
+        initialScore += indexRouteValue;//3
+    }
+    //['','user','*']=>['','user']
+    return segments.filter(s => !isSplat(s)).reduce((score, segment) => {
+        let currentScope = 0;
+        //如果这个片断是路径参数的话 :id
+        if (paramRegexp.test(segment)) {
+            currentScope += dynamicSegmentValue;
+        } else {
+            if (segment === '') {
+                currentScope += emptySegmentValue;
+            } else {
+                currentScope += staticSegmentValue;
+            }
+        }
+        score += currentScope;
+        return score;
+    }, initialScore);
+}
+function rankRouteBranches(branches) {
+    branches.sort((a, b) => a.score !== b.score ? b.score - a.score : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
+}
+function compareIndexes(a, b) {
+    let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
+    return siblings ? a[a.length - 1] - b[b.length - 1] : 0;
+}
export function matchRoutes(routes, location) {
    let { pathname } = location;
    let branches = flattenRoutes(routes);
    rankRouteBranches(branches);
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; ++i) {
        matches = matchRouteBranch(branches[i], pathname);
    }
    return matches;
}
// 定义一个函数,将嵌套的路由数组扁平化
function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "") {
    // 定义一个内部函数,处理单个路由对象
    let flattenRoute = (route, index) => {
        // 定义一个元数据对象,存储路由相关信息
        let meta = {
            relativePath: route.path,
            childrenIndex: index,
            route
        };
        // 使用 joinPaths 函数将父路径与当前相对路径组合,生成完整路径
        let path = joinPaths([parentPath, meta.relativePath]);
        // 将当前路由元数据添加到父级元数据数组中
        let routesMeta = parentsMeta.concat(meta);
        // 如果当前路由对象有子路由,则递归处理
        if (route.children && route.children.length > 0) {
            flattenRoutes(route.children, branches, routesMeta, path);
        }
        // 将路径和元数据对象添加到结果数组中
+        branches.push({ path, routesMeta, score: computeScore(path, route.index) });
    };
    // 遍历路由数组,调用内部函数处理每个路由对象
    routes.forEach((route, index) => {
        flattenRoute(route, index);
    });
    // 返回扁平化后的路由数组
    return branches;
}
/**
 * 定义matchRouteBranch函数,用于匹配路由分支和路径名
 * @param {*} branch 分支
 * @param {*} pathname 路径名
 * @returns 
 */
function matchRouteBranch(branch, pathname) {
    // 从branch中解构routesMeta
    let { routesMeta } = branch;
    // 初始化匹配参数、匹配路径名、匹配数组
    let matchedParams = {};
    let matchedPathname = "/";
    let matches = [];
    // 遍历routesMeta
    // 遍历routesMeta
    for (let i = 0; i < routesMeta.length; ++i) {
        // 获取当前元素的meta
        let meta = routesMeta[i];
        // 判断是否为最后一个元素
        let end = i === routesMeta.length - 1;
        // 获取剩余路径名
        let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
        // 获取当前路径匹配结果
        let match = matchPath({ path: meta.relativePath, end }, remainingPathname);
        // 若没有匹配结果,返回null
        if (!match) return null;
        // 将当前匹配参数合并到matchedParams
        Object.assign(matchedParams, match.params);
        // 获取当前路由
        let route = meta.route;
        // 将匹配结果添加到matches数组
        matches.push({
            params: matchedParams,
            pathname: joinPaths([matchedPathname, match.pathname]),
            route
        });
        // 更新匹配路径名
        matchedPathname = joinPaths([matchedPathname, match.pathname]);
    }
    // 返回匹配结果数组
    return matches;
}
export function matchPath({ path, end }, pathname) {
    let [matcher, paramNames] = compilePath(path, end);
    let match = pathname.match(matcher);
    if (!match) return null;
    let matchedPathname = match[0];
    let captureGroups = match.slice(1);
    let params = paramNames.reduce((memo, paramName, index) => {
        memo[paramName] = captureGroups[index];
        return memo;
    }, {});
    return {
        params,
        pathname: matchedPathname
    };
}

function compilePath(path, end = true) {
    let paramNames = [];
    let regexpSource = "^" + path
        .replace(/\/*\*?$/, "")
        .replace(/^\/*/, "/")
        .replace(/\/:(\w+)/g, (_, paramName) => {
            paramNames.push(paramName);
            return "/([^\\/]+)";
        });
    if (path === "*") {
        paramNames.push("*");
        regexpSource += "(.*)$";
    } if (end) {
        regexpSource += "$";
    }
    let matcher = new RegExp(regexpSource);
    return [matcher, paramNames];
}