基本构成和关系
核心源码三部分
- react-router
- react-router-dom
- react-router-native
核心和关键在 react-router
react-router 可以理解为是 react-router-dom 的核心, 封装了 Router、Routes 等的组件,实现了核心的路由匹配和渲染切换
react-router 里面, 路由的核心是 history 库,histroy 实现了路由的原理, 就像是在 single-spa 中介绍的路由原理那样,处理了路由的两种路由模式下的
监听问题, 处理了两种路由模式下的 api 的处理问题
基本使用
基本使用代码示例
import * as React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(
document.getElementById("root")
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
import * as React from "react";
import { Routes, Route, Link } from "react-router-dom";
import "./App.css";
function App() {
return (
<div className="App">
<h1>Welcome to React Router!</h1>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
</Routes>
</div>
);
}
接下来分析这种使用方式的的原理
3. 原理分析
3.1. BrowserRouter
/**
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
*
* @see https://reactrouter.com/docs/en/v6/routers/browser-router
*/
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}
/>
);
}
history 中有针对路由的监听, BrowserRouter 组件, 创建了 BrowserRouter 实例, 并且在 history 变化的时候, 重新监听路由。
react-router 中路由的监听便是在这里完成的, 同时向下传递路由实例
3.2. Router
// context provide 提供路由信息和上下文环境
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
invariant(
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
// 格式化 basename
let basename = normalizePathname(basenameProp);
// navigationContext 上下文
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === "string") {
// 解析路径, 转化为对象
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
warning(
location != null,
`<Router basename="${basename}"> is not able to match the URL ` +
`"${pathname}${search}${hash}" because it does not start with the ` +
`basename, so the <Router> won't render anything.`
);
if (location == null) {
// 无法匹配,不渲染任何东西
return null;
}
// 主要用来注入 NavigationContext LocationContext 的上下文
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
Router 很好理解, 其实就是一个上下文提供者, 没有实质性的组件渲染,提供了 NavigationContext: 存储 history路由实例等数据
提供 LocationContext:主要用来提供 location 信息, 和目前的导航方式
3.3. Routes
/**
* A container for a nested tree of <Route> elements that renders the branch
* that best matches the current location.
*
* @see https://reactrouter.com/docs/en/v6/components/routes
*/
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
代码比较简单: 主要调用 createRoutesFromChildren 来生成 routes 配置的数据结构,然后传递到 useRoutes 来渲染
createRoutesFromChildren
/**
* Creates a route config from a React "children" object, which is usually
* either a `<Route>` element or an array of them. Used internally by
* `<Routes>` to create a route config from its children.
*
* @see https://reactrouter.com/docs/en/v6/utils/create-routes-from-children
*/
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element) => {
if (!React.isValidElement(element)) {
// Ignore non-elements. This allows people to more easily inline
// conditionals in their route config.
return;
}
// React.Fragment 子元素
if (element.type === React.Fragment) {
// Transparently support React.Fragment and its children.
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}
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;
}
从如下结构里面, 递归拿到路由的配置,不再赘述
<Routes>
<Route path="/" element={<Home />} />
<Route path="users" element={<Users />}>
<Route path="me" element={<OwnUserProfile />} />
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
useRoutes
/**
* Returns the element of the route that matched the current location, prepared
* with the correct context to render the remainder of the route tree. Route
* elements in the tree must render an <Outlet> to render their child route's
* element.
*
* @see https://reactrouter.com/docs/en/v6/hooks/use-routes
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// 非locationContext.provider 报错
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useRoutes() may be used only in the context of a <Router> component.`
);
// {
// outlet: null,
// matches: [],
// }
//
let { matches: parentMatches } = React.useContext(RouteContext);
let routeMatch = parentMatches[parentMatches.length - 1];
let parentParams = routeMatch ? routeMatch.params : {};
let parentPathname = routeMatch ? routeMatch.pathname : "/";
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
let parentRoute = routeMatch && routeMatch.route;
if (__DEV__) {
// You won't get a warning about 2 different <Routes> under a <Route>
// without a trailing *, but this is a best-effort warning anyway since we
// cannot even give the warning unless they land at the parent route.
//
// Example:
//
// <Routes>
// {/* This route path MUST end with /* because otherwise
// it will never match /blog/post/123 */}
// <Route path="blog" element={<Blog />} />
// <Route path="blog/feed" element={<BlogFeed />} />
// </Routes>
//
// function Blog() {
// return (
// <Routes>
// <Route path="post/:id" element={<Post />} />
// </Routes>
// );
// }
let parentPath = (parentRoute && parentRoute.path) || "";
warningOnce(
parentPathname,
!parentRoute || parentPath.endsWith("*"),
`You rendered descendant <Routes> (or called \`useRoutes()\`) at ` +
`"${parentPathname}" (under <Route path="${parentPath}">) but the ` +
`parent route path has no trailing "*". This means if you navigate ` +
`deeper, the parent won't match anymore and therefore the child ` +
`routes will never render.\n\n` +
`Please change the parent <Route path="${parentPath}"> to <Route ` +
`path="${parentPath === "/" ? "*" : `${parentPath}/*`}">.`
);
}
// 默认值 null
let locationFromContext = useLocation();
let location;
if (locationArg) {
let parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
invariant(
parentPathnameBase === "/" ||
parsedLocationArg.pathname?.startsWith(parentPathnameBase),
`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, ` +
`the location pathname must begin with the portion of the URL pathname that was ` +
`matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` +
`but pathname "${parsedLocationArg.pathname}" was given in the \`location\` prop.`
);
location = parsedLocationArg;
} else {
location = locationFromContext;
}
// 计算 pathname
let pathname = location.pathname || "/";
let remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
// 核心匹配算法
let matches = matchRoutes(routes, { pathname: remainingPathname });
if (__DEV__) {
warning(
parentRoute || matches != null,
`No routes matched location "${location.pathname}${location.search}${location.hash}" `
);
warning(
matches == null ||
matches[matches.length - 1].route.element !== undefined,
`Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element. ` +
`This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`
);
}
// 渲染组件, 由子到父顺序
return _renderMatches(
matches &&
matches.map((match) =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase:
match.pathnameBase === "/"
? parentPathnameBase
: joinPaths([parentPathnameBase, match.pathnameBase]),
})
),
parentMatches
);
}
这部分属于是核心部分源码, 一共做三个事情
- 生成 pathname , 比如:路由 /aa/bb, 那么pathname 就是 /aa/bb
- 通过 matchRoutes 核心路由的匹配算法, 找到匹配的路由分支 。 比如:/manage/edit 明显是二级路由,那么路由分支应该是: / → manage→ edit
- 渲染匹配到的路由的对应组件, 同时 location 变化时,也会重新进行渲染
matchRoutes
/**
* Matches the given routes to a location and returns the match data.
*
* @see https://reactrouter.com/docs/en/v6/utils/match-routes
*/
export function matchRoutes(
routes: RouteObject[],
locationArg: Partial<Location> | string,
basename = "/"
): RouteMatch[] | null {
// 解析翻译为对象
let location =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
let pathname = stripBasename(location.pathname || "/", basename);
if (pathname == null) {
return null;
}
// 拿到拍平以后的路由配置, 并且给了 store
let branches = flattenRoutes(routes);
// 按照分数进行排序
rankRouteBranches(branches);
let matches = null;
for (let i = 0; matches == null && i < branches.length; ++i) {
// 从子路由开始匹配
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
- 拍平 routes 的结构, 子路由在前,父路由在后
- 根据不同的分支, 不同的权重, 将分支按照 store 进行排序, 确定了匹配的优先级
- 根据排序后的数组, 匹配对应的组件
_renderMatches
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);
}
这段代码很精妙,信息量也非常大,通过 reduceRight 来形成 react 结构 elmenet,这一段解决了三个问题:
- 第一层 route 页面是怎么渲染
- outlet 是如何作为子路由渲染的。
- 路由状态是怎么传递的。
reduceRight 是从右向左开始遍历,那么之前讲到过 match 结构是 root -> children -> child1, reduceRight 把前一项返回的内容作为后一项的 outlet,那么如上的 match 结构会这样被处理。
- 1 首先通过 provider 包裹 child1,那么 child1 真正需要渲染的内容 Child1 组件 ,将被当作 provider 的 children,最后把当前 provider 返回,child1 没有子路由,所以第一层 outlet 为 null。
- 2 接下来第一层返回的 provider,讲作为第二层的 outlet ,通过第二层的 provider 的 value 里面 outlet 属性传递下去。然后把 Layout 组件作为 children 返回。
- 3 接下来渲染的是第一层的 Provider ,所以 Layout 会被渲染,那么 Child1 并没有直接渲染,而是作为 provider 的属性传递下去。
那么从上面我们都知道 child1 是在 container 中用 Outlet 占位组件的形式渲染的。那么我们先想一下 Outlet 会做哪些事情,应该会用 useContext 把第一层 provider 的 outlet 获取到然后渲染就可以渲染 child1 的 provider 了,而 child1 为 children 也就会被渲染了
4. 总结
路由本质在于 Routes 组件,当 location 上下文改变的时候,Routes 重新渲染,重新形成渲染分支,然后通过 provider 方式逐层传递 Outlet,进行匹配渲染