【源码阅读】| 简易实现React Router(二)

在上一篇文章中,我们已经实现了react-router的核心功能。接下来,我们将继续实现其他功能。

本文主要包括以下内容:

  • 动态路由匹配
  • 路由守卫
  • 懒加载
  • NavLink

动态路由匹配

在实际使用中,经常会用到动态路由。以掘金为例,通常会在路由后面添加id来跳转到相应的详情页。

tsx 复制代码
export default function App(props) {
  return (
    <div className="app">
        <Router>
          <Routes>
              <Route path="product" element={<Product />}>
                <Route path=":id" element={<ProductDetail />} />
              </Route>
          </Routes>
        </Router>
    </div>
  )
}
// Product
function Product() {
  return (
    <div>
      <h1>Product</h1>
      <Link to="/product/123">商品详情</Link>
      <Outlet />
    </div>
  )
}

function ProductDetail() {
  let navigate = useNavigate()
  const params = useParams()
  return (
    <div>
      <h1>ProductDetail</h1>
      <p>{params.id}</p>
      <button onClick={() => navigate('/product')}>go back</button>
    </div>
  )
}

需求分析

● 匹配动态路由,找到相应的元素

● 在对应的组件中可以获取参数

tsx 复制代码
import {matchRoutes } from 'react-router-dom'
export function useRoutes(routes) {
  const location = useLocation()
  const pathname = location.pathname
  // 这里使用了react-router-dom的函数
  // 具体源码
  // https://github.com/remix-run/react-router/blob/7ce38dc49ee997706902ac2d033ba1fd683cfed0/packages/router/utils.ts#L467
  const matches = matchRoutes(routes, { pathname })

  return renderMatches(matches)
}

function renderMatches(matches) {
  if (matches == null) {
    return null
  }

  return matches.reduceRight((outlet, match) => {
    return (
      <RouteContext.Provider
        value={{ outlet, matches }}
        children={match.route.element || outlet}
      />
    )
  }, null)
}
// useLocation
export function useLocation() {
  const { location } = React.useContext(NavigationContext)
  return location
}
// useParams
export function useParams() {
  const { matches } = React.useContext(RouteContext)
  const routeMatch = matches[matches.length - 1]
  return routeMatch ? routeMatch.params : {}
}

通过matchRoutes我们可以得到

useParams取最后一个的params即可

路由守卫

通常来说,我们经常使用路由守卫来完成登录校验

本质还是利用Context来传递

tsx 复制代码
// auth.js
import React from 'react'

const AuthContext = React.createContext()

export function AuthProvider({ children }) {
  const [user, setUser] = React.useState(null)

  const signIn = (newUser, callback) => {
    setUser(newUser)
    callback()
  }

  const signOut = (callback) => {
    setUser(null)
    callback()
  }

  let value = {
    user,
    signIn,
    signOut
  }

  return (
    <AuthContext.Provider
      value={value}
      children={children}
    ></AuthContext.Provider>
  )
}

export function useAuth() {
  return React.useContext(AuthContext)
}
tsx 复制代码
export default function App(props) {
  return (
    <div className="app">
      <AuthProvider>
        <Router>
          <Routes>
            <Route path="/" element={<Layout />}>
              <Route
                path="user"
                element={
                  <RequireAuth>
                    <User />
                  </RequireAuth>
                }
              />
              <Route path="login" element={<Login />} />
            </Route>
          </Routes>
        </Router>
      </AuthProvider>
    </div>
  )
}

// RequireAuth
function RequireAuth({ children }) {
  const auth = useAuth()
  const location = useLocation()
  if (!auth.user) {
    return <Navigate to="/login" state={{ from: location }} replace={true} />
  }
  return children
}
// useAuth
export function useAuth() {
  return React.useContext(AuthContext)
}

懒加载

  • 利用了React 本身的函数实现延迟加载组件

react.dev/reference/r...

tsx 复制代码
const About = React.lazy(() => import("./About"));

<Route
  path="about"
  element={
  <React.Suspense fallback={<h1>Loading...</h1>}>
    <About />
  </React.Suspense>
  }
  />

NavLink和Link非常相似,它们的区别在于NavLink具有active和pending状态,可以显示当前路径的激活样式。

reactrouter.com/en/main/com...

需求分析:

  • 监听当前路由地址
  • 添加相应样式

我们来改进一下已有的Link组件

