手写react-router2-Switch、Link、withRouter、hooks

165 阅读2分钟
class Swith extends React.Component{
    static contextType = RouterContext
    render(){
        const {context}= this;
        const {children}=this.props;
        console.log(children);
        const {location} = context;
        let element,match;
        React.Children.forEach(children,child=>{
            //child $$typeof == Symbol('react.element')
            if(React.isValidElement(child)){//如果此节点是一个React元素
                if(!match){//如果尚未有任何元素匹配
                    element = child;
                    match = matchPath(location.pathname,child.props);
                }
            }
        });
        return match?React.cloneElement(element,{computedMatch:match}):null;
    }
}
class Route extends React.Component{
    static contextType = RouterContext;
    render(){
        const {history,location} = this.context;
        const {path,component:RouteComponent,exact=false,computedMatch} = this.props;
        //const match = exact?location.pathname===path:location.pathname.startsWith(path);// /user /user
        //const match  = matchPath(location.pathname,this.props);
        const match = computedMatch?computedMatch:matchPath(location.pathname,this.props);
        const routeProps = {history,location};
        let renderElement=null;// null也一个合法的react渲染节点 代表我们render的返顺值,代表此组件将要渲染的内容
        if(match){
            routeProps.match=match;
            //React.createElement(RouteComponent,routeProps);
            renderElement = <RouteComponent {...routeProps}/>
        }
        return renderElement
    }
}

Switch和Route通常是嵌套关系,例如:

    <Switch>
      <Route path="/"  component={Home} exact={true}/>
      <Route path="/user" component={User}/>
      <Route path="/profile" component={Profile}/>
      <Redirect to="/"/>
    </Switch>

Switch中会根据当前的location从children中过滤出来匹配的项进行渲染,但Route本身也可以单独使用,所以它内部也有获取当前location,再判断Route是否匹配这个location,这部分逻辑是重复的,所以Switch会将computedMatch这样一个属性传给Route

通常在最后会放一个Redirect组件,用于前面所有Route都匹配不到时跳转到一个指定路径来用

function Redirect({to}){
  return (
      <RouterContext.Consumer>
          {
              value=>{
                  const {history} = value;
                 /*  history.push(to);
                  return null; */
                  return <Lifecycle onMount={()=>history.push(to)}/>
              }
          }
      </RouterContext.Consumer>
  )
}

export default Redirect;
import React from 'react';

class Lifecycle  extends React.Component{
    componentDidMount(){
        if(this.props.onMount)
            this.props.onMount(this);
    }
    render(){
        return null;
    }
}
export default Lifecycle;

Link:

import React from 'react'
import {__RouterContext as RouterContext} from '../react-router';
export default function Link(props){
    return (
        <RouterContext.Consumer>
            {
                value=>{
                    return (
                        <a
                          {...props}
                          onClick={(event)=>{
                            event.preventDefault();
                            value.history.push(props.to);
                          }}
                        >{props.children}</a>
                    )
                }
            }
        </RouterContext.Consumer>
    )
}

如果想让一个普通组件获得history这样的路由组件才有的属性,则需要使用withRouter包装一个高阶组件:

例如,希望下面的NavHeader组件有history属性:

class NavHeader extends Component {
    render() {
        return (
            <div onClick={()=>this.props.history.push('/')}>
                {this.props.title}
            </div>
        )
    }
}

export default withRouter(NavHeader);

就可以通过withRouter包装

import {Route} from './';

export default function withRouter(OldComponent) {
    //TODO
    return (
        (props)=><Route render={
            routeProps=><OldComponent {...routeProps} {...props}/>
        }/>
    )
}

源码中withRouter的实现方式:

function withRouter(OldComponent) {
    function NewComponent(props){
        return (
            <RouterContext.Consumer>
                {
                    value=>{
                        return <OldComponent {...value} {...props}/>
                    }
                }
            </RouterContext.Consumer>
        )
    }
    return NewComponent
}
export default withRouter;

hooks:

import React from 'react';
import RouterContext from './RouterContext';
import matchPath from './matchPath';

export function useParams(){
    let match = React.useContext(RouterContext).match;
    return match?match.params:{};
}
export function useHistory(){
     return React.useContext(RouterContext).history;
}

export function useLocation(){
    return React.useContext(RouterContext).location;
}

export function useRouteMatch(options){//TODO
    const location = useLocation();
    return matchPath(location.pathname,options);
}