示例
之前有过分析react大体的渲染和更新,这里再次看看react配套的react-router是如何实现其路由系统的。
下面是一个react-router使用的简单例子。
1 2 3 4 5 6 7 8
| import { BrowserRouter as Router, Route } from 'react-router-dom'
<Router> <div> <Route exact path="/" component={Home}/> <Route path="/news" component={NewsFeed}/> </div> </Router>
|
简要分析
BrowserRouter组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class BrowserRouter extends React.Component { static propTypes = { basename: PropTypes.string, forceRefresh: PropTypes.bool, getUserConfirmation: PropTypes.func, keyLength: PropTypes.number, children: PropTypes.node }; history = createHistory(this.props); componentWillMount() { warning( !this.props.history, "<BrowserRouter> ignores the history prop. To use a custom history, " + "use `import { Router }` instead of `import { BrowserRouter as Router }`." ); } render() { return <Router history={this.history} children={this.props.children} />; } }
|
这里BrowserRouter
组件返回Router
组件,带了history && children
两个props。
Router组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| class Router extends React.Component { static propTypes = { history: PropTypes.object.isRequired, children: PropTypes.node };
static contextTypes = { router: PropTypes.object };
static childContextTypes = { router: PropTypes.object.isRequired };
getChildContext() { return { router: { ...this.context.router, history: this.props.history, route: { location: this.props.history.location, match: this.state.match } } }; }
state = { match: this.computeMatch(this.props.history.location.pathname) };
computeMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; }
componentWillMount() { const { children, history } = this.props;
invariant( children == null || React.Children.count(children) === 1, "A <Router> may have only one child element" );
this.unlisten = history.listen(() => { this.setState({ match: this.computeMatch(history.location.pathname) }); }); }
componentWillReceiveProps(nextProps) { warning( this.props.history === nextProps.history, "You cannot change <Router history>" ); }
componentWillUnmount() { this.unlisten(); }
render() { const { children } = this.props; return children ? React.Children.only(children) : null; } }
|
Router这块的render没有太多内容,仅仅是判定children里面的仅有一个有效,这样返回这个Route,否则就会返回null。
但是这里有一个特别重要的定义getChildContext
,他就是传说中的context
,一个几乎所有流行库都在用,但是自己业务逻辑上可能永远都用不到的设定。
当它设定了这个getChildContext
之后,其所有子组件都可以访问到里面的这个对象。这里需要注意的是,当设定好了这个,那么childContextTypes
的设定,也就是必不可少的了。
Route组件代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| class Route extends React.Component { static propTypes = { computedMatch: PropTypes.object, path: PropTypes.string, exact: PropTypes.bool, strict: PropTypes.bool, sensitive: PropTypes.bool, component: PropTypes.func, render: PropTypes.func, children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), location: PropTypes.object };
static contextTypes = { router: PropTypes.shape({ history: PropTypes.object.isRequired, route: PropTypes.object.isRequired, staticContext: PropTypes.object }) };
static childContextTypes = { router: PropTypes.object.isRequired };
getChildContext() { return { router: { ...this.context.router, route: { location: this.props.location || this.context.router.route.location, match: this.state.match } } }; }
state = { match: this.computeMatch(this.props, this.context.router) };
computeMatch( { computedMatch, location, path, strict, exact, sensitive }, router ) { if (computedMatch) return computedMatch; const { route } = router; const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match); }
componentWillMount() { }
componentWillReceiveProps(nextProps, nextContext) { this.setState({ match: this.computeMatch(nextProps, nextContext.router) }); }
render() { const { match } = this.state; const { children, component, render } = this.props; const { history, route, staticContext } = this.context.router; const location = this.props.location || route.location; const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children)) return React.Children.only(children);
return null; } }
|
这里主要的更新流程是这样的:
Router
设定getChildContext
,然后使用观察者模式(history.listen
)调用state更新。而state更新之后,我们getChildContext
返回的context
也会随之变更,并触发updates流程
Route
组件收到更新通知后componentWillReceiveProps
里面同样也会触发这个getChildContext
返回值的变更并触发下级的更新。
简单使用
router的使用大致是有两周,一种是组件Link
式跳转,一种是编程式跳转。
Link跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import React from "react"; import PropTypes from "prop-types"; import invariant from "invariant"; import { createLocation } from "history";
class Link extends React.Component { static propTypes = { onClick: PropTypes.func, target: PropTypes.string, replace: PropTypes.bool, to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) };
static defaultProps = { replace: false };
static contextTypes = { router: PropTypes.shape({ history: PropTypes.shape({ push: PropTypes.func.isRequired, replace: PropTypes.func.isRequired, createHref: PropTypes.func.isRequired }).isRequired }).isRequired };
handleClick = event => { if (this.props.onClick) this.props.onClick(event);
if ( !event.defaultPrevented && event.button === 0 && !this.props.target && !isModifiedEvent(event) ) { event.preventDefault();
const { history } = this.context.router; const { replace, to } = this.props;
if (replace) { history.replace(to); } else { history.push(to); } } };
render() { const { replace, to, innerRef, ...props } = this.props; const { history } = this.context.router; const location = typeof to === "string" ? createLocation(to, null, null, history.location) : to;
const href = history.createHref(location); return ( <a {...props} onClick={this.handleClick} href={href} ref={innerRef} /> ); } }
export default Link;
|
这里可以看出,Link
实质上是捕获了context.router
,直接调用的context.router.history.push && context.router.history.replace
进行跳转。
编程式导航
编程式导航也可以很容易理解。当我们做了一个交互,满足一定条件使用代码来跳转。
本质上,他还是对context.router.history
的方法的引用。
不过这里一般可以使用withRouter
来对组件进行包裹(如果还有redux那么withRouter(connect(...)(MyComponent))
)。withRouter
是一个高阶组件,他给组件加上了context.router
的引用并作为props传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const withRouter = Component => { const C = props => { const { wrappedComponentRef, ...remainingProps } = props; return ( <Route children={routeComponentProps => ( <Component {...remainingProps} {...routeComponentProps} ref={wrappedComponentRef} /> )} /> ); };
C.displayName = `withRouter(${Component.displayName || Component.name})`; C.WrappedComponent = Component; C.propTypes = { wrappedComponentRef: PropTypes.func };
return hoistStatics(C, Component); };
|
然后就和Link
组件里面走一致的操作就好。
未完待续。。。
// TODO: 需要完善一下整体脉络
这里暂时先这样,去年到今年留下好多文章没有发到blog,再不批量传上来可能就不会传了…