只做简单实现 了解如何在react中管理路由 并不完全一模一样...而且不完整 实例在vite + react-ts中运行
使用到的第三方包
- history 路由管理
- path-to-regexp 将路径转化为正则
history与path-to-regexp 简单介绍
history
history 可创建基于 history hash memory(内存) 的路由管理
import {createBrowserHistory, createMemoryHistory, createHashHistory} from "history"
返回类型都是自定义的History接口类型
export interface History<S extends State = State> {
readonly action: Action; // 当前操作类型 如 PUSH POP
readonly location: Location<S>;// 当前页面路由信息
createHref(to: To): string; // 创建一条跳转链接
push(to: To, state?: S): void;// 添加一条记录
replace(to: To, state?: S): void;// 替换
go(delta: number): void;// ...
back(): void; // ...
forward(): void; // ...
listen(listener: Listener<S>): () => void;// 开启监听路由变化
block(blocker: Blocker<S>): () => void; //... 后边再说
}
path-to-regexp
可将路径转化为对应的正则...
import { pathToRegexp, parse, compile, match } from "path-to-regexp";
-
pathToRegexp 将路径转换为正则
-
match 通过正则匹配匹配路径
-
哪两个不咋用先过了...
const options = {end:false} // end 是否匹配结尾
const regexp = pathToRegexp("/article/:id", [],options);
const exec = match(regexp)
exec("/article/1") // {xxx,params:{id:1}}
了解react-router
组件树结构
<Router>
<Route path="/login/add">
<LoginPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<Route path="/register/add" exact>
<RegPage />
</Route>
<Route path="/register">
<RegPage />
</Route>
<Route path="/add" exact>
<DefaultLayout />
</Route>
</Router>
当我们使用以上代码时,组件结构是这样的
观察到Router props存在history 组件内存在数据location
- location保存了当前路由的信息
- history则是由Browser传入的 (其实这个history对象就是由上面的history包 中得到的)
router中创建了一个上下文Router.Provider,上下文格式如下
- history还是当前全局路由对象
- location保存了当前页面路由信息
- match 则是根据配置得到的当前页面的匹配结果 (...)
再往下看,Route组件内使用了这个上下文
到这里其实可以看出Router组件内创建了一个history路由管理器,并把操作的history对象 页面当前信息的location对象 放到了上下文中,而Route组件使用了上下文 ,在Route组件内接受了我们的配置信息(props) 如 path exact component children 等等信息 在Route组件内通过判断是否匹配进行页面的显示...
实现
简单实现一下 Browser Router Route
- 创建文件夹 router
- 创建文件 browser-router.tsx router.tsx route.tsx
- 创建上下文router-context.ts
先来实现context
根据react-router的数据接口创建类型 与 初始值
import React from "react";
import { History, Location } from "history";
import { MatchResult } from "./types";
// context初始值 后边用
export const RouterContextDefaultVal: RouterContextValueProps = {
history: null,
location: null,
match: {
isExact: true,
params: {},
path: "/",
url: "/",
},
};
// 创建context上下文对象
const Context = React.createContext<RouterContextValueProps>(
RouterContextDefaultVal
);
Context.displayName = "Router";
// context储存的值类型
export type RouterContextValueProps = {
history: History | null;
location: Location | null;
match: MatchResult;
};
export default Context;
router
引入所需要的东西...??
import React, { useEffect, useMemo, useState } from "react";
import { History, Location } from "history"; // interface
import RouterContext, {
RouterContextDefaultVal,
RouterContextValueProps,
} from "./router-context";
创建Router组件
export type RouterProps = {
history: History;
};
export const Router: React.FC<RouterProps> = (props) => {
const { history, children } = props; // history为history包创建的对象
const [location, setLocation] = useState<Location | null>(null);// 当前页面(路由)信息
const value = useMemo<RouterContextValueProps>(() => {
// 初次渲染
if (location == null) {
return RouterContextDefaultVal;
}
// 路由变化后 返回新的对象 触发子组件渲染
return {
history,
match: RouterContextDefaultVal.match, // 这个match在这里暂时用不着
location,
};
}, [location]); // 当路由进行跳转触发下边的history.listen监听时 进行重新赋值
useEffect(() => {
// 首次渲染 将进入路由的信息初始化
setLocation(history.location);
const unListen = history.listen(({ action, location: newLocation }) => {
// 当路由变化时 重新设置自己的location 并更改RouterContext value(useMemo)
// setLocation 运行后 重新执行 这时value useMemo依赖性被改变 重新计算新的 RouterContext value
// 依赖RouterContext的子组件 会重新执行渲染...
setLocation(newLocation);
});
return () => unListen(); // 页面卸载 取消监听 理论说这个页面是不会卸载的。。
}, [history]);
return (
{// 将value传入上下文对象中}
<RouterContext.Provider value={value}>
{children}
</RouterContext.Provider>
);
};
Route
引入所需的依赖
import React, { useContext } from "react";
import RouterContext from "./router-context";
import { History } from "history"; // interface
import { computedRouteIsRender } from "./util"; // 计算匹配结果
import { MatchResult } from "./types";// 匹配结果类型
// ./util.ts
import { match, pathToRegexp } from "path-to-regexp";
import {
RouterContextDefaultVal,
RouterContextValueProps,
} from "./router-context";
import { MatchResult } from "./types";
/**
* 路径匹配
*/
export const computedRouteIsRender = (
path: string, // 需要匹配的路径
exact: boolean | undefined,// 精准匹配
value: RouterContextValueProps // 上下文对象 使用它的location进行匹配
): MatchResult => {
// When true the regexp will match to the end of the string. (default: true)
// end 为true时匹配字符串末尾
const regExp = pathToRegexp(path, [], { end: !!exact });
const exec = match(regExp, {
decode: decodeURIComponent,
});
const result = exec(value.location?.pathname || "");
// 如果false匹配失败 isExact返回false
if (!result) {
return {
...RouterContextDefaultVal.match,
isExact: false,
};
}
// 匹配成功 返回对应信息
return {
isExact: true,
path: path,
url: value.location?.pathname || "",
params: result.params,
};
};
// ./types.ts
export type MatchResult<P extends Object = {}> = {
isExact: boolean;
params: P;
path: string;
url: string;
};
route实现
route的实现思路很简单. 由于已经接受了传来props中的信息 ,根据这些信息进行当前路径与信息中的路径和其他信息进行匹配,选择是否展示页面子组件就行了
/**
* 优先级 children > component > render
*/
export type RouteProps = {
path: string; // 匹配的路径
exact?: boolean; // 是否精准匹配
render?: (history: History | null) => React.ReactNode; // render函数
component?: React.ReactNode; // 组件
computedMath?: MatchResult; // 后边使用
};
export const Route:React.FC<RouteProps> = (props) => {
const { path, exact, render, children, component, computedMath } = props;
const ctx = useContext(RouterContext);
// 判断是否需要渲染
// 如果computedMath 存在则不用计算了 使用了switch
// 匹配失败
const result = computedRouteIsRender(path, exact, ctx);
console.log(computedMath);
if (!result.isExact) {
return <>{null}</>;
}
// 匹配成功 根据优先级展示
const MatchCom: React.ReactNode | null = children
? children
: component
? component
: render
? render(ctx.history)
: null;
// 这里的RouterContext.Provider 是因为子组件可能需要获取匹配结果。匹配参数 等等 后边使用
return (
<RouterContext.Provider
value={{
...ctx,
match: result,
}}
>
{MatchCom}
</RouterContext.Provider>
);
};
Browser-router
这里的实现就更容易了,就是创建一个history对象.将它传入Router组件中就行了。使用hash时直接改为createHashHistory...
创建browser-router.tsx
import { Router } from "./router";
import React, { useState } from "react";
import { createBrowserHistory, History } from "history";
export const BrowserRouter: React.FC = (props) => {
const [history, setHistory] = useState<History>(() => {
return createBrowserHistory({
window,
});
});
console.log(props);
return <Router history={history}>{props.children}</Router>;
};
这时把测试页面引入的路径改为自己实现的
// import {
// BrowserRouter as Router,
// Route,
// Redirect,
// Switch,
// } from "react-router-dom";
import { Route, BrowserRouter as Router } from "./react-router/index";
查看页面组件树 ,可以正常使用
Switch
react-router在子组件route匹配成功一个后就停止了
先来看一段代码与它的输出
const A = (props) => {
console.log(props.children)
return <div>sb</div>
}
const C = (props) => {
return <div>nt</div>
}
const B = (props) => {
return <A>
<C count={1} />
<C count={2} />
<C count={3} />
</A>
}
ReactDOM.render(<B />, document.getElementById("root"));
得到打印结果
[
{
$$typeof: Symbol(react.element),
key:null,
props:{count:1},
xxx
},
{xxx},
{xxx}
]
可以通过访问props得到子组件的props
那就容易实现了
import React, { useContext } from "react";
import RouterContext from "./router-context";
import { computedRouteIsRender } from "./util";
export const Switch: React.FC = (props) => {
const value = useContext(RouterContext);
// 遍历子元素
if (Array.isArray(props.children)) {
for (let i = 0; i < props.children.length; i++) {
const child = props.children[i];
const {
path,
exact,
}: { path: string; exact: boolean } = (child as JSX.Element).props;
const result = computedRouteIsRender(path, exact, value);
// 匹配成功一个直接返回该组件 不再继续
if (result.isExact) {
return <>{child}</>;
}
}
}
return <>{null}</>;
};
但现在有个问题。。我如何通知Route组件不再执行computedRouteIsRender函数呢。。,,,....
剩下的例如Link useHistory useParams啥的实现更容易些
Link
import React, { useContext } from "react";
import RouterContext from "./router-context";
import { To } from "history";
export type LinkProps = {
to: To;
};
export const Link: React.FC<LinkProps> = (props) => {
const ctx = useContext(RouterContext);
return (
<a
href="#"
onClick={() => {
ctx.history?.push(props.to);
}}
>
{props.children}
</a>
);
};
usexxx
import React, { useContext } from "react";
import RouterContext from "./router-context";
export const useHistory = () => {
const ctx = useContext(RouterContext);
return ctx.history;
};
export const useParams = (): Record<string, string> => {
const ctx = useContext(RouterContext);
return ctx.match.params;
};
export const useState = () => {
const ctx = useContext(RouterContext);
return ctx.location?.state
}
// 这里需要获取params参数 state数据 使用了context 但是如果正常情况下获取到的是离自己最近的context.provider根节点
// 所以在route中再次上一层
<RouterContext.Provider
value={{
...ctx,
match: result,
}}
>
{MatchCom}
</RouterContext.Provider>
// 是很有必要的。这里传入的是匹配的结果等等数据 子组件可以获取
只是简单实现。。。
只是简单实现。。。
只是简单实现。。。
over