react-router 原始碼閱讀

Grewer發表於2022-02-01
這次的版本是 6.2.1

使用

相比較 5.x 版本, <Switch>元素升級為了<Routes>

簡單的 v6 例子:

function App(){
    return  <BrowserRouter>
        <Routes>
            <Route path="/about" element={<About/>}/>
            <Route path="/users" element={<Users/>}/>
            <Route path="/" element={<Home/>}/>
        </Routes>
    </BrowserRouter>
}

context

在 react-router 中, 他建立了兩個 context 供後續的使用, 當然這兩個 context 是在內部的, 並沒有 API 暴露出來

NavigationContext

/**
 * 一個路由物件的基本構成
 */
export interface RouteObject {
    caseSensitive?: boolean;
    children?: RouteObject[];
    element?: React.ReactNode;
    index?: boolean;
    path?: string;
}

// 常用的引數型別
export type Params<Key extends string = string> = {
    readonly [key in Key]: string | undefined;
};

/**
 * 一個 路由匹配 介面
 */
export interface RouteMatch<ParamKey extends string = string> {
    /**
     * 動態引數的名稱和值的URL
     */
    params: Params<ParamKey>;
    /**
     * 路徑名
     */
    pathname: string;
    /**
     * 之前匹配的路徑名
     */
    pathnameBase: string;
    /**
     * 匹配到的路由物件
     */
    route: RouteObject;
}

interface RouteContextObject {
    outlet: React.ReactElement | null;
    matches: RouteMatch[];
}

const RouteContext = React.createContext<RouteContextObject>({
    outlet: null,
    matches: []
});

LocationContext

import type {
    Location,
    Action as NavigationType
} from "history";

interface LocationContextObject {
    location: Location; // 原生的 location 物件, window.location

    /**
     * enum Action 一個列舉, 他有三個引數, 代表路由三種動作
     * Pop = "POP",
     * Push = "PUSH",
     * Replace = "REPLACE"
     */
    navigationType: NavigationType;  
}

const LocationContext = React.createContext<LocationContextObject>(null!);

MemoryRouter

react-router-dom 的原始碼解析中我們說到了 BrowserRouterHashRouter, 那麼這個 MemoryRouter又是什麼呢

他是將 URL 的歷史記錄儲存在記憶體中的 <Router>(不讀取或寫入位址列)。在測試和非瀏覽器環境中很有用,例如 React Native。

他的原始碼和其他兩個 Router 最大的區別就是一個 createMemoryHistory 方法, 此方法也來自於 history 庫中

export function MemoryRouter({
                                 basename,
                                 children,
                                 initialEntries,
                                 initialIndex
                             }: MemoryRouterProps): React.ReactElement {
    let historyRef = React.useRef<MemoryHistory>();
    if (historyRef.current == null) {
        historyRef.current = createMemoryHistory({ initialEntries, initialIndex });
    }

    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}
        />
    );
}

那我們現在來看一看這個方法, 這裡只講他與 createHashHistory 不同的地方:

export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
): MemoryHistory {
  let { initialEntries = ['/'], initialIndex } = options; // 不同的初始值 initialEntries
  let entries: Location[] = initialEntries.map((entry) => {
    let location = readOnly<Location>({
      pathname: '/',
      search: '',
      hash: '',
      state: null,
      key: createKey(), // 通過 random 生成唯一值
      ...(typeof entry === 'string' ? parsePath(entry) : entry)
    }); // 這裡的 location 屬於是直接建立, HashHistory 中是使用的 window.location
      // readOnly方法 可以看做 (obj)=>obj, 並沒有太大作用
    return location;
  });
 

  function push(to: To, state?: any) {
    let nextAction = Action.Push;
    let nextLocation = getNextLocation(to, state);
    function retry() {
      push(to, state);
    }

    // 忽略其他類似的程式碼
    
    if (allowTx(nextAction, nextLocation, retry)) {
      index += 1;
      // 別處是呼叫原生 API, history.pushState
      entries.splice(index, entries.length, nextLocation);
      applyTx(nextAction, nextLocation);
    }
  }

  
  // 與 push 類似, 忽略 replace

  function go(delta: number) {
      // 與HashHistory不同, 也是走的類似 push
    let nextIndex = clamp(index + delta, 0, entries.length - 1);
    let nextAction = Action.Pop;
    let nextLocation = entries[nextIndex];
    function retry() {
      go(delta);
    }

    if (allowTx(nextAction, nextLocation, retry)) {
      index = nextIndex;
      applyTx(nextAction, nextLocation);
    }
  }

  let history: MemoryHistory = {
    // 基本相同
  };

  return history;
}

