首先看一下v6版本相对于v5版本的改动点
1、v5版本的 Switch 被废弃, v6使用 Routes 来替代 Switch 的功能
Routes组件源码:
export function Routes({
basename = '',
caseSensitive = false,
children
}) {
let routes = createRoutesFromChildren(children);
console.log(routes);
return useRoutes(routes, basename, caseSensitive);
}
可以看到这个组件首先处理 children 以生成符合v6格式的 routes 对象
举个🌰, 给一个官方文档给出的示例
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="courses" element={<Courses />}>
<Route path="/" element={<CoursesIndex />} />
<Route path="react-fundamentals/*" element={<ReactFundamentals />} />
<Route path="advanced-react/*" element={<AdvancedReact />} />
</Route>
</Routes>
生成的 routes 的对象为
关于 createRoutesFromChildren 函数, 这里就不多讲了, 就是通过递归的方式生成routes数组,有一点需要提到的是该函数内部做了React.Fragment的 "穿透" 处理
2、把v5版本满足路由匹配的 Component 属性修改为 element 属性, 传递的形式也略有不同,
<Route path="advanced-react/*" element={<AdvancedReact />} />
可以直接对匹配的组件传递参数,不用再像v5版本一样使用renderProps的方式
3、v5版本的路由跳转通常使用 history.go 和 history.push / replace 来做, v6版本 提供了useNavigate 的自定义hook函数
(源码省略部分错误输出逻辑)
export function useNavigate() {
let { pathname } = React.useContext(RouteContext);
let locationContext = React.useContext(LocationContext);
let { history, pending } = locationContext;
let activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
let navigate = React.useCallback(
(to, { replace, state } = {}) => {
if (activeRef.current) {
if (typeof to === 'number') {
history.go(to);
}
else { l
let relativeTo = resolveLocation(to, pathname);
// If we are pending transition, use REPLACE instead of PUSH.
// This will prevent URLs that we started navigating to but
// never fully loaded from appearing in the history stack.
let method = !!replace || pending ? 'replace' : 'push';
history[method](relativeTo, state);
}
}
}, [history, pathname, pending]);
return navigate;
}
emmm(没什么太大的改变)
4、v5版本的 Redirect 组件 替换为 v6 的 Navigate 组件
(源码同样省略部分错误处理逻辑)
export function Navigate({ to, replace, state }) {
let navigate = useNavigate();
let locationContext = React.useContext(LocationContext);
React.useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
采用React Hooks的 useEffect 方法在组件挂载完毕后调用navigate达到重定向的目的
5、v6版本在Route组件嵌套的情况下 path的取值是父级的pathname再加上当前组件的path,而不需要像v5版本的那样使用useRouteMatch获取match对象,然后使用match.url 和 match.path来手动拼接pathname
那么针对文章最初给出的示例, 新的v6版本是怎么一步一步的匹配解析达到我们想要的路由效果的呢? 下边我们就来看一下最核心的 useRoutes 函数(关键的地方做了标注)
export function useRoutes(
routes,
basename = '',
caseSensitive = false
) {
let {
params: parentParams,
pathname: parentPathname,
route: parentRoute,
} = React.useContext(RouteContext);
basename = basename ? joinPaths([parentPathname, basename]) : parentPathname;
let location = useLocation(); // 获取location对象
let matches = React.useMemo(
() => matchRoutes(routes, location, basename, caseSensitive),
[routes, location, basename, caseSensitive]
);
if (!matches) {
// TODO: Warn about nothing matching, suggest using a catch-all route.
return null;
}
// 如果存在路由匹配的情况
// 这里解密了我们在Route组件里获取到的 outlet 到底是什么东西
// 其实它就是更底层级的已经匹配到的 Route 组件!!!
// 这里的生成也是从由向右向左生成的一个嵌套了多层的RouteContext结构,为的就是保证按照最近获取的原则
// 获取到正确的context对象
let element = matches.reduceRight((outlet, { params, pathname, route }) => {
return (
<RouteContext.Provider
children={route.element}
value={{
outlet,
params: readOnly({ ...parentParams, ...params }),
pathname: joinPaths([basename, pathname]),
route,
}}
/>
);
}, null);
return element;
}
export function matchRoutes(
routes,
location,
basename = '',
caseSensitive = false
) {
if (typeof location === 'string') {
location = parsePath(location);
}
// TODO: Validate location
// - it should have a pathname
let base = basename.replace(/^\/+|\/+$/g, ''); // 删除开头和结尾多余的 '/' 符号
let target = location.pathname.slice(1); // 去掉开头的 '/'
if (base) {
// 计算剩余要匹配的路由部分
if (base === target) {
target = ''; // -- 无剩余路由匹配
} else if (target.startsWith(base)) {
target = target.slice(base.length).replace(/^\/+/, '');
} else {
// 路由不匹配
return null;
}
}
let flattenedRoutes = flattenRoutes(routes); // 将routes数组flat成一维的数组
rankFlattenedRoutes(flattenedRoutes);
// 找到第一个满足路由匹配规则的 Route 组件并返回
for (let i = 0; i < flattenedRoutes.length; ++i) {
// 返回当前的Route组件的path对象和在其之前的所有的父级Route组件和该组件自身组成的数组
let [path, flatRoutes] = flattenedRoutes[i];
// TODO: Match on search, state too
// 对path做一些正则方面的特殊处理, 返回新创建的用来匹配路由的正则对象
// 正则的形式是 ^(...) 的格式 这也解释了为什么下文生成pathname的时候使用了match[1]的写法
let [matcher] = compilePath(path, /* end */ true, caseSensitive);
if (matcher.test(target)) {
// 如果当前的Route组件匹配了路由,
return flatRoutes.map((route, index) => {
let routes = flatRoutes.slice(0, index + 1);
let path = joinPaths(routes.map(r => r.path));
let [matcher, keys] = compilePath(path, /* end */ false, caseSensitive);
let match = target.match(matcher);
let pathname = '/' + match[1];
let values = match.slice(2);
let params = keys.reduce((memo, key, index) => {
memo[key] = safelyDecodeURIComponent(values[index], key);
return memo;
}, {});
return { params, pathname, route };
});
}
}
return null;
}