手写一个简单的react-router6 Part1

前端路由方案 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()方法,我们看看它打印了什么?

http://localhost:5173/product

帮我们匹配到了所有符合条件的路由,然后我们通过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(现在的并不是完整版),我们下一篇文章会通过路由守卫引出。

相关推荐
curdcv_po4 分钟前
🔥🔥🔥结合 vue 或 react,去写three.js
前端·react.js·three.js
猫头_32 分钟前
uni-app 转微信小程序 · 避坑与实战全记录
前端·微信小程序·uni-app
天生我材必有用_吴用35 分钟前
网页接入弹窗客服功能的完整实现(Vue3 + WebSocket预备方案)
前端
海拥41 分钟前
8 Ball Pool:在浏览器里打一局酣畅淋漓的桌球!
前端
Cache技术分享1 小时前
148. Java Lambda 表达式 - 捕获局部变量
前端·后端
YGY Webgis糕手之路1 小时前
Cesium 快速入门(二)底图更换
前端·经验分享·笔记·vue
神仙别闹1 小时前
基于JSP+MySQL 实现(Web)毕业设计题目收集系统
java·前端·mysql
前端李二牛1 小时前
Web字体使用最佳实践
前端·http
YGY_Webgis糕手之路1 小时前
Cesium 快速入门(六)实体类型介绍
前端·gis·cesium
Jacob02341 小时前
UI 代码不写也行?我用 MCP Server 和 ShadCN 自动生成前端界面
前端·llm·ai编程