需要实现的组件或方法
- 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>
)
}