react-router-dom 原始碼閱讀

Grewer發表於2021-12-24
這次的版本是 6.0.2

這裡只講 react-router-dom 提供的 API, 像是 Routes, Router 這些都是 react-router 提供的

BrowserRouter, HashRouter

BrowserRouter 和 hashRouter 的主要區別就在於使用的路由 API

簡單解釋

BrowserRouter

它使用了 history 庫 的API,也就是說,瀏覽器(IE 9和更低版本以及同時代的瀏覽器)是不可用的。
客戶端React應用程式能夠維護乾淨的路由,如 example.com/react/route ,但需要得到Web伺服器的支援。
這需要Web伺服器應該被配置為單頁應用程式,即為/react/route路徑或伺服器上的任何其他路由提供相同的index.html。

HashRouter

它使用URL雜湊,對支援的瀏覽器或網路伺服器沒有限制, 如 example.com/#/react/route.

效果是所有後續的URL路徑內容在伺服器請求中被忽略(即你傳送 "www.mywebsite.com/#/person/john",伺服器得到 "www.mywebsite.com"。
因此,伺服器將返回前#URL響應,然後後#路徑將由你的客戶端反應程式進行解析處理。

程式碼解析

先說 hashRouter , 他的依賴度是最低的, 程式碼也很簡單:

import {createHashHistory} from "history";

function HashRouter({basename, children, window}: HashRouterProps) {
    let historyRef = React.useRef<HashHistory>(); // 用來儲存 createHashHistory 結果 
    if (historyRef.current == null) {
        historyRef.current = createHashHistory({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}
        />
    );
}

這裡需要了解的一個 API 是 createHashHistory, 他來自於 history 倉庫, 這裡我們需要解析一下這個方法:

/**
 * 此方法裡並不是全部的原始碼, 省略了部分不太 core 的程式碼
 */
function createHashHistory(
    options: HashHistoryOptions = {}
): HashHistory {
    let {window = document.defaultView!} = options; // window 是傳遞的引數
    let globalHistory = window.history; // 全域性的 history 物件

    // 獲取當前 state.idx 和 location 物件
    function getIndexAndLocation(): [number, Location] {
        let {
            pathname = '/',
            search = '',
            hash = ''
        } = parsePath(window.location.hash.substr(1)); // 解析 hash
        let state = globalHistory.state || {};
        return [
            state.idx,
            readOnly<Location>({
                pathname,
                search,
                hash,
                state: state.usr || null,
                key: state.key || 'default'
            })
        ];
    }

    let blockedPopTx: Transition | null = null;

    // pop 的操作 最終呼叫的是 go() 函式
    function handlePop() {
        // 省略
    }

    // popstate 事件監聽
    window.addEventListener(PopStateEventType, handlePop);

    // hashchange 事件監聽  ie11 中存在問題
    window.addEventListener(HashChangeEventType, () => {
        let [, nextLocation] = getIndexAndLocation();

        // 忽略外部的 hashchange 事件  createPath = pathname + search + hash 
        if (createPath(nextLocation) !== createPath(location)) {
            handlePop();
        }
    });

    // Action 是一個 列舉, Pop = 'POP'
    let action = Action.Pop;
    let [index, location] = getIndexAndLocation();

    /**
     *  createEvents 方法
     *  一個閉包方法, 維護一個陣列,類似觀察者模式, 返回 push, call 兩個方法
     */
    let listeners = createEvents<Listener>();
    let blockers = createEvents<Blocker>();
    
    // 常用的 push 方法
    function push(to: To, state?: any) {
        let nextAction = Action.Push; // 列舉 Action.Push = 'PUSH'
        let nextLocation = getNextLocation(to, state); // 生成一個新的 location 物件

        function retry() {
            push(to, state);
        }

        // blockers 為空的時候
        if (allowTx(nextAction, nextLocation, retry)) {
            // 根據 location 生成需要的物件, 只是資料格式更改了下
            /* historyState = {
                usr: nextLocation.state,
                key: nextLocation.key,
                idx: index
            }*/
            let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);

            try {
                // 呼叫原生 API, history.pushState
                globalHistory.pushState(historyState, '', url);
            } catch (error) {
                // 不相容就使用這個
                window.location.assign(url);
            }
            applyTx(nextAction); // listeners 中新增回撥 nextAction
        }
    }

    function replace(to: To, state?: any) {
        // 同 push, 只不過呼叫的原生改成了這個  globalHistory.replaceState(historyState, '', url);
    }

    function go(delta: number) { // 原生 go 方法
        globalHistory.go(delta);
    }

    let history: HashHistory = { // 定義的區域性 history 物件, 最後要返回的
        get action() {
            return action;
        },
        get location() {
            return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
            go(-1);
        },
        forward() {
            go(1);
        },
        listen(listener) {
            return listeners.push(listener);
        },
        block(blocker) {
            let unblock = blockers.push(blocker);

            if (blockers.length === 1) {
                window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
            }

            return function () {
                // 在頁面 UnMount 的時候呼叫
                unblock();
                if (!blockers.length) {
                    window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
                }
            };
        }
    };

    return history;
}

Link

一個經常用到的小元件, 常用來做跳轉


const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
    function LinkWithRef(
        { onClick, reloadDocument, replace = false, state, target, to, ...rest },
        ref
    ) {
        // useHref 來自於 react-router 中, 用來 parse URL
        let href = useHref(to);
        
        // 真實點選跳轉呼叫的函式, 具體原始碼在下面給出
        let internalOnClick = useLinkClickHandler(to, { replace, state, target });
        
        // 點選 a 標籤的控制程式碼, 如果有 onClick 事件 則優先
        function handleClick(
            event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
        ) {
            if (onClick) onClick(event);
            if (!event.defaultPrevented && !reloadDocument) {
                internalOnClick(event);
            }
        }

        return (
            <a
                {...rest}
                href={href}
                onClick={handleClick}
                ref={ref}
                target={target}
            />
        );
    }
);

