React-router v6.0.0-alpha.3源码解读

1,180 阅读4分钟
开始之前先解决github的示例项目报错的问题(history的版本不是最新的,2020/04/04版本) 


首先看一下v6版本相对于v5版本的改动点 

 1、v5版本的 Switch 被废弃, v6使用 Routes 来替代 Switch 的功能

Routes组件源码:

export function Routes({ 
    basename = '', 
    caseSensitive = false, 
    children 
}) {  
    let routes = createRoutesFromChildren(children);  
    console.log(routes);  
    return useRoutes(routes, basename, caseSensitive);
}

可以看到这个组件首先处理 children 以生成符合v6格式的 routes 对象

举个🌰, 给一个官方文档给出的示例

<Routes>    
    <Route path="/" element={<Home />} />    
    <Route path="about" element={<About />} />    
    <Route path="courses" element={<Courses />}>        
    <Route path="/" element={<CoursesIndex />} />        
        <Route path="react-fundamentals/*" element={<ReactFundamentals />} />        
        <Route path="advanced-react/*" element={<AdvancedReact />} />    
    </Route>
</Routes>

生成的 routes 的对象为


关于 createRoutesFromChildren 函数, 这里就不多讲了, 就是通过递归的方式生成routes数组,有一点需要提到的是该函数内部做了React.Fragment的 "穿透" 处理

2、把v5版本满足路由匹配的 Component 属性修改为 element 属性, 传递的形式也略有不同, 

<Route path="advanced-react/*" element={<AdvancedReact />} />

可以直接对匹配的组件传递参数,不用再像v5版本一样使用renderProps的方式

3、v5版本的路由跳转通常使用 history.go 和 history.push / replace 来做, v6版本 提供了useNavigate 的自定义hook函数

(源码省略部分错误输出逻辑)

export function useNavigate() {  
    let { pathname } = React.useContext(RouteContext);  
    let locationContext = React.useContext(LocationContext);   
    let { history, pending } = locationContext;  
    let activeRef = React.useRef(false);  
    React.useEffect(() => {    
        activeRef.current = true;  
    });  
    let navigate = React.useCallback(    
          (to, { replace, state } = {}) => {     
                if (activeRef.current) {        
                    if (typeof to === 'number') {          
                        history.go(to);        
                    } 
                    else {          l
                        let relativeTo = resolveLocation(to, pathname);         
                        // If we are pending transition, use REPLACE instead of PUSH.          
                        // This will prevent URLs that we started navigating to but          
                        // never fully loaded from appearing in the history stack.          
                        let method = !!replace || pending ? 'replace' : 'push';          
                        history[method](relativeTo, state);        
                    }      
                }    
            },   [history, pathname, pending]);  
    return navigate;
}

emmm(没什么太大的改变)

4、v5版本的 Redirect 组件 替换为 v6 的 Navigate 组件

(源码同样省略部分错误处理逻辑)

export function Navigate({ to, replace, state }) {  
    let navigate = useNavigate();  
    let locationContext = React.useContext(LocationContext);  
    React.useEffect(() => {    
        navigate(to, { replace, state });  
    });  
    return null;
}

采用React Hooks的 useEffect 方法在组件挂载完毕后调用navigate达到重定向的目的

5、v6版本在Route组件嵌套的情况下 path的取值是父级的pathname再加上当前组件的path,而不需要像v5版本的那样使用useRouteMatch获取match对象,然后使用match.url 和 match.path来手动拼接pathname


那么针对文章最初给出的示例, 新的v6版本是怎么一步一步的匹配解析达到我们想要的路由效果的呢? 下边我们就来看一下最核心的 useRoutes 函数(关键的地方做了标注)

export function useRoutes(
    routes, 
    basename = '', 
    caseSensitive = false
) {  
    let {    
        params: parentParams,    
        pathname: parentPathname,    
        route: parentRoute,  
     } = React.useContext(RouteContext);  
     basename = basename ? joinPaths([parentPathname, basename]) : parentPathname;  
     let location = useLocation(); // 获取location对象  
     let matches = React.useMemo(    
         () => matchRoutes(routes, location, basename, caseSensitive),    
         [routes, location, basename, caseSensitive]  
     );  
     if (!matches) {    
         // TODO: Warn about nothing matching, suggest using a catch-all route.    
         return null;  
     } 
    // 如果存在路由匹配的情况  
    // 这里解密了我们在Route组件里获取到的 outlet 到底是什么东西  
    // 其实它就是更底层级的已经匹配到的 Route 组件!!!  
    // 这里的生成也是从由向右向左生成的一个嵌套了多层的RouteContext结构,为的就是保证按照最近获取的原则  
    // 获取到正确的context对象  
    let element = matches.reduceRight((outlet, { params, pathname, route }) => {   
        return (      
            <RouteContext.Provider        
                children={route.element}        
                value={{          
                    outlet,          
                    params: readOnly({ ...parentParams, ...params }),          
                    pathname: joinPaths([basename, pathname]),          
                    route,        
                }}      
            />    
        );  
    }, null);  
    return element;
}

export function matchRoutes(  
    routes,  
    location,  
    basename = '',  
    caseSensitive = false
) {  
    if (typeof location === 'string') {    
        location = parsePath(location);  
    }  
    // TODO: Validate location  
    // - it should have a pathname  
    let base = basename.replace(/^\/+|\/+$/g, ''); // 删除开头和结尾多余的 '/' 符号  
    let target = location.pathname.slice(1); // 去掉开头的 '/'  
    if (base) {    
        // 计算剩余要匹配的路由部分    
        if (base === target) {      
            target = ''; // -- 无剩余路由匹配    
        } else if (target.startsWith(base)) {      
            target = target.slice(base.length).replace(/^\/+/, '');    
        } else {      
            // 路由不匹配      
            return null;    
        }  
    }  
    let flattenedRoutes = flattenRoutes(routes); // 将routes数组flat成一维的数组  
    rankFlattenedRoutes(flattenedRoutes);  
    // 找到第一个满足路由匹配规则的 Route 组件并返回  
    for (let i = 0; i < flattenedRoutes.length; ++i) {    
        // 返回当前的Route组件的path对象和在其之前的所有的父级Route组件和该组件自身组成的数组    
        let [path, flatRoutes] = flattenedRoutes[i];    
        // TODO: Match on search, state too    
        // 对path做一些正则方面的特殊处理, 返回新创建的用来匹配路由的正则对象    
        // 正则的形式是 ^(...) 的格式 这也解释了为什么下文生成pathname的时候使用了match[1]的写法    
        let [matcher] = compilePath(path, /* end */ true, caseSensitive);    
        if (matcher.test(target)) {      
            // 如果当前的Route组件匹配了路由,      
            return flatRoutes.map((route, index) => {        
                let routes = flatRoutes.slice(0, index + 1);        
                let path = joinPaths(routes.map(r => r.path));        
                let [matcher, keys] = compilePath(path, /* end */ false, caseSensitive);        
                let match = target.match(matcher);        
                let pathname = '/' + match[1];        
                let values = match.slice(2);        
                let params = keys.reduce((memo, key, index) => {          
                    memo[key] = safelyDecodeURIComponent(values[index], key);          
                    return memo;        
                }, {});        
                return { params, pathname, route };      
        });    
    }  
  }  
    return null;
}