简单实现react router

·  阅读 851

只做简单实现 了解如何在react中管理路由 并不完全一模一样...而且不完整 实例在vite + react-ts中运行

使用到的第三方包

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>
复制代码

当我们使用以上代码时,组件结构是这样的

image-20210110195344302

观察到Router props存在history 组件内存在数据location

image-20210110195651423
  • location保存了当前路由的信息
  • history则是由Browser传入的 (其实这个history对象就是由上面的history包 中得到的)

router中创建了一个上下文Router.Provider,上下文格式如下

image-20210110200007733
  • 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>
    
// 是很有必要的。这里传入的是匹配的结果等等数据 子组件可以获取    

复制代码

代码地址:github 放这里再说

只是简单实现。。。

只是简单实现。。。

只是简单实现。。。

over

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改