useLinkClickHandler

來看看這個 hooks 的具體構成

export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
    to: To,
    {
        target,
        replace: replaceProp,
        state
    }: {
        target?: React.HTMLAttributeAnchorTarget;
        replace?: boolean;
        state?: any;
    } = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
    // 來源於 react-router, 獲取 navigate 函式, 可以用來跳轉
    let navigate = useNavigate();
    // 獲取當前的 location 物件(非 window.location)
    let location = useLocation();
    // 同樣來源於react-router,   解析 to, 獲取 path
    let path = useResolvedPath(to);

    return React.useCallback(
        (event: React.MouseEvent<E, MouseEvent>) => {
            if (
                event.button === 0 && // 忽略除了左鍵點選以外的, 參考: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
                (!target || target === "_self") && 
                !isModifiedEvent(event) // 忽略各類鍵盤按鈕, 參考 https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
                //  isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
            ) {
                event.preventDefault();

                // 對比是否有變化
                let replace =
                    !!replaceProp || createPath(location) === createPath(path);

                navigate(to, { replace, state });
            }
        },
        [location, navigate, path, replaceProp, state, target, to]
    );
}

NavLink

等於是對 Link 元件的包裝

const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
    function NavLinkWithRef(
        {
            "aria-current": ariaCurrentProp = "page",
            caseSensitive = false,
            className: classNameProp = "",
            end = false,
            style: styleProp,
            to,
            ...rest
        },
        ref
    ) {
        // 這兩個 hooks 上述已經說過
        let location = useLocation();
        let path = useResolvedPath(to);

        let locationPathname = location.pathname;
        let toPathname = path.pathname;
        if (!caseSensitive) { // 支援字串大小寫不敏感
            locationPathname = locationPathname.toLowerCase();
            toPathname = toPathname.toLowerCase();
        }

        // 是否 active
        // /user =>  /user/name 
        let isActive =
            locationPathname === toPathname ||
            (!end &&
                locationPathname.startsWith(toPathname) &&
                locationPathname.charAt(toPathname.length) === "/");
        
        // aria 是幫助殘障人士輔助閱讀的
        let ariaCurrent = isActive ? ariaCurrentProp : undefined;

        // class 樣式計算
        let className: string;
        if (typeof classNameProp === "function") {
            className = classNameProp({ isActive });
        } else {
            className = [classNameProp, isActive ? "active" : null]
                .filter(Boolean)
                .join(" ");
        }

        let style =
            typeof styleProp === "function" ? styleProp({ isActive }) : styleProp;

        return (
            <Link
                {...rest}
                aria-current={ariaCurrent}
                className={className}
                ref={ref}
                style={style}
                to={to}
            />
        );
    }
);

useSearchParams

用來獲取/設定 query 的 hooks

export function useSearchParams(defaultInit?: URLSearchParamsInit) {

    // createSearchParams 的原始碼下面會講, 大體是包裝了 URLSearchParams 
    // 相關知識點: https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams
    let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit));

    // 獲取 location 
    let location = useLocation();
    
    // 解析, 通過對比 更新
    let searchParams = React.useMemo(() => {
        let searchParams = createSearchParams(location.search);

        for (let key of defaultSearchParamsRef.current.keys()) {
            if (!searchParams.has(key)) {
                defaultSearchParamsRef.current.getAll(key).forEach(value => {
                    searchParams.append(key, value);
                });
            }
        }

        return searchParams;
    }, [location.search]);

    let navigate = useNavigate();
    // 通過 navigate 方法 實現 location.search 的變更
    let setSearchParams = React.useCallback(
        (
            nextInit: URLSearchParamsInit,
            navigateOptions?: { replace?: boolean; state?: any }
        ) => {
            // URLSearchParams toString 就成了 query 格式
            navigate("?" + createSearchParams(nextInit), navigateOptions);
        },
        [navigate]
    );

    return [searchParams, setSearchParams] as const;
}

createSearchParams


function createSearchParams(
    init: URLSearchParamsInit = ""
): URLSearchParams {
    // 通過原生 api 建立, 陣列的話 就類似於 tuple
    return new URLSearchParams(
        typeof init === "string" ||
        Array.isArray(init) ||
        init instanceof URLSearchParams
            ? init
            : Object.keys(init).reduce((memo, key) => {
                let value = init[key];
                return memo.concat(
                    Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]
                );
            }, [] as ParamKeyValuePair[])
    );
}

總結

react-router-dom 其實就像 react-router 的再度包裝, 給開發提供了良好的基礎設施

引用

相關知識點彙總

相關文章