react-route

示例

之前有过分析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, // private, from <Switch>
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; // <Switch> already computed the match for us
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 && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
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,再不批量传上来可能就不会传了…