实现react-router-dom的组件和方法

192 阅读2分钟

需要实现的组件或方法

  • BrowserRouter
  • Link
  • Router
  • Route
  • Switch
  • useHistory
  • useParams
  • useLocation
  • useRouteMatch
  • withRouter

实现过程:

1. 搭架子 BrowserRouter + Link + Route

function App() {
  return (
    <BrowserRouter>
      <Link to="/index">link</Link>
      <Route children={<Product />}></Route>
    </BrowserRouter>
  )
}
class BrowserRouter extends React.Component {
  render() {
    return this.props.children
  }
}
class Route extends Component {
  render() {
    const { children, component, render } = this.props
    return children
      ? typeof children === 'function'
        ? children()
        : children
      : component
        ? createElement(component)
        : render
          ? render()
          : null
  }
}
const Link = (props) => {
  return <a href={props.to}>{props.children}</a>
}

2. Link 改变地址栏

点击Link需要改变地址栏地址,我们用react-router的createBrowserHistory方法实现

const Link = ({ to, children }) => {
  const history = createBrowserHistory()
  const handle = (e) => {
    e.preventDefault();
    history.push(to)
  }
  return <a href={to} onClick={handle}>{children}</a>
}

保存history的地方应该放在最外层,并通过context传递下来,我们来改造下

class BrowserRouter extends React.Component {
  constructor(props) {
    super(props)
    this.history = createBrowserHistory();
  }
  render() {
    return (
      <RouterContext.Provider value={{ history: this.history }}>
        {this.props.children}
      </RouterContext.Provider>
    )
  }
}
const Link = ({ to, children }) => {
  const context = useContext(RouterContext)
  const handle = (e) => {
    e.preventDefault();
    context.history.push(to)
  }
  return <a href={to} onClick={handle}>{children}</a>
}

3. Route 根据地址匹配渲染

现在点击link可以往history push url,但route组件还需要增加listen来监听路由变化和匹配path值来作出渲染改变。

首次渲染先根据路由地址location和path参数得出匹配的route,location变化时listen会通过setState触发重新渲染。location的变化并触发重新渲染我们可以放到最外层,并由context将变化传到子组件。

我们来改造下router和route组件

match方法这里先不实现了,直接从react-router-dom里引用,match要作为参数传给下一个组件,并在最外层声明一个初始match值

class BrowserRouter extends React.Component {
  constructor(props) {
    super(props)
    this.history = createBrowserHistory();
    this.state = {
      location: { location: this.history.location }
    }
    this.history.listen((location) => {
      this.setState({ location })
    })
  }
  render() {
    return (
      <RouterContext.Provider value={{ history: this.history, location: this.state.location }}>
        {this.props.children}
      </RouterContext.Provider>
    )
  }
}
class Route extends Component {
  render() {
    return <RouterContext.Consumer>
      {
        (context) => {
          const { children, component, render, path } = this.props
          const match = context.location.pathname === path
          return match ?
            children
              ? typeof children === 'function'
                ? children()
                : children
              : component
                ? createElement(component)
                : render
                  ? render()
                  : null
            : typeof children === 'function'
              ? children()
              : null
        }
      }
    </RouterContext.Consumer>

  }
}

4. Switch组件

接下来实现Switch组件,被Switch包裹的Route组件只会渲染被匹配到的第一个组件。

class Switch extends React.Component {
  render() {
    return <RouterContext.Consumer>
      {
        (context) => {
          const { location } = context
          let element, match;
          //拿到子组件的vNode,不同的子组件不统一,比如class,funciton,原生,React.Children使它统一
          React.Children.forEach(this.props.children, (child) => {
            if (match == null && React.isValidElement(child)) {
              element = child;
              match = child.props.path
                ? matchPath(location.pathname, child.props)
                : context.match;
            }
          })
          //选用cloneElement保留原有props,并添加新的props
          return match ? React.cloneElement(element, { computedMatch: match }) : null
        }
      }
    </RouterContext.Consumer>
  }
}

5. hooks

useHistory,useParams,useLocation,useRouteMatch,利用context和hook提取需要的值。

export function useHistory() {
  return useContext(RouterContext).history
}
export function useLocation() {
  return useContext(RouterContext).location
}
export function useRouteMatch() {
  return useContext(RouterContext).match
}
export function useParams() {
  const match = useContext(RouterContext).match;
  return match ? match.params : {};
}

6. withRouter

const withRouter = (WrappedComponent) => (props) => {
  return (
    <RouterContext.Consumer>
      {(context) => {
        return <WrappedComponent {...props} {...context} />
      }}
    </RouterContext.Consumer>
  )
}