BrowserRouter
先看一些例子
const App = () => {
return (
<BrowserRouter>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home/>} />
<Route path="/courses" element={<CourseLayout />}>
<Route index element={<CourseIndex/>} />
<Route path="/courses/:id" element={<Course/>} />
</Route>
</Route>
{/* <Route path="*" element={<NotMatch/>} /> */}
</Routes>
</BrowserRouter>
</BrowserRouter>
)
}
源码实现
- 使用
history
库 创建BrowserHistory
- 监听当前
history
变化, 即当 url 中的location 发生变化时, 会重新setState - 讲这些参数传到
<Router />
组件中,<Router />
组件 其实就是包含两个provider
的 context
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window });
}
let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
组件简易实现
传下去的children 就是 Routes
下的children 进行遍历
const Router = (props) => {
const {
basename,
children,
location: locationProps
navigator
} = props;
// 对当前locationProps 做处理,最后返回, 这里我直接用‘’ 表示
const location = {
pathname: '',
search: '',
hash: '',
state: '',
key: '',
}
// navigator 就是 history
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
)
}
- 再来看
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
这里说明一下<Route>
就是一个空方法,但在下面遍历的时候会判断当前组件是不是<Route />
后面也是会直接收集element
属性
createRoutesFromChildren 是对element.props.children
就会进行递归遍历
在遍历过程中会判断当前组件是不是
已上面例子为例: 就会生成以下结果:
[
{
caseSensitive: '',
element: <Layout />,
index: '',
path: '/',
children: [
{
caseSensitive: '',
element: <Home />,
index: true,
path: undefined,
},
{
caseSensitive: '',
element: <CourseLayout />,
index: undefined,
path: "/courses",
children: [
// 略
]
}
]
}
]
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element) => {
if (!React.isValidElement(element)) {
return;
}
if (element.type === React.Fragment) {
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}
// 判断当前children 是不是<Route /> 组件, 如果不是会报错,
invariant(
element.type === Route,
`[${
typeof element.type === "string" ? element.type : element.type.name
}] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`
);
let route: RouteObject = {
caseSensitive: element.props.caseSensitive,
element: element.props.element,
index: element.props.index,
path: element.props.path,
};
if (element.props.children) {
route.children = createRoutesFromChildren(element.props.children);
}
routes.push(route);
});
return routes;
}
在来看 useRoutes 这个hook, 这个hook 生成了一个ReactNode, 是一个 RouterContext.Provider 生成的节点
- 从上面得到的
routes
列表以及url 上的location
可以得到符合当前地址的所有路由,数组顺序为路由的辈分关系
比如上面的/courses
连接, 会得到一下匹配的路由
matches = [ { params: {}, pathname: "/", pathnameBase: "/", route: [ { caseSensitive: '', element: <Layout />, index: '', path: '/', children: [ // 略 ] } ] }, { params: {}, pathname: "/courses", pathnameBase: "/courses", route: [ { caseSensitive: '', element: <CourseLayout />, index: undefined, path: "/courses", children: [ // 略 ] } ] }, { params: {}, pathname: "/courses/", pathnameBase: "/courses", route: [ { caseSensitive: '', element: <CourseIndex />, index: true, path: undefined, } ] } ]
生成节点, 这里的outlet 可以当做children,在官网上可以用
<Outlet />
表示children
- 如果有
element
, 那么用provider
包裹一下,注意:这里是从孙子节点到爷爷节点反序构建节点的
export function _renderMatches( matches: RouteMatch[] | null, parentMatches: RouteMatch[] = [] ): React.ReactElement | null { if (matches == null) return null; return matches.reduceRight((outlet, match, index) => { return ( <RouteContext.Provider children={ match.route.element !== undefined ? match.route.element : outlet } value={{ outlet, matches: parentMatches.concat(matches.slice(0, index + 1)), }} /> ); }, null as React.ReactElement | null); }
- 可以这么理解
// 第一次 let first = ( <RouteContext.Provider> children={ <CourseIndex /> } value={{ outlet: null, matches: matches.slice(0, 3) }} > </RouteContext.Provider> ) // 第二次 let second = ( <RouteContext.Provider> children={ <CourseLayout /> } value={{ outlet: first, matches: matches.slice(0, 2) }} > </RouteContext.Provider> ) // 第三次 let third = ( <RouteContext.Provider> children={ <Layout /> } value={{ outlet: second, matches: matches.slice(0, 1) }} > </RouteContext.Provider> )
- 如果有
- 从上面得到的
在嵌套的路由中渲染 children
- 文档地址
- 嵌套路由中使用
children
, 是使用API
中的<Outlet />
, - 我们知道上面在匹配
matches
的时候, 会进行<RouteContext.Provider />
封装,同时传递outlet
给他的子组件,那么<OutLet />
可以消费他的context
实现
const OutletContext = React.createContext<unknown>(null);
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
if (outlet) {
return (
<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}
return outlet;
}
Link 组件实现
- Link 其实是用
<a />
标签 做的, - 当我们传入自定义
onClick
事件,会在点击时运行,后续会判断一下event.defaultPrevented
, 表明当前事件是否被调用了,如果没有则调用内置internalOnClick
事件
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef(
{ onClick, reloadDocument, replace = false, state, target, to, ...rest },
ref
) {
let href = useHref(to);
let internalOnClick = useLinkClickHandler(to, { replace, state, target });
function handleClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
if (onClick) onClick(event);
if (!event.defaultPrevented && !reloadDocument) {
internalOnClick(event);
}
}
return (
<a
{...rest}
href={href}
onClick={handleClick}
ref={ref}
target={target}
/>
);
}
);
useLinkClickHandler
的实现
其实就是调用了 history.push
或者是 history.replace
做跳转
export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
to: To,
{
target,
replace: replaceProp,
state,
}: {
target?: React.HTMLAttributeAnchorTarget;
replace?: boolean;
state?: any;
} = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
let navigate = useNavigate();
let location = useLocation();
let path = useResolvedPath(to);
return React.useCallback(
(event: React.MouseEvent<E, MouseEvent>) => {
if (
// 表示用户点击了鼠标左键
event.button === 0 &&
(!target || target === "_self") &&
!isModifiedEvent(event)
) {
event.preventDefault();
// 判断一下当前应该用 history.replace 还是 history.push
let replace =
!!replaceProp || createPath(location) === createPath(path);
navigate(to, { replace, state });
}
},
[location, navigate, path, replaceProp, state, target, to]
);
}
useNavigation
实现
一开始 <BrowserHistory />
下的<Router />
使用 <NavigationContext.Provider />
下发 navigator
,
最后返回 navigate
方法,实际上就是 history.replace
或者是 history.push
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;
export function useNavigate(): NavigateFunction {
// 这里拿到 navigator, 实际就是 history, 就是一开始 createBrowserHistory
let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
let routePathnamesJson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
let activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
if (!activeRef.current) return;
if (typeof to === "number") {
navigator.go(to);
return;
}
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
// 最后这里 进行 history.replace 或者是 history.push
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}