挑选和创建router
react-router共支持5种router类型,一个项目只支持使用一种router类型。
- 在web项目里,常用的有两种historyRouter、hashRouter。
- memoryRouter(单元测试用?或者使用DOM history APIs进行路由管理的项目使用?没有考证过)
- nativeRouter(React Native项目使用)
react-router中创建router有两种方式
createBrowerRouter<BrowserRouter>
两者的区别在于:
<BrowserRouter>只能将router配置使用jsx、tsx形式以children的形式传入。createBrowerRouter可以使用RouterObject的形式,也可以使用官方提供的createRoutesFromElements方法,将jsx转换为object。另外,createBrowerRouter支持react-router的一些data apis,如:lazy、loader等。
// createRoutesFromElements
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="dashboard" element={<Dashboard />} />
{/* ... etc. */}
</Route>
)
);
// jsx
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "team",
element: <Team />,
},
],
},
]);
// 使用递归处理route的层级结构,创建object对象,提取route里props,赋值为routeObject
export function createRoutesFromChildren(
children: React.ReactNode,
parentPath: number[] = []
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element, index) => {
let treePath = [...parentPath, index];
if (element.type === React.Fragment) {
// support React.Fragment
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children, treePath)
);
return;
}
let route: RouteObject = {
id: element.props.id || treePath.join("-"),
caseSensitive: element.props.caseSensitive,
element: element.props.element,
Component: element.props.Component,
index: element.props.index,
path: element.props.path,
loader: element.props.loader,
action: element.props.action,
errorElement: element.props.errorElement,
ErrorBoundary: element.props.ErrorBoundary,
hasErrorBoundary:
element.props.ErrorBoundary != null ||
element.props.errorElement != null,
shouldRevalidate: element.props.shouldRevalidate,
handle: element.props.handle,
lazy: element.props.lazy,
};
if (element.props.children) {
route.children = createRoutesFromChildren(
element.props.children,
treePath
);
}
routes.push(route);
});
return routes;
}
createBrowserRouter
web中常用的两种router,创建时都是调用的createRouter方法,执行initialize方法,区别是history的创建方法不同。
export function createBrowserRouter(
routes: RouteObject[],
opts?: DOMRouterOpts
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: {
...opts?.future,
v7_prependBasename: true,
},
history: createBrowserHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
window: opts?.window,
}).initialize();
}
export function createHashRouter(
routes: RouteObject[],
opts?: DOMRouterOpts
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: {
...opts?.future,
v7_prependBasename: true,
},
history: createHashHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
window: opts?.window,
}).initialize();
}
该方法会创建history对象,将listen、push、replace、handlePop这几个方法赋值给history对象,并返回
createRouter
代码有省略
/** 创建router对象,处理跳转 */
export function createRouter(init: RouterInit): Router {
let mapRouteProperties: MapRoutePropertiesFunction;
// 拍平routes结构
let dataRoutes = convertRoutesToDataRoutes(
init.routes,
mapRouteProperties,
undefined,
manifest
);
let state: RouterState = {};
// state变化时,会触发set里的的回调函数
// 回调函数注册是通过subscribe方法
// function subscribe(fn: RouterSubscriber) {
// subscribers.add(fn);
// return () => subscribers.delete(fn);
// }
let subscribers = new Set<RouterSubscriber>();
// 匹配route
let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
let router: Router;
// 初始化router的方法
// 调用方式为 let router = createRouter(init).initialize();
function initialize() {
// 注册监听popstate,传入popstate事件监听回调函数,popstate事件触发的acion为POP
unlistenHistory = init.history.listen(
({ action: historyAction, location, delta }) => {
// 内部实际执行为startNavigation方法
return startNavigation(historyAction, location);
}
);
// 返回router对象
return router;
}
// 更新state并通知state变化
function updateState(newState: Partial<RouterState>,...): void {
state = {
...state,
...newState,
};
// 遍历subscribers,并执行内部存储的回调方法
[...subscribers].forEach((subscriber) =>
subscriber(state, {...})
);
}
// 根据传入的location、action执行跳转
async function startNavigation(
historyAction: HistoryAction,
location: Location,
opts?: {...}
): Promise<void> {
pendingAction = historyAction;
// 匹配路由
let matches = matchRoutes(routesToUse, location, basename);
// 未匹配到,返回404错误error boundary
if (!matches) {
let error = getInternalRouterError(404, { pathname: location.pathname });
completeNavigation(...);
return;
}
// 执行completeNavigation
completeNavigation(location, {
matches,
...getActionDataForCommit(pendingActionResult),
loaderData,
errors,
});
}
// 完成navigation、更新state(historyAction/location/matches)
function completeNavigation(location,newState): void {
// 根据跳转类型,执行不同方法
if (pendingAction === HistoryAction.Pop) {
// popstate事件触发,不需要动作,因为事件触发说明,url已经发生了变更。
} else if (pendingAction === HistoryAction.Push) {
// 此处即执行window.history.pushState方法
init.history.push(location, location.state);
} else if (pendingAction === HistoryAction.Replace) {
// 此处即执行window.history.replaceState方法
init.history.replace(location, location.state);
}
// 更新state
updateState(...props);
}
// 手动触发跳转方法 --useNavigate内部即执行此方法
async function navigate(to: number | To | null,opts?: RouterNavigateOptions){
// 传入数字,即执行window.history.go方法
if (typeof to === "number") {
init.history.go(to);
return;
}
// 其他情况即执行startNavigation
return await startNavigation(...props);
}
// 返回router对象
router = {
initialize,
subscribe,
navigate,
};
return router;
}
RouterProvider
// RouterProvider通过context,将各种router相关的变量传递给下层组件
export function RouterProvider({
fallbackElement,
router,
future,
}: RouterProviderProps): React.ReactElement {
let [state, setStateImpl] = React.useState(router.state);
// 执行updateState方法后会触发state的变更,从而触发重新渲染,渲染逻辑包含在DataRoutes中
let setState = React.useCallback<RouterSubscriber>(
(newState: RouterState) => {setStateImpl(newState);},
[setStateImpl]
);
// 注册updateState变化后的回调方法,
React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);
let navigator = React.useMemo((): Navigator => {
return {
createHref: router.createHref,
encodeLocation: router.encodeLocation,
go: (n) => router.navigate(n),
push: (to, state, opts) =>
router.navigate(to, {
state,
preventScrollReset: opts?.preventScrollReset,
}),
replace: (to, state, opts) =>
router.navigate(to, {
replace: true,
state,
preventScrollReset: opts?.preventScrollReset,
}),
};
}, [router]);
let basename = router.basename || "/";
let dataRouterContext = React.useMemo(
() => ({
router,
navigator,
static: false,
basename,
}),
[router, navigator, basename]
);
return (
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<Router
basename={basename}
location={state.location}
navigationType={state.historyAction}
navigator={navigator}
>
<DataRoutes
routes={router.routes}
future={router.future}
state={state}
/>
</Router>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
{null}
</>
);
}
总结
与跳转有关的逻辑,router需要做的事情简单来说有两个,即管理url和根据url渲染相关的组件。 url的变更分为两种类型,
- 外部触发的url变更(外部相对于react-router而言),此类变更是通过监听popState事件,执行handlePop方法,即startNavigation->completeNavigation方法来达到的
- react-router内部的方法触发的,比如useNavigate hook、Navigate组件等,此类变更是通过主动触发startNavigation方法来达到的,与外部触发的区别在于,主动触发的跳转需要维护url,同时不能触发页面的刷新,这里的实现即通过原生的history.pushState和history.replaceState方法达到的
github.com/remix-run/r… reactrouter.com/en/main/rou… developer.mozilla.org/en-US/docs/…