引言
实现一个简单的路由系统,首先看下官方用法,最外层使用 BrowserRouter 来包裹,Routes 里面使用 Route 书写 path 和 element 的对应关系,完整代码
// Index.js
import {
Link,
Route,
Routes,
BrowserRouter,
} from "react-router-dom";
const About = () => {
return <div>about</div>;
};
const Version = () => {
return <div>version</div>;
};
const Index = () => {
return (
<BrowserRouter>
<Link to="/about">去关于页面</Link>
<hr />
<Link to="/version">去版本页面</Link>
<hr />
<Routes>
<Route path="/about" element={<About />}></Route>
<Route path="/version" element={<Version />}></Route>
</Routes>
</BrowserRouter>
);
};
export default Index;
实战
需要准备一些物料,history 是对浏览器 history 对象的增强,后续也会用到
"react": "^18.3.1",
"react-dom": "^18.3.1",
"history": "^5.3.0"
分析
其实路由的本质就是,根据当前 path,去路由表里面查找对应的组件,然后执行组件的挂载与销毁,综上所述,需要实现三步
-
实现一个函数,入参为 react 组件,出参是路由表,便于后续查找 path 与 element 的对应关系
-
实现 Routes 组件,根据 path 返回对应的 element,借鉴 useRoutes hook
-
实现 BrowserRouter 组件,主要负责更新界面,派发相应的 history,便于第二步使用
childrenToConfig
先搭建一个框架,代码如下
import React from 'react'
const BrowserRouter = ({ children }) => {
// ...
};
// 第一步,将 dom 变成 config
const childrenToConfig = (tree) => {
const res = [];
// 使用 React 内置的遍历方法
React.Children.forEach(tree, (node) => {
// 记录 path 和 element
const route = {
path: node.props.path,
element: node.props.element,
};
// 递归处理
if (node.props.children) {
route.children = childrenToConfig(node.props.children);
}
res.push(route);
});
// 返回路由表
return res;
};
const Routes = ({ children }) => {
const routeMap = childrenToConfig(children)
// ...
};
const Route = () => {
// ...
};
export { BrowserRouter, Routes, Route };
Routes
Routes 组件可以理解为一个占位符,其永远返回当前 path 下的 element,现在有了路由表,还需要一个函数来根据当前 path,返回对应的 element,称之为 useRoutes
// 第二步,拿到当前 pathname,去路由表里面找对应的 element,返回,useLocation 待实现
const useRoutes = (config) => {
// 假设 useLocation 已经实现
const location = useLocation();
const path = location.pathname || "";
// 递归查找 element
const find = (arr) => {
for (let c of arr) {
if (c.path === path) {
return c.element;
}
if (c.children) {
const el = find(c.children);
if (el) {
return el;
}
}
}
};
return find(config) || null;
}
const Routes = ({ children }) => {
return useRoutes(childrenToConfig(children))
}
BrowserRouter
该组件主要负责维护、派发、更新状态,包括上面的 useLocation,代码如下
const LocationContext = createContext({});
const BrowserRouter = ({ children }) => {
// 防止后续多次创建,使用 useRef 缓存
const historyRef = useRef()
if (!historyRef.current) {
historyRef.current = createBrowserHistory()
}
const history = historyRef.current
// 存储 location
const [state, setState] = useState({
location: history.location
})
// history 变化时,重新渲染
useEffect(() => {
history.listen(setState)
}, [history])
// 这里的 children 就是要渲染的组件,也就是 Routes 组件的返回值
return (
<LocationContext.Provider
value={history}
children={children}
>
</LocationContext.Provider>
)
}
const useLocation = () => useContext(LocationContext)
总结
整体流程如下图所示完整代码如下
// router.js
import React, {
useEffect,
useRef,
useState,
createContext,
useContext,
useMemo,
} from "react";
import { createBrowserHistory } from "history";
const LocationContext = createContext({});
const HistoryContext = createContext({});
const BrowserRouter = ({ children }) => {
const historyRef = useRef();
if (!historyRef.current) {
historyRef.current = createBrowserHistory();
}
const history = historyRef.current;
const [state, setState] = useState({
location: history.location,
});
useEffect(() => {
history.listen(setState);
}, [history]);
return (
<Router children={children} location={state.location} history={history} />
);
};
const Router = ({ children, location, history }) => {
// 缓存 location 和 history 的结果,并且派发 context
const locationValue = useMemo(() => location, [location]);
const historyValue = useMemo(() => history, [history]);
return (
<LocationContext.Provider value={locationValue}>
<HistoryContext.Provider
value={historyValue}
children={children}
></HistoryContext.Provider>
</LocationContext.Provider>
);
};
const useLocation = () => {
return useContext(LocationContext);
};
// 第二步,拿到当前 pathname,去路由表里面找对应的 element,返回
const useRoutes = (config) => {
const location = useLocation();
const path = location.pathname || "";
const find = (arr) => {
for (let c of arr) {
if (c.path === path) {
return c.element;
}
if (c.children) {
const el = find(c.children);
if (el) {
return el;
}
}
}
};
return find(config) || null;
};
// 第一步,将 dom 变成 config,传给 useRoutes
const childrenToConfig = (tree) => {
const res = [];
React.Children.forEach(tree, (node) => {
const route = {
path: node.props.path,
element: node.props.element,
};
if (node.props.children) {
route.children = childrenToConfig(node.props.children);
}
res.push(route);
});
return res;
};
const Routes = ({ children }) => {
return useRoutes(childrenToConfig(children));
};
const Route = () => {};
export { BrowserRouter, Routes, Route };
使用方代码如下
// Index.js
import { BrowserRouter , Routes , Route } from "./router.jsx" ;
const About = () => {
return <div>about</div>;
};
const Version = () => {
return <div>version</div>;
};
const Index = () => {
return (
<BrowserRouter>
<a href="/about">去关于页面</a>
<hr />
<a href="/version">去版本页面</a>
<hr />
<Routes>
<Route path="/about" element={<About />}></Route>
<Route path="/version" element={<Version />}></Route>
</Routes>
</BrowserRouter>
);
};
export default Index;