前端路由方案 React-router6
相信大家应该在写react的时候应该都用过,那么如何自己实现一个简单的react-router呢?我们一步一步往下看。
首先,来看看怎么使用react-router?
下面是一个最基本形态的react-router。以BrowserRouter为例子。
js
export default function App() {
return (
<div className="app">
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route path='/' element={<Home />} />
<Route path="product" element={<Product />} >
{/* <Route path=":id" element={<ProductDetail></ProductDetail>}></Route> */}
</Route>
{/* <Route path="*" element={<NoMatch />} /> */}
</Route>
</Routes>
</BrowserRouter>
</div>
);
}
function Layout() {
return (
<div className="border">
<h3>Layout</h3>
<Link to="/">首页</Link>
<Link to="/product">商品</Link>
{/* <Outlet /> */}
</div>
);
}
function Home() {
return (
<div>
<h1>Home</h1>
</div>
);
}
function Product() {
return (
<div>
<h1>Product</h1>
{/* <Link to="/product/123">商品123</Link>
<Outlet/> */}
</div>
);
}
在第一次使用react-router的时候,会很苦恼为什么要有BrowserRouter,Routes,Route这么多层,每一层都是干什么的,或者经常忘记Outlet诸如此类问题。
那么我们自己试着实现一个路由,看看都做了什么吧。
step1
为了从0开始,我们把其他代码都注释了,先集中精力看Routes,Route,BrowserRouter。
我们可以从Routes 和 Route 开始看,想象Route是一个if条件, <Route path="/" element={<Layout />}>
,如果路径是什么,就渲染什么。那么我们需要一个数据结构构建整个路由的样式,这个数据结构应该方便if,else的判断。 它应该是什么样子的呢?考虑到有嵌套结构,应该是一个嵌套的数组?。应该由谁来负责呢?就是Routes。
看看Routes都做了什么?
js
export default function Routes({children}) {
const routes = createRoutesFromChildren(children)
return useRoutes(routes)
}
export default function createRoutesFromChildren (children) {
const routes = []
React.Children.forEach(children,child=>{
const route = {
element: child.props.element,
path: child.props.path
}
if(child.props.children){
route.children = createRoutesFromChildren(child.props.children)
}
routes.push(route)
})
return routes
}
export function useRoutes(routes){
const path = window.location.pathname
// console.log(routes)
return routes.map((route)=>{
// const match = path === route.path || path === '/' + route.path
const match = path.startsWith(route.path)
//todo 实现子路由 outlet
return match ? route.element:null
})
}
我们看看这个代码,当然这并不是react-router6最终的实现。不要着急,一步一步来。
createRoutesFromChildren就是创造了一个数据结构,我们打印出来看看它做了什么。

不难理解,我们已经创建了结构非常清晰的数组。
useRoutes呢?它决定了要渲染什么?当然这一看就知道不太行,因为我们完全没有考虑嵌套路由的问题,无论如何这是只能渲染出Layout的。没关系,嵌套路由是Outlet做的,我们还没写到。 现在情况是这样,我们至少渲染了第一层。

step2
下一步,我们该尝试一下路由切换的问题了。路由切换是谁来做的呢?BrowserRouter。那又如何传递给后代组件?(useContext)传递什么?一个history对象,未来我们切换路由的时候都用得到。(要知道,BrowserHistory就是基于history API实现的嘛)。
js
const NavigationContext = React.createContext()
export default function Router({navigator,children}){
const navigationContext = React.useMemo(()=>{
return {navigator}
},[navigator])
return (
<NavigationContext.Provider value = {navigationContext}>
{children}
</NavigationContext.Provider>
)
}
export default function BrowserRouter({children}) {
let historyRef = React.useRef()
if(historyRef.current == null){
historyRef.current = createBrowserHistory()
}
const history = historyRef.current
return (
<Router children = {children} navigator={history}></Router>
)
}
重点注意一下为什么用historyRef, 因为我们只是卸载组件的时候才需要清理,你也不想每次render都创建一次吧。现在我们已经把navigator传下去了,那怎么拿到并使用呢?
js
export function useNavigate(){
const {navigator} = React.useContext(NavigationContext)
return navigator.push
}
export default function Link({to,children}){
const navigate = useNavigate()
const handle = (e) => {
e.preventDefault() //自己试试不加会怎么样
navigate(to)
}
return (
<a href={to} onClick={handle}>{children}</a>
)
}
现在我们已经可以通过Link组件进行路由跳转了。当然我们现在还不能渲染子路由。
step3
终于我们要尝试渲染子路由了。我们都知道,子组件路由是需要通过Outlet渲染的。 在实现之前,先解决一下历史问题。现在切换路由还用的是window.loaction,我们自定义一个useLocation hook API,从history对象上拿。
js
export function useLocation(){
const {location} = React.useContext(NavigationContext)
return location
}
export default function BrowserRouter({children}) {
let historyRef = React.useRef()
if(historyRef.current == null){
historyRef.current = createBrowserHistory()
}
const history = historyRef.current
const [state, setState] = useState({location: history.location})
useLayoutEffect(()=>{ //思考为什么是useLayoutEffect?
history.listen(setState)
},[history])
return (
<Router children = {children} navigator={history} location = {state.location}></Router>
)
}
export default function Router({navigator,children,location}){
const navigationContext = React.useMemo(()=>{
return {navigator, location}
},[navigator,location])
return (
<NavigationContext.Provider value = {navigationContext}>
{children}
</NavigationContext.Provider>
)
}
好,现在我们修改了路由切换的方法。我们看看Outlet。怎么做呢?Outlet现在是要渲染children,把children传递给子路由。是不是再来一个context?
js
export default function Outlet(){
return (
useOutlet()
)
}
------------------文件分割-------------------------
const RouterContext = React.createContext()
------------------文件分割--------------------------
import {matchRoutes} from "react-router-dom"
export function useRoutes(routes){
const location = useLocation()
console.log(location)
const pathname = location.pathname
const matches = matchRoutes(routes,{pathname})
console.log(matches)
return renderMatches(matches)
}
function renderMatches(matches){
if(matches == null){
return null
}
return matches.reduceRight((outlet,match)=>{
return(
<RouterContext.Provider
value={{outlet,matches}}
children={match.route.element || outlet}
>
</RouterContext.Provider>)
},null)
}
----------------文件分割-------------------------
export function useOutlet(){
let {outlet} = React.useContext(RouterContext)
return outlet
}
在这里,我们用到了matchRoutes()方法,我们看看它打印了什么?
帮我们匹配到了所有符合条件的路由,然后我们通过reduceRight一层一层套起来,原来是这样。事情到了这里就豁然开朗了,Routes帮我们对路由进行了判断并且返回了最后要渲染的所有组件。
RouterContext.Provider value={{outlet,matches}}
这里传递下来两个值,outlet是渲染子路由的,matches是做什么的? 其实是在动态路由中用来实现useParams的。不多说,看代码,不知道matches是什么的话再看看上面的图。
js
export function useParams(){
const {matches} = React.useContext(RouterContext)
const routeMatch = matches[matches.length - 1]
return routeMatch? routeMatch.params : {}
}
另外这里别忘了加依赖项
js
export default function Router({navigator,children,location}){
const navigationContext = React.useMemo(()=>{
return {navigator, location}
},[navigator,location])
return (
<NavigationContext.Provider value = {navigationContext}>
{children}
</NavigationContext.Provider>
)
}
好,现在我们已经拥有了最基础的路由方案,但是我们还差了很多,包括常用的Navigate组件,useNavigate(现在的并不是完整版),我们下一篇文章会通过路由守卫引出。