tsx 复制代码
export default function Link({ to, children, ...rest }) {
  const navigate = useNavigate()
  const handle = (e) => {
    e.preventDefault()
    navigate(to)
  }
  return (
    <a href={to} onClick={handle} {...rest}>
      {children}
    </a>
  )
}

// NavLink
function NavLink({ to, ...rest }) {
  const resolve = useResolvedPath(to)
  const match = useMatch({
    path: resolve.pathname,
    end: true
  })
  return (
    <Link to={to} {...rest} style={{ color: match ? 'red' : 'black' }}></Link>
  )
}

// useNavigate
export function useNavigate() {
  const { navigator } = React.useContext(NavigationContext)

  const navigate = useCallback(
    (to, options = {}) => {
      ;(!!options.replace ? navigator.replace : navigator.push)(
        to,
        options.state
      )
    },
    [navigator]
  )

  return navigate
}

接下来,我们来实现监听对应的路由

  • 利用库的matchPath函数

reactrouter.com/en/main/uti...

tsx 复制代码
// useMatch
export function useMatch(pattern) {
  const { pathname } = useLocation()
  // 缓存
  return React.useMemo(() => matchPath(pattern, pathname), [pathname, pattern])
}

useResolvedPath

● 用于获取实际的路由路径

● 因为NavLink组件可以传递相对路径,所以需要计算出实际路径

reactrouter.com/en/main/uti...

tsx 复制代码
import { parsePath } from 'history'
import { resolvePath } from 'react-router-dom'

export function useResolvedPath(to) {
  // 使用useLocation()获取当前的路径名
  const { pathname: locationPathname } = useLocation()

  // 使用React.useContext(RouteContext)获取RouteContext中的matches
  const { matches } = React.useContext(RouteContext)

  // 将matches中的每个match的pathnameBase转换为JSON字符串,存储在routePathnameJson中
  let routePathnameJson = JSON.stringify(
    matches.map((match) => match.pathnameBase)
  )

  // 使用React.useMemo创建一个记忆化版本的函数
  return React.useMemo(
    () => resolveTo(to, JSON.parse(routePathnameJson), locationPathname),
    [to, routePathnameJson, locationPathname]
  )
}
export function resolveTo(toArg, routePathnames, locationPathname) {
  let to = typeof toArg === 'string' ? parsePath(toArg) : toArg
  // 如果toArg或to.pathname为空字符串,那么toPathname为'/',否则为to.pathname
  let toPathname = toArg === '' || to.pathname === '' ? '/' : to.pathname

  let from
  if (toPathname == null) {
    from = locationPathname
  } else {
    let routePathnameIndex = routePathnames.length - 1

    // 如果toPathname以'..'开头
    if (toPathname.startsWith('..')) {
      // 将toPathname分割成数组toSegments
      let toSegments = toPathname.split('/')

      // 当toSegments的第一个元素为'..'时,删除第一个元素,并将routePathnameIndex减1
      while (toSegments[0] === '..') {
        toSegments.shift()
        routePathnameIndex -= 1
      }

      // 将toSegments重新连接成字符串,赋值给to.pathname
      to.pathname = toSegments.join('/')
    }

    from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : '/'
  }

  let path = resolvePath(to, from)
  if (
    toPathname &&
    toPathname !== '/' &&
    toPathname.endsWith('/') &&
    !path.endsWith('/')
  ) {
    path = path + '/'
  }
  return path
}

至此,我们已经实现了上文中简易实现react-router的剩余功能。

总结

通过阅读源代码,了解流程和react router库的运行方式。优化频繁使用的地方,使用缓存函数。利用Context传递数据状态跨组件传递。

相关推荐
码喽哈哈哈8 分钟前
day01
前端·javascript·html
XT462520 分钟前
解决 npm install 卡住不动或执行失败
前端·npm·node.js
前端小魔女35 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端
mubeibeinv41 分钟前
分页/列表分页
java·前端·javascript
林太白1 小时前
js属性-IntersectionObserver
前端·javascript
爱吃羊的老虎1 小时前
【WEB开发.js】getElementById :通过元素id属性获取HTML元素
前端·javascript·html
lucifer3111 小时前
未集成Jenkins、Kubernetes 等自动化部署工具的解决方案
前端
妙哉7361 小时前
零基础学安全--HTML
前端·安全·html
咔叽布吉1 小时前
【前端学习笔记】AJAX、axios、fetch、跨域
前端·笔记·学习
GISer_Jing2 小时前
Vue3常见Composition API详解(适用Vue2学习进入Vue3学习)
前端·javascript·vue.js