在上一篇文章中,我们已经实现了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 本身的函数实现延迟加载组件
tsx
const About = React.lazy(() => import("./About"));
<Route
path="about"
element={
<React.Suspense fallback={<h1>Loading...</h1>}>
<About />
</React.Suspense>
}
/>
NavLink
NavLink和Link非常相似,它们的区别在于NavLink具有active和pending状态,可以显示当前路径的激活样式。
需求分析:
- 监听当前路由地址
- 添加相应样式
我们来改进一下已有的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
函数
tsx
// useMatch
export function useMatch(pattern) {
const { pathname } = useLocation()
// 缓存
return React.useMemo(() => matchPath(pattern, pathname), [pathname, pattern])
}
useResolvedPath
● 用于获取实际的路由路径
● 因为NavLink组件可以传递相对路径,所以需要计算出实际路径
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传递数据状态跨组件传递。