九、使用路由处理导航
几乎每个 web 应用都需要路由。它是基于一组路由配置响应给定 URL 的过程。换句话说,从 URL 到呈现内容的简单映射。然而,这个简单的任务比最初看起来要复杂得多。这就是为什么我们要利用本章中的react-router
包,即 React 的事实上的路由工具。
首先,您将学习使用 JSX 语法声明路由的基础知识。然后,我们将深入研究路由的动态方面,例如动态路径段和查询参数。接下来,您将使用react-router
中的组件实现链接。我们将以一个示例结束本章,该示例演示如何在 URL 更改时延迟加载组件。
申报路由
如果您曾经在 React 之外处理过路由,您可能已经知道它可能很快就会变得混乱。有了react-router
,将路由与它们呈现的内容搭配起来就容易多了。在本节中,您将看到这在很大程度上归因于用于定义路由的声明性 JSX 语法。
我们将创建一个基本的 hello world 示例路由,这样您就可以了解 React 应用中路由的基本情况。然后,我们将研究如何通过特性而不是在单片模块中组织路由声明。最后,您将实现一个通用的父子路由模式。
你好
让我们创建一个呈现简单组件的简单路由。首先,我们有一个最简单的 React 组件,我们希望在激活路由时渲染该组件:
import React from 'react';
export default () => (
<p>Hello Route!</p>
);
接下来,让我们看看路由定义本身:
import React from 'react';
import {
Router,
Route,
browserHistory,
} from 'react-router';
import MyComponent from './MyComponent';
// Exports a "<Router>" component to be rendered.
export default (
<Router history={browserHistory}>
<Route path="/" component={MyComponent} />
</Router>
);
如您所见,此模块导出一个 JSX<Router>
元素。这实际上是应用的顶级组件,稍后您将看到。但首先,让我们分析一下这里发生的事情。browserHistory
对象用于告诉路由它应该使用本机浏览器历史 API 来跟踪导航。不要太担心这个财产;大多数情况下,您将使用此选项。
在路由中,我们将实际路由声明为<Route>
元素。任何路由都有两个关键属性:path
和component
。这些都是不言自明的,当path
与活动 URL 匹配时,component
被呈现。但它到底是在哪里渲染的呢?
为了帮助回答这个问题,让我们来看看应用的主要模块:
import React from 'react';
import { render } from 'react-dom';
// Imports the "<Router>", and all the "<Route>"
// elements within.
import routes from './routes';
// The "<Router>" is the root element of the app.
render(
routes,
document.getElementById('app')
);
正如我前面提到的,路由是顶级组件。路由本身实际上并不呈现任何内容,它只负责管理基于当前 URL 呈现其他组件的方式。果然,当您在浏览器中查看此示例时,<MyComponent>
按预期呈现:
这就是 React 中路由工作原理的要点。现在,让我们从功能的角度来看声明路由。
解耦路由声明
如前一节所示,使用react-router
声明路由非常简单。当您的应用增长并且几十条路由都在一个模块中声明时,困难就来了,因为在心里将一条路由映射到一个功能更加困难。
为了帮助实现这一点,应用的每个顶级功能都可以定义自己的routes
模块。这样,就可以清楚地知道哪些路线属于哪个功能。那么,让我们从根路由模块开始,看看它在引入特定于功能的路由时是什么样子的:
import React from 'react';
import {
Router,
browserHistory,
} from 'react-router';
// Import the routes from our features.
import oneRoutes from './one/routes';
import twoRoutes from './two/routes';
// The feature routes are rendered as children of
// the main router.
export default (
<Router history={browserHistory}>
{oneRoutes}
{twoRoutes}
</Router>
);
在本例中,应用有两个显著命名的特性:一个和两个。oneRoutes
和twoRoutes
值从各自的功能目录中拉入,并作为子元素呈现在<Router>
元素中。现在,这个模块只会增加应用功能的数量,而不是路由的数量,路由的数量可能会大得多。现在让我们来看一个特性路由:
import React from 'react';
import { Route, IndexRedirect } from 'react-router';
// The pages that make up feature "one".
import First from './First';
import Second from './Second';
// The routes of our feature. The "<IndexRedirect>"
// handles "/one" requests by redirecting to "/one/1".
export default (
<Route path="/one">
<IndexRedirect to="1" />
<Route path="1" component={First} />
<Route path="2" component={Second} />
</Route>
);
此模块导出单个<Route>
元素。此功能使用的每条路线都是一个子<Route>
。例如,如果运行此示例并将浏览器指向/one/1
,您将看到First
的渲染内容。
<IndexRedirect>
元素需要重定向/one
到/one/1
。
在本章末尾,我们将详细介绍如何构造路由和应用功能。但首先,我们需要更深入地思考父路径和子路径。
父和子路由
上例中的App
组件是应用的主要组件。这是因为它定义了根 URL:/
。然而,一旦用户导航到特定的功能 URL,App
组件就不再相关。
路由的挑战在于,有时您需要一个父组件,例如App
,这样您就不必继续重新呈现页面骨架。所谓骨架,我指的是应用的每个页面都通用的内容。在这个场景中,我们总是希望App
进行渲染。当 URL 更改时,只有App
组件的某些部分呈现新内容。下面是一张图表,说明了这个想法:
随着 URL 的更改,父组件将填充相应的子组件。从App
组件开始,让我们了解如何进行此设置:
import React, { PropTypes } from 'react';
// The "header" and "main" properties are the rendered
// components specified in the route. They're placed
// in the JSX of this component - "App".
const App = ({ header, main }) => (
<section>
<header>
{header}
</header>
<main>
{main}
</main>
</section>
);
// The "header" and "main" properties should be
// a React element.
App.propTypes = {
header: PropTypes.element,
main: PropTypes.element,
};
现在我们已经定义了页面框架。现在由路由配置来正确呈现App
。现在让我们看看这个配置:
import React from 'react';
import {
Router,
Route,
browserHistory,
} from 'react-router';
// The "App" component is rendered with every route.
import App from './App';
// The "User" components rendered with the "/users"
// route.
import UsersHeader from './users/UsersHeader';
import UsersMain from './users/UsersMain';
// The "Groups" components rendered with the "/groups"
// route.
import GroupsHeader from './groups/GroupsHeader';
import GroupsMain from './groups/GroupsMain';
// Configures the "/users" route. It has a path,
// and named components that are placed in "App".
const users = {
path: 'users',
components: {
header: UsersHeader,
main: UsersMain,
},
};
// Configures the "/groups" route. It has a path,
// and named components that are placed in "App".
const groups = {
path: 'groups',
components: {
header: GroupsHeader,
main: GroupsMain,
},
};
// Setup the router, using the "users" and "groups"
// route configurations.
export default (
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route {...users} />
<Route {...groups} />
</Route>
</Router>
);
此应用有两个页面/功能-users
和groups
。它们中的每一个都定义了自己的App
组件。例如,UsersHeader
填入header
值,UsersMain
填入主值。还要注意,每个路由都有一个components
属性,您可以在其中传递header
和main
值。这就是App
如何根据当前路线找到它需要的内容。如果您运行此示例并导航到/users
,您将看到以下内容:
对于react-router
的新手来说,一个常见的错误是认为组件本身被传递给了父组件。实际上,传递给父级的是呈现的 JSX 元素。路由实际呈现内容,然后将内容传递给父级。
办理进路参数
到目前为止,我们在本章中看到的 URL 都是静态的。大多数应用将同时使用静态和动态路由。在本节中,您将学习如何将动态 URL 段传递到组件中,如何使这些段成为可选段,以及如何获取查询字符串参数。
路由中的资源 ID
一个常见的用例是将后端资源的 ID 作为 URL 的一部分。这使得我们的代码很容易获取 ID,然后进行 API 调用以获取相关的资源数据。让我们实现一个呈现用户详细信息页面的路由。这将需要一个包含用户 ID 的路由,然后需要以某种方式将用户 ID 传递给组件,以便组件能够获取用户。
我们将从路线开始:
import React from 'react';
import {
Router,
Route,
browserHistory,
} from 'react-router';
import UserContainer from './UserContainer';
export default (
<Router history={browserHistory}>
{ /* Note the ":" before "id". This denotes
a dynamic URL segment and the value will
be available in the "params" property of
the rendered component. */ }
<Route path="/users/:id" component={UserContainer} />
</Router>
);
:
语法标记 URL 变量的开始。因此在本例中,id
变量将被传递到UserContainer
组件,我们现在将实现该组件:
import React, { Component, PropTypes } from 'react';
import { fromJS } from 'immutable';
import User from './User';
import { fetchUser } from './api';
export default class UserContainer extends Component {
state = {
data: fromJS({
error: null,
first: null,
last: null,
age: null,
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
componentWillMount() {
// The dynamic URL segment we're interested in, "id",
// is stored in the "params" property.
const { params: { id } } = this.props;
// Fetches a user based on the "id". Note that it's
// converted to a number first.
fetchUser(+id).then(
// If the user was successfully fetched, then
// merge the user properties into the state. Also,
// make sure that "error" is cleared.
(user) => {
this.data = this.data
.merge(user, { error: null });
},
// If the user fetch failed, set the "error" state
// to the resolved error value. Also, make sure the
// other user properties are restored to their defaults
// since the component is now in an error state.
(error) => {
this.data = this.data
.merge({
error,
first: null,
last: null,
age: null,
});
}
);
}
render() {
return (
<User {...this.data.toJS()} />
);
}
}
// Params should always be there...
UserContainer.propTypes = {
params: PropTypes.object.isRequired,
};
params
属性包含 URL 的任何动态部分。在本例中,我们对id
参数感兴趣。然后,我们将该值的整数版本传递给fetchUser()
API 调用。如果 URL 完全缺少该段,那么该代码将根本无法运行;路由只会抱怨找不到匹配的路由。但是,没有在路由级别进行类型检查,这意味着由您来设计故障模式。
在本例中,如果用户传递字符串“one”,则类型转换将失败,并将发生 500 错误。您可以编写一个类型检查参数的函数,该函数不会因异常而失败,而是以 404 错误响应。无论如何,提供有意义的故障模式取决于应用,而不是react-route
库。
现在让我们来看一下 API 函数,我们用它来响应用户数据:
// Mock data...
const users = [
{ first: 'First 1', last: 'Last 1', age: 1 },
{ first: 'First 2', last: 'Last 2', age: 2 },
];
// Returns a promise that resolves to a
// user from the "users" array, using the
// given "id" index. If nothing is found,
// the promise is rejected.
export function fetchUser(id) {
return new Promise((resolve, reject) => {
const user = users[id];
if (user === undefined) {
reject(`User ${id} not found`);
} else {
resolve(user);
}
});
}
要么在模拟数据的users
数组中找到用户,要么拒绝承诺。如果被拒绝,则调用UsersContainer
组件的错误处理行为。最后但并非最不重要的一点是,我们有负责实际呈现用户详细信息的组件:
import React, { PropTypes } from 'react';
import { Map as ImmutableMap } from 'immutable';
// Renders "error" text, unless "error" is
// null - then nothing is rendered.
const Error = ({ error }) =>
ImmutableMap()
.set(null, null)
.get(error, (<p><strong>{error}</strong></p>));
// Renders "children" text, unless "children"
// is null - then nothing is rendered.
const Text = ({ children }) =>
ImmutableMap()
.set(null, null)
.get(children, (<p>{children}</p>));
const User = ({
error,
first,
last,
age,
}) => (
<section>
{ /* If there's an API error, display it. */ }
<Error error={error} />
{ /* If there's a first, last, or age value,
display it. */ }
<Text>{first}</Text>
<Text>{last}</Text>
<Text>{age}</Text>
</section>
);
// Every property is optional, since we might
// have have to render them.
User.propTypes = {
error: PropTypes.string,
first: PropTypes.string,
last: PropTypes.string,
age: PropTypes.number,
};
export default User;
现在,如果运行该示例并导航到/users/0
,则呈现的页面如下所示:
如果您导航到一个不存在的用户,/users/2
,您将看到以下内容:
可选参数
有时,我们需要指定启用某个特性的某些方面。在许多情况下,指定此选项的最佳方法是将该选项编码为 URL 的一部分,或将其作为查询字符串提供。URL 最适用于简单选项,如果组件可以使用许多可选值,则查询字符串最适用。
让我们实现一个呈现用户列表的用户列表组件。或者,我们希望能够按降序对列表进行排序。让我们在此页面的路由定义中将其作为可选路径段:
import React from 'react';
import {
Router,
Route,
browserHistory,
} from 'react-router';
import UsersContainer from './UsersContainer';
export default (
<Router history={browserHistory}>
{ /* The ":desc" parameter is optional, and so
is the "/" that precedes it. The "()" syntax
marks anything that's optional. */ }
<Route
path="/users(/:desc)"
component={UsersContainer}
/>
</Router>
);
正是()
语法将两者之间的任何内容标记为可选。在本例中,它是/
和-desc
参数。这意味着用户可以在/users/
之后提供他们想要的任何东西。这也意味着组件需要确保提供了字符串desc
,并且忽略了所有其他内容。
组件还可以处理提供给它的任何查询字符串。因此,虽然路由声明没有提供任何机制来定义接受的查询字符串,但路由仍然会处理它们,并以可预测的方式将它们传递给组件。现在让我们来看一看用户列表容器组件:
import React, { Component, PropTypes } from 'react';
import { fromJS } from 'immutable';
import Users from './Users';
import { fetchUsers } from './api';
export default class UsersContainer extends Component {
// The "users" state is an empty immutable list
// by default.
state = {
data: fromJS({
users: [],
}),
}
componentWillMount() {
// The URL and query string data we need...
const {
props: {
params,
location: {
query,
},
},
} = this;
// If the "params.desc" value is "desc", it means that
// "desc" is a URL segment. If "query.desc" is true, it
// means "desc" was provided as a query parameter.
const desc = params.desc === 'desc' || !!(query.desc);
// Tell the "fetchUsers()" API to sort in descending
// order if the "desc" value is true.
fetchUsers(desc).then((users) => {
this.data = this.data
.set('users', users);
});
}
render() {
return (
<Users {...this.data.toJS()} />
);
}
}
UsersContainer.propTypes = {
params: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
};
这个容器的关键方面是它寻找params.desc
或query.desc
。它将此用作fetchUsers()
API 的参数,以确定排序顺序。下面是导航到/users
时呈现的内容:
如果我们通过导航到/users/desc
来包含降序参数,我们会得到以下结果:
使用连杆组件
在本节中,我们将介绍如何创建链接。您可能会尝试使用标准的<a>
元素链接到react-router
控制的页面。这种方法的问题是,这些链接将试图通过发送 GET 请求在后端定位页面。这不是我们想要的,因为路由配置已经在浏览器中。
我们将从一个非常简单的示例开始,该示例演示了<Link>
元素在大多数方面如何与<a>
元素相似。然后,您将看到如何构建使用 URL 参数和查询参数的链接。
基本链接
链接的想法很简单。我们使用它来指向路由,这些路由随后呈现新内容。然而,Link
组件还负责浏览器历史记录和查找路由。下面是一个呈现两个链接的应用组件:
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
const App = ({ content }) => (
<section>
{content}
</section>
);
App.propTypes = {
content: PropTypes.node.isRequired,
};
// The "content" property has the default content
// for the "App" component. The "<Link>" elements
// handle dealing with the history API. Regular
// "<a>" links result in requests being sent to
// the server.
App.defaultProps = {
content: (
<section>
<p><Link to="first">First</Link></p>
<p><Link to="second">Second</Link></p>
</section>
),
};
export default App;
to
属性指定单击时要激活的路由。在这种情况下,应用有两条路由-/first
和/second
。下面是渲染链接的外观:
如果单击第一个链接,页面内容将更改为如下所示:
URL 及查询参数
构造传递给<Link>
的路径的动态段是简单的字符串操作。路径中的所有内容都在to
属性中。这意味着我们必须自己编写更多的代码来构造字符串,但也意味着代表路由的幕后魔法更少。
另一方面,<Link>
的query
属性接受查询参数的映射,并将为您构造查询字符串。让我们创建一个简单的组件,它将回显传递给 echo URL 段或echo
查询参数的任何内容:
import React from 'react';
// Simple component that expects either an "echo"
// URL segment parameter, or an "echo" query parameter.
export default ({
params,
location: {
query,
},
}) => (
<h1>{params.echo || query.echo}</h1>
);
现在让我们看一下呈现两个链接的 AutoT0.组件。第一个将构建一个使用动态值作为 URL 参数的字符串。第二个将向query
属性传递一个对象以构建查询字符串:
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
const App = ({ content }) => (
<section>
{content}
</section>
);
App.propTypes = {
content: PropTypes.node.isRequired,
};
// Link parameter and query data...
const param = 'From Param';
const query = { echo: 'From Query' };
App.defaultProps = {
content: (
<section>
{ /* This "<Link>" uses a parameter as part of
the "to" property. */}
<p><Link to={`echo/${param}`}>Echo param</Link></p>
{ /* This "<Link>" uses the "query" property
to add query parameters to the link URL. */ }
<p><Link to="echo" query={query}>Echo query</Link></p>
</section>
),
};
export default App;
下面是两个链接在渲染时的外观:
param 链接将您带到/echo/From Param
,如下所示:
查询链接带您到/echo?echo=From+Query
,如下图:
延迟路由
在本章前面,您学习了如何按功能拆分路由声明。这样做是为了避免使用单片路由模块,因为单片路由模块不仅难以破译,还可能导致性能问题。考虑一个具有数十个特性和数百条路由的大型应用。必须预先加载所有这些路由和组件将导致糟糕的用户体验。
在本章的最后一节中,我们将详细介绍装载路线。其中一部分涉及利用加载程序,如本例中使用的 Webpack 加载程序。首先,让我们来看看主要的组成部分:
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
// The "App" component is divided into
// "header" and "content" sections, and will
// simply render these properties.
const App = ({ header, content }) => (
<section>
<header>
{header}
</header>
<main>
{content}
</main>
</section>
);
// The "header" and "content" properties need to be
// renderable values.
App.propTypes = {
header: PropTypes.node.isRequired,
content: PropTypes.node.isRequired,
};
// The default content for our "App" component.
App.defaultProps = {
header: (<h1>App</h1>),
content: (
<ul>
<li><Link to="users">Users</Link></li>
<li><Link to="groups">Groups</Link></li>
</ul>
),
};
export default App;
这个示例应用有两个特性:出色地命名为users
和groups
。在主页上,每个功能都有一个链接。我们知道,这个应用总有一天会成功,并将变得巨大。因此,我们不希望只在用户单击其中一个链接时,预先加载所有路由和组件。
现在让我们来关注主路线模块:
import React from 'react';
import {
Router,
browserHistory,
} from 'react-router';
import App from './App';
// Defines the main application route as
// a plain object, instead of a "<Route>"
// component.
const routes = {
// The "path" and "component" are specified,
// just like any other route.
path: '/',
component: App,
// This allows for lazy route configuration loading.
// The "require.ensure()" call allows for tools like
// Webpack to split the code bundles into separate
// modules that are loaded on demand. The actual
// "require()" calls get the route configurations.
// In this case, it's routes for the "users" feature
// and the "groups" feature.
getChildRoutes(partialNextState, callback) {
require.ensure([], (require) => {
const { users } = require('./users/routes');
const { groups } = require('./groups/routes');
callback(null, [
users,
groups,
]);
});
},
};
// The "routes" object is passed to the "routes"
// property. There are no nested "<Route>" elements
// needed for this router.
export default (
<Router history={browserHistory} routes={routes} />
);
App
路线的关键方面是getChildRoutes()
方法。当需要进行路由查找时,将调用此函数。这是完美的,因为这允许用户与应用交互,而无需加载这些路由,直到他们单击其中一个链接。这被称为延迟加载,因为此代码加载另外两个路由模块。
require.ensure()
功能是在 Webpack 等代码绑定器中启用代码拆分的功能。require.ensure()
函数告诉绑定器,此回调中的require()
调用可以进入一个单独的绑定中,并根据需要加载,而不是创建一个大的绑定。我建议您在为本书下载的代码中运行此示例,并在浏览应用时查看 NetworkDeveloper 工具。
以下是主页呈现时的外观:
现在,让我们来看看其中的一个特色路由模块:
import React from 'react';
import UsersHeader from './UsersHeader';
import UsersContent from './UsersContent';
// The route configuration for our "users" feature.
export const users = {
// The components rendered by "App" are specified
// here as "UsersHeader" and "UsersContent".
path: 'users',
components: {
header: UsersHeader,
content: UsersContent,
},
childRoutes: [
// The "users/active" route lazily-loads the
// "UsersActiveHeader" and "UsersActiveContent"
// components when the route is activated.
{
path: 'active',
getComponents(next, cb) {
require.ensure([], (require) => {
const {
UsersActiveHeader,
} = require('./UsersActiveHeader');
const {
UsersActiveContent,
} = require('./UsersActiveContent');
cb(null, {
header: UsersActiveHeader,
content: UsersActiveContent,
});
});
},
},
// The "users/inactive" route lazily-loads the
// "UsersInactiveHeader" and "UsersInactiveContent"
// components when the route is activated.
{
path: 'inactive',
getComponents(next, cb) {
require.ensure([], (require) => {
const {
UsersInactiveHeader,
} = require('./UsersInactiveHeader');
const {
UsersInactiveContent,
} = require('./UsersInactiveContent');
cb(null, {
header: UsersInactiveHeader,
content: UsersInactiveContent,
});
});
},
},
],
};
export default users;
它遵循与主routes
模块相同的延迟加载原则。这一次,当找到匹配的路由时,调用的是getComponents()
方法。然后加载组件代码包。
以下是Users
页面的外观:
以下是Active Users
页面的外观:
这种技术对于大型应用是绝对必要的。然而,重构现有特性以使用这种延迟加载方法进行路由并不太困难。因此,在应用的早期,不要太担心延迟加载。
总结
在本章中,您学习了 React 应用中的路由。路由的任务是呈现与 URL 对应的内容。react-router
包是作业的标准工具。
您了解了路由如何是 JSX 元素,就像它们呈现的组件一样。有时您需要将路由拆分为基于功能的routes
模块。构建页面内容的一种常见模式是,有一个父组件在 URL 更改时呈现动态部分。
您学习了如何处理 URL 段和查询字符串的动态部分。您还学习了如何使用<Link>
元素在整个应用中构建链接。最后,您看到了大型应用能够通过延迟加载其路由配置和组件来扩展。
在下一章中,您将学习如何在 Node.js 中呈现 React 组件。
版权属于:月萌API www.moonapi.com,转载请注明出处