zoukankan      html  css  js  c++  java
  • reactrouterdom 源码阅读

    这次的版本是 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;
    }
    
    

    一个经常用到的小组件, 常用来做跳转

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

    等于是对 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 的再度包装, 给开发提供了良好的基础设施

    引用

    相关知识点汇总

  • 相关阅读:
    centos7 安装redis服务及phpredis扩展
    centos7 LANMP 安装
    php reids 单机命令
    浅谈正向代理和反向代理
    php-fpm日志告警"seem busy"
    PHP-redis中文文档
    Nginx配置文件nginx.conf中文详解
    生成带参数的微信二维码
    微信临时二维码时为32位非0整型
    [Linux]Service mysql start出错(mysql: unrecognized service)解决方法
  • 原文地址:https://www.cnblogs.com/Grewer/p/15725684.html
Copyright © 2011-2022 走看看