路由即组件
路由即组件
在
const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Topic = ({ topicId }) => <h3>{topicId}</h3>;
const Topics = ({ match }) => {
const items = [
{ name: "Rendering with React", slug: "rendering" },
{ name: "Components", slug: "components" },
{ name: "Props v. State", slug: "props-v-state" }
];
return (
<div>
<h2>Topics</h2>
<ul>
{items.map(({ name, slug }) => (
<li key={name}>
<Link to={`${match.url}/${slug}`}>{name}</Link>
</li>
))}
</ul>
{items.map(({ name, slug }) => (
<Route
key={name}
path={`${match.path}/${slug}`}
render={() => <Topic topicId={name} />}
/>
))}
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
};
const App = () => (
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li> <li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
);
上述代码中Route
会在当前path
属性值相符的时候渲染相关组件,而 Link
提供了声明式的,易使用的方式来在应用内进行跳转。换言之,Link
组件允许你更新当前Route
组件则是根据
路由组件
我们首先来考量下如何构建Route
组件,包括其暴露的Route
组件包含三个exact
、path
以及 component
。这也就意味着我们的propTypes
声明如下:
static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
}
这里有一些微妙的细节需要考虑,首先对于 path
并没有设置为必须参数,这是因为我们认为对于没有指定关联路径的 Route
组件应该自动默认渲染。而component
参数也没有被设置为必须是因为我们提供了其他的方式进行渲染,譬如 render
函数:
<Route
path="/settings"
render={({ match }) => {
return <Settings authed={isAuthed} match={match} />;
}}
/>
render
函数允许你方便地使用内联函数来创建
static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
render: PropTypes.func,
}
在确定了 Route
需要接收的组件参数之后,我们需要来考量其实际功能;Route
核心的功能在于能够当path
属性相一致时执行渲染操作。基于这个论断,我们首先需要实现判断是否匹配的功能,如果判断为匹配则执行渲染否则返回空值。我们在这里将该函数命名为 matchPatch
,那么此时整个 Route
组件的 render
函数定义如下:
class Route extends Component {
// ...
// 判断路径是否匹配
const match = matchPath(
location.pathname, // 全局 DOM 变量
{ path, exact }
)
if (!match) {
// 不匹配则直接返回
return null
}
if (component) {
// 如果设置了 Component 对象,则挂载该对象
return React.createElement(component, { match })
}
if (render) {
// 否则调用渲染函数
return render({ match })
}
return null;
}
现在的 Route
看起来已经相对明确了,当路径相匹配的时候才会执行界面渲染,否则返回为空。
变化监听
现在我们再回过头来考虑客户端路由中常见的跳转策略,一般来说用户只有两种方式会更新当前history
对象的 replace/push
方法;另一种是用户点击前进.listen
方法来监听当前popstate
事件。popstate
事件会在用户点击某个前进Route
组件都会重现检测当前
class Route extends Component {
// ...
componentWillMount() {
// 监听状态变化
addEventListener("popstate", this.handlePop);
}
componentWillUnmount() {
removeEventListener("popstate", this.handlePop);
}
handlePop = () => {
this.forceUpdate();
}; // ...
}
你会发现上面的代码与之前的相比多了挂载与卸载 popstate
监听器的功能,其会在组件挂载时添加一个 popstate
监听器;当监听到 popstate
事件被触发时,我们会调用 forceUpdate
函数来强制进行重渲染。总结而言,无论我们在系统中设置了多少的路由组件,它们都会独立地监听 popstate
事件并且相应地执行重渲染操作。
路径匹配
接下来我们继续讨论 matchPath
这个 Route
组件中至关重要的函数,它负责决定当前路由组件的 path
参数是否与当前Route
的参数 exact
,其用于指明路径匹配策略;当 exact
值被设置为 true
时,仅当路径完全匹配于 location.pathname
才会被认为匹配成功:
path | location.pathname | exact | matches? |
---|---|---|---|
/one |
/one/two |
true |
no |
/one |
/one/two |
false |
yes |
让我们深度了解下 matchPath
函数的工作原理,该函数的签名如下:
const match = matchPath(location.pathname, { path, exact });
其中函数的返回值 match
应该根据路径是否匹配的情况返回为空或者一个对象。基于这些推导我们可以得出 matchPatch
的原型:
const matchPath = (pathname, options) => {
const { exact = false, path } = options;
if (!path) {
return {
path: null,
url: pathname,
isExact: true
};
}
};
这里我们使用false
。我在上文提及的 path
非必要参数的具体支撑实现就在这里,我们首先进行空检测,当发现 path
为未定义或者为空时则直接返回匹配成功。接下来继续考虑具体执行匹配的部分,
const matchPath = (pathname, options) => {
...
const match = new RegExp(`^${path}`).exec(pathname)
}
这里使用的 .exec
函数,会在包含指定的文本时返回一个数组,否则返回空值;下表即是当我们的路由设置为 /topics/components
时具体的返回:
path | location.pathname | return value |
---|---|---|
/ |
/topics/components |
['/'] |
/about |
/topics/components |
null |
/topics |
/topics/components |
['/topics'] |
/topics/rendering |
/topics/components |
null |
/topics/components |
/topics/components |
['/topics/components'] |
/topics/props-v-state |
/topics/components |
null |
/topics |
/topics/components |
['/topics'] |
这里大家就会看出来,我们会为每个 <Route>
实例创建一个 match
对象。在获取到 match
对象之后,我们需要再做如下判断是否匹配:
const matchPath = (pathname, options) => {
// ...
if (!match) {
// 不匹配则返回空
return null;
}
const url = match[0];
const isExact = pathname === url;
if (exact && !isExact) {
// 判断是否需要精确匹配
return null;
}
return {
path,
url,
isExact
};
};
跳转组件
上文我们已经提及通过监听 popstate
状态来响应用户点击前进Link
组件来处理用户通过点击锚标签进行跳转的事件。Link
组件的
<Link to="/some-path" replace={false} />
其中的 to
是一个指向跳转目标地址的字符串,而 replace
则是布尔变量来指定当用户点击跳转时是替换
class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool
};
}
现在我们已经知道 Link
组件的渲染函数中需要返回一个锚标签,不过我们的前提是要避免每次用户切换路由的时候都进行整页的刷新,因此我们需要为每个锚标签添加一个点击事件的处理器:
class Link extends Component {
// ...
handleClick = event => {
const { replace, to } = this.props;
event.preventDefault(); // route here.
};
render() {
const { to, children } = this.props;
return (
<a href={to} onClick={this.handleClick}>
{children} {" "}
</a>
);
}
}
这里实际的跳转操作我们还是执行 Historypush
与 replace
函数,在使用 browserHistory
的情况下我们本质上还是使用pushState
与 replaceState
函数。pushState
与 replaceState
函数都要求输入三个参数,首先是一个与最新的历史记录相关的对象,在
const historyPush = path => {
history.pushState({}, null, path);
};
const historyReplace = path => {
history.replaceState({}, null, path);
};
而后在 Link
组件内,我们会根据 replace
参数来调用 historyPush
或者 historyReplace
函数:
class Link extends Component {
// ...
handleClick = event => {
const { replace, to } = this.props;
event.preventDefault();
replace ? historyReplace(to) : historyPush(to);
}; // ...
}
路由注册
现在我们需要考虑如何保证用户点击了 Link
组件之后触发全部路由组件的检测与重渲染。在我们上面实现的 Link
组件中,用户执行跳转之后浏览器的显示地址会发生变化,但是页面尚不能重新渲染;我们声明的 Route
组件并不能收到相应的通知。为了解决这个问题,我们需要追踪那些显现在界面上实际被渲染的 Route
组件并且当路由变化时调用它们的 forceUpdate
方法。setState
、context
以及 history.listen
方法来实现该功能。每个 Route
组件被挂载时我们会将其加入到某个数组中,然后当位置变化时,我们可以遍历该数组然后对每个实例调用 forceUpdate
方法:
let instances = [];
const register = comp => instances.push(comp);
const unregister = comp => instances.splice(instances.indexOf(comp), 1);
这里我们创建了两个函数,当 Route
挂载时调用 register
函数,而卸载时调用 unregister
函数。然后无论何时调用 historyPush
或者 historyReplace
函数时都会遍历实例数组中的对象的渲染方法,此时我们的 Route
组件就需要声明为如下样式:
class Route extends Component {
// ...
componentWillMount() {
addEventListener("popstate", this.handlePop);
register(this);
}
componentWillUnmount() {
unregister(this);
removeEventListener("popstate", this.handlePop);
} // ...
}
然后我们需要更新 historyPush
与 historyReplace
函数:
const historyPush = path => {
history.pushState({}, null, path);
instances.forEach(instance => instance.forceUpdate());
};
const historyReplace = path => {
history.replaceState({}, null, path);
instances.forEach(instance => instance.forceUpdate());
};
这样的话就保证了无论何时用户点击 <Link>
组件之后,在位置显示变化的同时,所有的 <Route>
组件都能够被通知到并且执行重匹配与重渲染。现在我们完整的路由解决方案就成形了。另外,<Redirect>
组件,允许执行路由跳转操作:
class Redirect extends Component {
static defaultProps = {
push: false
};
static propTypes = {
to: PropTypes.string.isRequired,
push: PropTypes.bool.isRequired
};
componentDidMount() {
const { to, push } = this.props;
push ? historyPush(to) : historyReplace(to);
}
render() {
return null;
}
}
注意这个组件并没有真实地进行界面渲染,而是仅仅进行了简单的跳转操作。到这里本文也就告一段落了,希望能够帮助你去了解