Navigate

用來改變 當然 location 的方法, 是一個 react-router 丟擲的 API

使用方式:


function App() {
    // 一旦 user 是有值的, 就跳轉至 `/dashboard` 頁面了
    // 算是跳轉路由的一種方案
    return <div>
        {user && (
            <Navigate to="/dashboard" replace={true} />
        )}
        <form onSubmit={event => this.handleSubmit(event)}>
            <input type="text" name="username" />
            <input type="password" name="password" />
        </form>
    </div>
}

原始碼


export function Navigate({ to, replace, state }: NavigateProps): null {
    // 直接呼叫 useNavigate 來獲取 navigate 方法, 並且  useEffect 每次都會觸發
    // useNavigate 原始碼在下方會講到
    let navigate = useNavigate();
    React.useEffect(() => {
        navigate(to, { replace, state });
    });

    return null;
}

Outlet

用來渲染子路由的元素, 簡單來說就是一個路由的佔位符

程式碼很簡單, 使用的邏輯是這樣

使用方式:


function App(props) {
    return (
        <HashRouter>
            <Routes>
                <Route path={'/'} element={<Dashboard></Dashboard>}>
                    <Route path="qqwe" element={<About/>}/>
                    <Route path="about" element={<About/>}/>
                    <Route path="users" element={<Users/>}/>
                </Route>
            </Routes>
        </HashRouter>
    );
}

// 其中外層的Dashboard:

function Dashboard() {
    return (
        <div>
            <h1>Dashboard</h1>
            <Outlet />
            // 這裡就會渲染他的子路由了
            // 和以前 children 差不多
        </div>
    );
}

原始碼

export function Outlet(props: OutletProps): React.ReactElement | null {
    return useOutlet(props.context);
}

export function useOutlet(context?: unknown): React.ReactElement | null {
    let outlet = React.useContext(RouteContext).outlet;
    if (outlet) {
        return (
            <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
        );
    }
    return outlet;
}

useParams

從當前URL所匹配的路徑中, 返回一個物件的鍵/值對的動態引數。

function useParams<
    ParamsOrKey extends string | Record<string, string | undefined> = string
    >(): Readonly<
    [ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>
    > {
    // 直接獲取了 RouteContext 中 matches 陣列的最後一個物件, 如果沒有就是空物件
    let { matches } = React.useContext(RouteContext);
    let routeMatch = matches[matches.length - 1];
    return routeMatch ? (routeMatch.params as any) : {};
}

useResolvedPath

將給定的`to'值的路徑名與當前位置進行比較

<NavLink> 這個元件中使用到

function useResolvedPath(to: To): Path {
    let { matches } = React.useContext(RouteContext);
    let { pathname: locationPathname } = useLocation();
    
    // 合併成一個 json 字元, 至於為什麼又要解析, 是為了新增字元層的快取, 如果是一個物件, 就不好淺比較了
    let routePathnamesJson = JSON.stringify(
        matches.map(match => match.pathnameBase)
    );
    
    // resolveTo 的具體作用在下方討論
    return React.useMemo(
        () => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
        [to, routePathnamesJson, locationPathname]
    );
}

useRoutes

useRoutes鉤子的功能等同於<Routes>,但它使用JavaScript物件而不是<Route>元素來定義路由。
相當於是一種 schema 版本, 更好的配置性

使用方式:

如果使用過 umi, 是不是會感覺到一模一樣

function App() {
  let element = useRoutes([
    { path: "/", element: <Home /> },
    { path: "dashboard", element: <Dashboard /> },
    {
      path: "invoices",
      element: <Invoices />,
      children: [
        { path: ":id", element: <Invoice /> },
        { path: "sent", element: <SentInvoices /> }
      ]
    },
    { path: "*", element: <NotFound /> }
  ]);

  return element;
}

原始碼

// 具體的 routes 物件是如何生成的, 下面的 Routes-createRoutesFromChildren 會講到

export function useRoutes(
    routes: RouteObject[],
    locationArg?: Partial<Location> | string
): React.ReactElement | null {
    
    let { matches: parentMatches } = React.useContext(RouteContext);
    let routeMatch = parentMatches[parentMatches.length - 1];
    // 獲取匹配的 route
    
    let parentParams = routeMatch ? routeMatch.params : {};
    let parentPathname = routeMatch ? routeMatch.pathname : "/";
    let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
    let parentRoute = routeMatch && routeMatch.route;
    // 這裡上面都是一些引數, 沒有就是預設值
    
    //  等於 React.useContext(LocationContext).location, 約等於原生的 location
    let locationFromContext = useLocation();

    let location;
    if (locationArg) { // 對於配置項引數的一些判斷
        let parsedLocationArg =
            typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
        location = parsedLocationArg;
    } else {
        location = locationFromContext;
    }
    // 如果引數裡有則使用引數裡的, 如果沒有使用 context 的
    

    let pathname = location.pathname || "/";
    let remainingPathname =
        parentPathnameBase === "/"
            ? pathname
            : pathname.slice(parentPathnameBase.length) || "/";
    // matchRoutes 大概的作用是通過pathname遍歷尋找,匹配到的路由    具體原始碼放在下面講
    let matches = matchRoutes(routes, { pathname: remainingPathname });

    
    // 最後呼叫渲染函式  首先對資料進行 map
    // joinPaths  的作用約等於 paths.join("/") 並且去除多餘的斜槓
    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
    );
}

useRoutes-matchRoutes

function matchRoutes(
    routes: RouteObject[],
    locationArg: Partial<Location> | string,
    basename = "/"
): RouteMatch[] | null {
    let location =
        typeof locationArg === "string" ? parsePath(locationArg) : locationArg;

    // 獲取排除 basename 的 pathname
    let pathname = stripBasename(location.pathname || "/", basename);

    if (pathname == null) {
        return null;
    }

    // flattenRoutes 函式的主要作用, 壓平 routes, 方便遍歷
    // 原始碼見下方
    let branches = flattenRoutes(routes);
    
    // 對路由進行排序
    // rankRouteBranches 原始碼見下方
    rankRouteBranches(branches);

    
    // 篩選出匹配到的路由 matchRouteBranch原始碼在下面講
    let matches = null;
    for (let i = 0; matches == null && i < branches.length; ++i) {
        matches = matchRouteBranch(branches[i], pathname);
    }

    return matches;
}

useRoutes-matchRoutes-stripBasename

拆分 basename, 程式碼很簡單, 這裡就直接貼出來了

function stripBasename(pathname: string, basename: string): string | null {
    if (basename === "/") return pathname;

    if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
        return null;
    }

    let nextChar = pathname.charAt(basename.length);
    if (nextChar && nextChar !== "/") {
        return null;
    }

    return pathname.slice(basename.length) || "/";
}

useRoutes-matchRoutes-flattenRoutes

遞迴處理 routes, 壓平 routes

function flattenRoutes(
    routes: RouteObject[],
    branches: RouteBranch[] = [],
    parentsMeta: RouteMeta[] = [],
    parentPath = ""
): RouteBranch[] {
    routes.forEach((route, index) => {
        let meta: RouteMeta = {
            relativePath: route.path || "",
            caseSensitive: route.caseSensitive === true,
            childrenIndex: index,
            route
        };

        if (meta.relativePath.startsWith("/")) {
            meta.relativePath = meta.relativePath.slice(parentPath.length);
        }
        
        // joinPaths 原始碼: (paths)=>paths.join("/").replace(/\/\/+/g, "/")
        // 把陣列轉成字串, 並且清除重複斜槓
        let path = joinPaths([parentPath, meta.relativePath]);
        let routesMeta = parentsMeta.concat(meta);

        // 如果有子路由則遞迴
        if (route.children && route.children.length > 0) {
            flattenRoutes(route.children, branches, routesMeta, path);
        }

        // 匹配不到就 return
        if (route.path == null && !route.index) {
            return;
        }
        // 壓平後元件新增的物件
        branches.push({ path, score: computeScore(path, route.index), routesMeta });
    });

    return branches;
}

useRoutes-matchRoutes-rankRouteBranches

對路由進行排序, 這裡可以略過,不管排序演算法如何, 只需要知道, 知道輸入的值是經過一系列排序的就行

function rankRouteBranches(branches: RouteBranch[]): void {
    branches.sort((a, b) =>
        a.score !== b.score
            ? b.score - a.score // Higher score first
            : compareIndexes(
                a.routesMeta.map(meta => meta.childrenIndex),
                b.routesMeta.map(meta => meta.childrenIndex)
            )
    );
}

useRoutes-matchRoutes-matchRouteBranch

匹配函式, 接受引數 branch 就是某一個 rankRouteBranches

function matchRouteBranch<ParamKey extends string = string>(
    branch: RouteBranch,
    pathname: string
): RouteMatch<ParamKey>[] | null {
    let { routesMeta } = branch;

    let matchedParams = {};
    let matchedPathname = "/";
    let matches: RouteMatch[] = [];
    
    //  routesMeta 詳細來源可以檢視 上面的flattenRoutes
    for (let i = 0; i < routesMeta.length; ++i) {
        let meta = routesMeta[i];
        let end = i === routesMeta.length - 1;
        let remainingPathname =
            matchedPathname === "/"
                ? pathname
                : pathname.slice(matchedPathname.length) || "/";
        
        // 比較, matchPath 原始碼在下方
        let match = matchPath(
            { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
            remainingPathname
        );

        // 如果返回是空 則直接返回
        if (!match) return null;

        // 更換物件源
        Object.assign(matchedParams, match.params);

        let route = meta.route;
        
        // push 到最終結果上, joinPaths 不再贅述
        matches.push({
            params: matchedParams,
            pathname: joinPaths([matchedPathname, match.pathname]),
            pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
            route
        });

        if (match.pathnameBase !== "/") {
            matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
        }
    }

    return matches;
}

useRoutes-matchRoutes-matchRouteBranch-matchPath

對一個URL路徑名進行模式匹配,並返回有關匹配的資訊。
他也是一個保留在外的可用 API

export function matchPath<
    ParamKey extends ParamParseKey<Path>,
    Path extends string
    >(
    pattern: PathPattern<Path> | Path,
    pathname: string
): PathMatch<ParamKey> | null {
    // pattern 的重新賦值
    if (typeof pattern === "string") {
        pattern = { path: pattern, caseSensitive: false, end: true };
    }

    // 通過正則匹配返回匹配到的正規表示式   matcher 為 RegExp
    let [matcher, paramNames] = compilePath(
        pattern.path,
        pattern.caseSensitive,
        pattern.end
    );

    // 正則物件的 match 方法
    let match = pathname.match(matcher);
    if (!match) return null;

    // 取 match 到的值
    let matchedPathname = match[0];
    let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
    let captureGroups = match.slice(1);
    
    // params 轉成物件  { param:value, ... }
    let params: Params = paramNames.reduce<Mutable<Params>>(
        (memo, paramName, index) => {
            // 如果是*號  轉換
            if (paramName === "*") {
                let splatValue = captureGroups[index] || "";
                pathnameBase = matchedPathname
                    .slice(0, matchedPathname.length - splatValue.length)
                    .replace(/(.)\/+$/, "$1");
            }

            // safelyDecodeURIComponent  等於 decodeURIComponent + try_catch
            memo[paramName] = safelyDecodeURIComponent(
                captureGroups[index] || "",
                paramName
            );
            return memo;
        },
        {}
    );

    return {
        params,
        pathname: matchedPathname,
        pathnameBase,
        pattern
    };
}

useRoutes-matchRoutes-matchRouteBranch-matchPath-compilePath


function compilePath(
    path: string,
    caseSensitive = false,
    end = true
): [RegExp, string[]] {
    let paramNames: string[] = [];
    // 正則匹配替換
    let regexpSource =
        "^" +
        path
            // 忽略尾隨的 / 和 /*
            .replace(/\/*\*?$/, "")
            // 確保以 / 開頭
            .replace(/^\/*/, "/") 
            // 轉義特殊字元
            .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
            .replace(/:(\w+)/g, (_: string, paramName: string) => {
                paramNames.push(paramName);
                return "([^\\/]+)";
            });

    // 對於*號的特別判斷
    if (path.endsWith("*")) {
        paramNames.push("*");
        regexpSource +=
            path === "*" || path === "/*"
                ? "(.*)$" // Already matched the initial /, just match the rest
                : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
    } else {
        regexpSource += end
            ? "\\/*$" // 匹配到末尾時,忽略尾部斜槓
            : 
            "(?:\\b|\\/|$)";
    }

    let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
    
    // 返回匹配結果
    return [matcher, paramNames];
}

useRoutes-_renderMatches

渲染匹配到的路由

function _renderMatches(
    matches: RouteMatch[] | null,
    parentMatches: RouteMatch[] = []
): React.ReactElement | null {
    
    if (matches == null) return null;
    
    // 通過 context 傳遞資料
    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);
}

Router

為應用程式的其他部分提供context資訊

通常不會使用此元件, 他是 MemoryRouter 最終渲染的元件

在 react-router-dom 庫中, 也是 BrowserRouter 和 HashRouter 的最終渲染元件

export function Router({
                           basename: basenameProp = "/",
                           children = null,
                           location: locationProp,
                           navigationType = NavigationType.Pop,
                           navigator,
                           static: staticProp = false
                       }: RouterProps): React.ReactElement | null {

    // 格式化 baseName 
    let basename = normalizePathname(basenameProp);
    
    // memo context value
    let navigationContext = React.useMemo(
        () => ({ basename, navigator, static: staticProp }),
        [basename, navigator, staticProp]
    );

    // 如果是字串則解析  根據 #, ? 特殊符號解析 url
    if (typeof locationProp === "string") {
        locationProp = parsePath(locationProp);
    }

    let {
        pathname = "/",
        search = "",
        hash = "",
        state = null,
        key = "default"
    } = locationProp;

    // 同樣的快取
    let location = React.useMemo(() => {
        // 這還方法在 useRoutes-matchRoutes-stripBasename 講過這裡就不多說
        let trailingPathname = stripBasename(pathname, basename);

        if (trailingPathname == null) {
            return null;
        }

        return {
            pathname: trailingPathname,
            search,
            hash,
            state,
            key
        };
    }, [basename, pathname, search, hash, state, key]);

    // 空值判斷
    if (location == null) {
        return null;
    }

    // 提供 context 的 provider, 傳遞 children
    return (
        <NavigationContext.Provider value={navigationContext}>
            <LocationContext.Provider
                children={children}
                value={{ location, navigationType }}
            />
        </NavigationContext.Provider>
    );
}

parsePath

此原始碼來自於 history 倉庫

function parsePath(path: string): Partial<Path> {
  let parsedPath: Partial<Path> = {};

  // 首先確定 path
  if (path) {
      // 是否有#號 , 如果有則擷取
    let hashIndex = path.indexOf('#');
    if (hashIndex >= 0) {
      parsedPath.hash = path.substr(hashIndex);
      path = path.substr(0, hashIndex);
    }

    // 再判斷 ? , 有也擷取
    let searchIndex = path.indexOf('?');
    if (searchIndex >= 0) {
      parsedPath.search = path.substr(searchIndex);
      path = path.substr(0, searchIndex);
    }

    // 最後就是 path
    if (path) {
      parsedPath.pathname = path;
    }
  }
// 返回結果
  return parsedPath;
}

Routes

用來包裹 route 的元素, 主要是通過 useRoutes 的邏輯

 function Routes({
                           children,
                           location
                       }: RoutesProps): React.ReactElement | null {
    return useRoutes(createRoutesFromChildren(children), location);
}

Routes-createRoutesFromChildren

接收到的引數一般都是 Route children, 可能是多層巢狀的, 最後得的我們定義的 route 元件結構,
它將被傳遞給 useRoutes 函式

function createRoutesFromChildren(
    children: React.ReactNode
): RouteObject[] {
    let routes: RouteObject[] = [];

    // 使用官方函式迴圈
    React.Children.forEach(children, element => {
        if (element.type === React.Fragment) {
            // 如果是 React.Fragment 元件 則直接push 遞迴函式
            routes.push.apply(
                routes,
                createRoutesFromChildren(element.props.children)
            );
            return;
        }
        
        let route: RouteObject = {
            caseSensitive: element.props.caseSensitive,
            element: element.props.element,
            index: element.props.index,
            path: element.props.path
        }; // route 物件具有的屬性
        
        // 同樣地遞迴
        if (element.props.children) {
            route.children = createRoutesFromChildren(element.props.children);
        }

        routes.push(route);
    });

    return routes;
}

useHref

返回完整的連結

export function useHref(to: To): string {
    let { basename, navigator } = React.useContext(NavigationContext);
    // useResolvedPath 在上面講過
    let { hash, pathname, search } = useResolvedPath(to);

    let joinedPathname = pathname;
    if (basename !== "/") {
        let toPathname = getToPathname(to);
        let endsWithSlash = toPathname != null && toPathname.endsWith("/");
        joinedPathname =
            pathname === "/"
                ? basename + (endsWithSlash ? "/" : "")
                : joinPaths([basename, pathname]);
    }

    // 可以看做, 路由的拼接, 包括 ? , #
    return navigator.createHref({ pathname: joinedPathname, search, hash });
}

resolveTo

解析toArg, 返回物件

function resolveTo(
    toArg: To,
    routePathnames: string[],
    locationPathname: string
): Path {
    // parsePath上面已經分析過了
    let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
    let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname;

    let from: string;
    if (toPathname == null) {
        from = locationPathname;
    } else {
        let routePathnameIndex = routePathnames.length - 1;

        // 如果以 .. 開始的路徑
        if (toPathname.startsWith("..")) {
            let toSegments = toPathname.split("/");

            // 去除 ..
            while (toSegments[0] === "..") {
                toSegments.shift();
                routePathnameIndex -= 1;
            }

            to.pathname = toSegments.join("/");
        }

        // from 複製
        from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
    }

    // 解析, 返回物件
    let path = resolvePath(to, from);

    if (
        toPathname &&
        toPathname !== "/" &&
        toPathname.endsWith("/") &&
        !path.pathname.endsWith("/")
    ) {
        path.pathname += "/";
    }
    // 確保加上末尾 /

    return path;
}

resolveTo-resolvePath

返回一個相對於給定路徑名的解析路徑物件, 這裡的函式也基本都講過

function resolvePath(to: To, fromPathname = "/"): Path {
    let {
        pathname: toPathname,
        search = "",
        hash = ""
    } = typeof to === "string" ? parsePath(to) : to;

    let pathname = toPathname
        ? toPathname.startsWith("/")
            ? toPathname
            // resolvePathname
            : resolvePathname(toPathname, fromPathname)
        : fromPathname;

    return {
        pathname,
        search: normalizeSearch(search),
        hash: normalizeHash(hash)
    };
}

resolveTo-resolvePath-resolvePathname

function resolvePathname(relativePath: string, fromPathname: string): string {
    // 去除末尾斜槓, 再以斜槓分割成陣列
    let segments = fromPathname.replace(/\/+$/, "").split("/");
    let relativeSegments = relativePath.split("/");

    relativeSegments.forEach(segment => {
        if (segment === "..") {
            // 移除 ..
            if (segments.length > 1) segments.pop();
        } else if (segment !== ".") {
            segments.push(segment);
        }
    });

    return segments.length > 1 ? segments.join("/") : "/";
}

useLocation useNavigationType

function useLocation(): Location {
    // 只是獲取 context 中的資料
    return React.useContext(LocationContext).location;
}

同上

function useNavigationType(): NavigationType {
    return React.useContext(LocationContext).navigationType;
}

useMatch


function useMatch<
    ParamKey extends ParamParseKey<Path>,
    Path extends string
    >(pattern: PathPattern<Path> | Path): PathMatch<ParamKey> | null {
    // 獲取 location.pathname
    let { pathname } = useLocation();
    // matchPath  在 useRoutes-matchRoutes-matchRouteBranch-matchPath 中講到過
    // 對一個URL路徑名進行模式匹配,並返回有關匹配的資訊。
    return React.useMemo(
        () => matchPath<ParamKey, Path>(pattern, pathname),
        [pathname, pattern]
    );
}

useNavigate

此 hooks 是用來獲取操作路由物件的

function useNavigate(): NavigateFunction {
    // 從 context 獲取資料
    let { basename, navigator } = React.useContext(NavigationContext);
    let { matches } = React.useContext(RouteContext);
    let { pathname: locationPathname } = useLocation();
    // 轉成 json, 方便 memo 對比
    let routePathnamesJson = JSON.stringify(
        matches.map(match => match.pathnameBase)
    );
    let activeRef = React.useRef(false);
    React.useEffect(() => {
        activeRef.current = true;
    }); // 控制渲染, 需要在渲染完畢一次後操作
    
    // 路由操作函式
    let navigate: NavigateFunction = React.useCallback(
        (to: To | number, options: NavigateOptions = {}) => {
            if (!activeRef.current) return; // 控制渲染
            // 如果 go 是數字, 則結果類似於 go 方法
            if (typeof to === "number") {
                navigator.go(to);
                return;
            }
            // 解析go
            let path = resolveTo(
                to,
                JSON.parse(routePathnamesJson),
                locationPathname
            );
            if (basename !== "/") {
                path.pathname = joinPaths([basename, path.pathname]);
            }
            // 這一塊 就是 前一個括號產生函式, 後一個括號傳遞引數
            // 小小地轉換下:
            // !!options.replace ? 
            //     navigator.replace(
            //         path,
            //         options.state
            //     )
            //     : navigator.push(
            //         path,
            //         options.state
            //     )
            //
            (!!options.replace ? navigator.replace : navigator.push)(
                path,
                options.state
            );
        },
        [basename, navigator, routePathnamesJson, locationPathname]
    );
    // 最後返回
    return navigate;
}

generatePath

返回一個有引數插值的路徑。 原理還是通過正則替換

function generatePath(path: string, params: Params = {}): string {
    return path
        .replace(/:(\w+)/g, (_, key) => {
            return params[key]!;
        })
        .replace(/\/*\*$/, _ =>
            params["*"] == null ? "" : params["*"].replace(/^\/*/, "/")
        );
}

他的具體使用:

generatePath("/users/:id", { id: 42 }); // "/users/42"
generatePath("/files/:type/*", {
  type: "img",
  "*": "cat.jpg"
}); // "/files/img/cat.jpg"

這裡的程式碼可以說是覆蓋整個 react-router 80%以上, 有些簡單的, 用處小的這裡也不再過多贅述了

參考文件:

相關文章