本教程是 Brad Westfall 的关于 React 的三部分系列文章的第一部分。当 Brad 向我推荐这篇文章时,他指出有很多关于 React 入门的教程,但关于如何从那里开始的教程却不多。如果您是 React 新手,我建议您 观看此介绍视频。本系列文章从基础知识的基础上继续。
警告! 本文是在 React Router 4 之前编写的,React Router 4 已成为 React 中路由的更标准选择。这里有一篇 关于 React Router 4 的新文章,您一定要阅读。
当我刚开始学习的时候,我发现了很多针对初学者的指南(例如 1、2、3、4),这些指南展示了如何创建单个组件并将它们渲染到 DOM 中。它们很好地讲解了 JSX 和 props 等基础知识,但我难以弄清楚 React 在更大的范围内是如何工作的——比如一个真实世界的单页面应用程序 (SPA)。由于本系列涵盖了大量内容,因此不会涵盖绝对的 初学者 概念。相反,它将假设您已经了解如何创建和渲染至少一个组件。
值得一提的是,这里还有一些其他针对初学者的优秀指南
系列代码
本系列文章还附带了一些可以在 GitHub 上使用的代码。在本系列文章中,我们将构建一个围绕用户和小部件的简单 SPA。
为了保持简单和简短,本系列文章中的示例将首先假设 React 和 React Router 来自 CDN。因此,您在下面的直接示例中不会看到 require()
或 import
。不过,在本教程快结束时,我们将为 GitHub 指南介绍 Webpack 和 Babel。到那时,它将全部使用 ES6!
React-路由
React 不是框架,而是一个库。因此,它并不能解决应用程序的所有需求。它在创建组件和提供管理状态的系统方面做得很好,但是创建更复杂的 SPA 将需要一个 支持演员阵容。我们将要看到的第一个是 React 路由.
如果您以前使用过任何前端路由器,那么许多这些概念您都会很熟悉。但是与我之前使用过的任何其他路由器不同,React Router 使用 JSX,这乍一看可能有点奇怪。
作为入门,这是渲染单个组件的样子
var Home = React.createClass({
render: function() {
return (<h1>Welcome to the Home Page</h1>);
}
});
ReactDOM.render((
<Home />
), document.getElementById('root'));
以下是使用 React Router 渲染 Home
组件的方式
...
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
</Router>
), document.getElementById('root'));
请注意,<Router>
和 <Route>
是两个不同的东西。它们在技术上是 React 组件,但它们本身并没有创建 DOM。虽然它看起来像是 <Router>
本身被渲染到 'root'
中,但实际上我们只是定义了有关应用程序如何工作的规则。今后,您将经常看到这个概念:组件有时存在的目的不是创建 DOM 本身,而是协调其他创建 DOM 的组件。
在示例中,<Route>
定义了一个规则,当访问主页 /
时,将渲染 Home
组件到 'root'
中。
多个路由
在前面的示例中,单个路由非常简单。它没有给我们带来太多价值,因为我们已经能够在没有路由器参与的情况下渲染 Home
组件。
当我们使用多个路由来定义哪个组件应该根据当前活动的路径进行渲染时,React Router 的力量就体现出来了
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
<Route path="/users" component={Users} />
<Route path="/widgets" component={Widgets} />
</Router>
), document.getElementById('root'));
当其路径与 URL 匹配时,每个 <Route>
将渲染其相应的组件。在任何给定时间,这三个组件中只有一个将被渲染到 'root'
中。使用这种策略,我们一次将路由器挂载到 DOM 的 'root'
中,然后路由器在路由更改时交换组件的进出。
还需要注意的是,路由器将在不向服务器发送请求的情况下切换路由,因此可以想象每个组件都可以是一个全新的页面。
可重用布局
我们开始看到单页面应用程序的雏形。但是,它仍然不能解决现实世界中的问题。当然,我们可以构建三个组件来作为完整的 HTML 页面,但是代码重用怎么办?这些三个组件很可能会共享一些公共资产,比如标题和侧边栏,那么我们如何防止在每个组件中重复 HTML 呢?
让我们假设我们正在构建一个类似于此模型的 Web 应用程序
当您开始考虑如何将此模型分解成可重用部分时,您可能会得到以下想法
从可嵌套组件和布局的角度思考将使我们能够创建可重用的部分。
突然,艺术部门通知您,应用程序需要一个用于搜索小部件的页面,它类似于用于搜索用户的页面。由于 用户列表 和 小部件列表 都需要相同的搜索页面“外观”,因此将 搜索布局 作为单独组件的想法现在更有意义了
搜索布局 现在可以作为所有类型搜索页面的父模板。虽然有些页面可能需要 搜索布局,但其他页面可以直接使用 主布局,而无需它
这是一种常见的策略,如果您使用过任何模板系统,您可能已经做过类似的事情。现在让我们处理 HTML。首先,我们将创建静态 HTML,而不考虑 JavaScript
<div id="root">
<!-- Main Layout -->
<div class="app">
<header class="primary-header"><header>
<aside class="primary-aside"></aside>
<main>
<!-- Search Layout -->
<div class="search">
<header class="search-header"></header>
<div class="results">
<!-- User List -->
<ul class="user-list">
<li>Dan</li>
<li>Ryan</li>
<li>Michael</li>
</ul>
</div>
<div class="search-footer pagination"></div>
</div>
</main>
</div>
</div>
请记住,'root'
元素将始终存在,因为它是 JavaScript 启动之前初始 HTML Body 中的唯一元素。“root”这个词很恰当,因为我们的整个 React 应用程序将挂载到它。但是,没有“正确名称”或约定来定义您对它的称呼。我选择“root”,因此我们将在整个示例中继续使用它。只是要注意,直接挂载到 <body>
元素上是 强烈建议避免的.
在创建静态 HTML 后,将其转换为 React 组件
var MainLayout = React.createClass({
render: function() {
// Note the `className` rather than `class`
// `class` is a reserved word in JavaScript, so JSX uses `className`
// Ultimately, it will render with a `class` in the DOM
return (
<div className="app">
<header className="primary-header"><header>
<aside className="primary-aside"></aside>
<main>
{this.props.children}
</main>
</div>
);
}
});
var SearchLayout = React.createClass({
render: function() {
return (
<div className="search">
<header className="search-header"></header>
<div className="results">
{this.props.children}
</div>
<div className="search-footer pagination"></div>
</div>
);
}
});
var UserList = React.createClass({
render: function() {
return (
<ul className="user-list">
<li>Dan</li>
<li>Ryan</li>
<li>Michael</li>
</ul>
);
}
});
不要过于区分我所说的“布局”和“组件”。这三个都是 React 组件。我只是选择将其中两个称为“布局”,因为它们执行的角色就是这样。
我们最终将使用“嵌套路由”将 UserList
放置在 SearchLayout
中,然后放置在 MainLayout
中。但是首先,请注意,当 UserList
被放置在它的父 SearchLayout
中时,父级将使用 this.props.children
来确定其位置。所有组件都有 this.props.children
作为 prop,但只有当组件嵌套时,父组件才会自动获得 React 填充的此 prop。对于不是父组件的组件,this.props.children
将为 null
。
嵌套路由
那么我们如何让这些组件嵌套呢?当我们嵌套路由时,路由器会为我们完成这项工作
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
组件将根据路由器嵌套路由的方式进行嵌套。当用户访问 /users
路由时,React Router 将把 UserList
组件放在 SearchLayout
中,然后将两者都放在 MainLayout
中。访问 /users
的最终结果是将三个嵌套组件放在 'root'
中。
请注意,我们没有为用户访问主页路径 (/
) 或要搜索小部件的情况制定规则。为了简单起见,这些规则被省略了,但让我们用新的路由器来添加它们
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route path="/" component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
你可能已经注意到,JSX 遵循 XML 规则,这意味着 Route
组件可以写成一个标签:<Route />
或两个标签:<Route>...</Route>
。这适用于所有 JSX,包括你的自定义组件和普通 DOM 节点。例如,<div />
是有效的 JSX,渲染时将转换为 <div></div>
。
为了简洁,只需想象 WidgetList
类似于 UserList
。
由于 <Route component={SearchLayout}>
现在有两个子路由,用户可以访问 /users
或 /widgets
,相应的 <Route>
将在 SearchLayout
组件中加载其各自的组件。
另外,请注意 Home
组件将直接放置在 MainLayout
内部,而不会涉及 SearchLayout
,这是由于 <Route>
的嵌套方式。你可能可以想象,通过重新排列路由,可以轻松地重新排列布局和组件的嵌套方式。
IndexRoutes
React Router 表达能力很强,通常有多种方法可以完成同一件事。例如,我们也可以这样编写上面的路由器
ReactDOM.render((
<Router>
<Route path="/" component={MainLayout}>
<IndexRoute component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
尽管外观不同,但它们的工作方式完全相同。
可选路由属性
有时,<Route>
将具有一个 component
属性,但没有 path
,如上面的 SearchLayout
路由。其他时候,可能需要一个具有 path
但没有 component
的 <Route>
。为了了解原因,让我们从这个例子开始
<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />
path
中的 /product
部分是重复的。我们可以通过将所有三个路由包装在一个新的 <Route>
中来删除重复部分
<Route path="product">
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
</Route>
同样,React Router 展示了它的表达能力。测验:你注意到这两个解决方案的问题了吗?目前我们没有规则来规定用户访问 /product
路径时的行为。
为了解决这个问题,我们可以添加一个 IndexRoute
<Route path="product">
<IndexRoute component={ProductProfile} />
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
</Route>
<Link>
而不是 <a>
使用 在为你的路由创建锚点时,你需要使用 <Link to="">
而不是 <a href="">
。不过不用担心,在使用 <Link>
组件时,React Router 最终会为你提供 DOM 中的普通锚点。但是,使用 <Link>
对 React Router 完成其部分路由魔法是必要的。
让我们在 MainLayout
中添加一些链接(锚点)
var MainLayout = React.createClass({
render: function() {
return (
<div className="app">
<header className="primary-header"></header>
<aside className="primary-aside">
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/users">Users</Link></li>
<li><Link to="/widgets">Widgets</Link></li>
</ul>
</aside>
<main>
{this.props.children}
</main>
</div>
);
}
});
<Link>
组件上的属性将传递到它们创建的锚点。所以这个 JSX
<Link to="/users" className="users">
将成为 DOM 中的这个
<a href="/users" class="users">
如果你需要为非路由路径创建锚点,例如外部网站,则照常使用普通锚点标签。有关更多信息,请参阅 IndexRoute 和 Link 的文档。
活动链接
<Link>
组件的一个很酷的功能是它能够知道何时处于活动状态
<Link to="/users" activeClassName="active">Users</Link>
如果用户位于 /users
路径上,路由器将查找使用 <Link>
创建的匹配锚点,并将切换其 active
类。有关更多信息,请参阅 此功能。
浏览器历史记录
为了避免混淆,我到现在才省略了一个重要的细节。<Router>
需要知道要使用哪种 历史记录 跟踪策略。React Router 文档 推荐使用 browserHistory,其实现如下
var browserHistory = ReactRouter.browserHistory;
ReactDOM.render((
<Router history={browserHistory}>
...
</Router>
), document.getElementById('root'));
在 React Router 的早期版本中,history
属性不是必需的,默认使用的是 hashHistory。顾名思义,它使用 URL 中的 #
哈希符号来管理前端 SPA 风格的路由,类似于你可能从 Backbone.js 路由器中期望的那样。
使用 hashHistory
,URL 将类似于
- example.com
- example.com/#/users?_k=ckuvup
- example.com/#/widgets?_k=ckuvup
当实现 browserHistory
时,路径看起来更自然
- example.com
- example.com/users
- example.com/widgets
但是,当在前端使用 browserHistory
时,服务器上有一个注意事项。如果用户从 example.com
开始访问,然后导航到 /users
和 /widgets
,React Router 将按预期处理这种情况。但是,如果用户直接在浏览器中输入 example.com/widgets
开始访问,或者在 example.com/widgets
上刷新,那么浏览器必须至少向服务器发送一个对 /widgets
的请求。但是,如果没有服务器端路由器,这将返回 404

为了解决服务器端的 404 问题,React Router 推荐在服务器端使用通配符路由器。使用这种策略,无论调用哪个服务器端路由,服务器都应该始终提供相同的 HTML 文件。然后,如果用户从 example.com/widgets
开始直接访问,即使返回了相同的 HTML 文件,React Router 也足够聪明,能够加载正确的组件。
用户不会注意到任何奇怪的事情,但你可能对始终提供相同的 HTML 文件有所顾虑。在本系列的代码示例中,将继续使用“通配符路由器”策略,但由你自行决定以你认为合适的方式处理服务器端路由。
React Router 可以以 同构 方式 在服务器端和客户端使用吗?当然可以,但这超出了本教程的范围。
browserHistory
重定向
使用 browserHistory
对象是单例,因此你可以在任何文件中包含它。如果你需要在任何代码中手动重定向用户,可以使用它的 push
方法来完成。
browserHistory.push('/some/path');
路由匹配
React 路由器处理 路由匹配,方式类似于其他路由器
<Route path="users/:userId" component={UserProfile} />
当用户访问以 users/
开头且之后具有任何值的任何路径时,此路由将匹配。它将匹配 /users/1
、/users/143
,甚至 /users/abc
(你将需要自行验证)。
React Router 将 :userId
的值作为道具传递给 UserProfile
。此道具在 UserProfile
内部以 this.props.params.userId
的形式访问。
路由器演示
在这一点上,我们有足够的代码来展示一个演示。
查看 Brad Westfall 在 CodePen 上的 Pen React-Router 演示 (@bradwestfall)。
如果你在示例中点击了几个路由,你可能会注意到浏览器的后退和前进按钮与路由器一起工作。这是这些 history
策略存在的主要原因之一。此外,请记住,对于你访问的每个路由,除了获取初始 HTML 的第一个请求之外,都不会向服务器发出任何请求。这多么酷啊!
ES6
在我们的 CodePen 示例中,React
、ReactDOM
和 ReactRouter
是来自 CDN 的全局变量。在 ReactRouter
对象中,包含了我们需要的所有内容,例如 Router
和 Route
组件。所以我们可以这样使用 ReactRouter
ReactDOM.render((
<ReactRouter.Router>
<ReactRouter.Route ... />
</ReactRouter.Router>
), document.getElementById('root'));
在这里,我们必须在所有路由组件前面加上它们的父对象 ReactRouter
。或者,我们可以使用 ES6 的新 解构 语法,如下所示
var { Router, Route, IndexRoute, Link } = ReactRouter
这将“提取” ReactRouter
的部分内容到普通的变量中,这样我们就可以直接访问它们。
从现在开始,本系列中的示例将使用各种 ES6 语法,包括解构、扩展运算符、导入/导出,以及其他一些语法。在每个新语法出现时,都会对其进行简要说明,本系列附带的 GitHub 仓库中也包含大量有关 ES6 的说明。
使用 webpack 和 Babel 打包
如前所述,本系列附带一个 GitHub 仓库,您可以在其中进行代码实验。由于它将类似于真实世界中的 SPA,因此将使用 webpack 和 Babel 等工具。
- webpack 将多个 JavaScript 文件打包成一个文件,供浏览器使用。
- Babel 将 ES6 (ES2015) 代码转换为 ES5,因为大多数浏览器还不支持所有 ES6 功能。随着这篇文章的更新,浏览器将支持 ES6,Babel 可能不再需要。
如果您不太熟悉这些工具,请不要担心,示例代码 已经将所有内容都设置好了,因此您可以专注于 React。但请务必查看示例代码的 README.md 文件,以获取其他工作流程文档。
注意弃用的语法
在 Google 上搜索有关 React Router 的信息可能会带您找到许多文章或 StackOverflow 页面,这些页面是在 React Router 1.0 版本之前发布时编写的。现在,1.0 版本之前的许多功能都已弃用。以下列出了简短清单
<Route name="" />
已弃用。请使用<Route path="" />
代替。<Route handler="" />
已弃用。请使用<Route component="" />
代替。<NotFoundRoute />
已弃用。请 查看替代方案<RouteHandler />
已弃用。willTransitionTo
已弃用。请 查看 onEnterwillTransitionFrom
已弃用。请 查看 onLeave- “Locations” 现在称为“histories”。
总结
React Router 还有更多未显示的功能,请务必查看 API 文档。React Router 的创建者还创建了 React Router 的分步教程,还可以查看 React.js Conf 视频,了解 React Router 的创建过程。
特别感谢 Lynn Fisher 提供的插图 @lynnandtonic
我有点难过的是服务器端渲染“超出了本教程的范围”。我希望 React 教程开始将其视为任何 React 设置的必要组成部分;在我看来,如果没有它,应用程序就不会完全正常工作。
我理解 Šime。我本可以将这篇文章写得再长三倍。除了这篇文章所讲的内容之外,React Router 还有很多东西需要学习。为了保持一定的范围,我无法做到面面俱到。
今天是星期一!让我们充满 CAN DO 的态度!这里有一篇关于这个主题的文章 我刚在网上搜索到的。
看起来似乎可以使用一个路由文件来处理服务器端和客户端,我觉得这简直是超凡脱俗的魔法,我想要它。
是的,那条评论听起来很严厉,这不是我的本意 :) 我觉得有必要指出服务器端渲染,因为我认为它的缺失会限制 Web 应用程序的功能,人们应该意识到这一点。
对于 browserHistory 问题,如果您使用的是 Webpack(以及运行 webpack-dev-server),那么在服务器启动命令中包含“–history-api-fallback”将解决该问题:“webpack-dev-server –inline –history-api-fallback”。
很棒的文章——我们使用 react-router 与 AltContainer、webpack、babel 一起使用——非常棒。这里提供的解释和演练非常出色(希望我们在开始时就拥有这些内容!)。
希望您能介绍 onEnter——我们以类似于 Angular1 中的“resolve”的方式使用它,例如。
再次感谢——期待本系列的后续文章!
感谢 Steve。您发现从
onEnter
触发 XHR 与componentWillMount
触发 XHR 的权衡取舍是什么?为什么这看起来像浏览器内部的 JavaScript 浏览器?叹气
看起来 React.js 最终可能会走 jQuery/Flash/etc.. 的路线,最终 ES7/HTML6/CSS4/etc… 将取代 react/angular/最新库框架热潮的所有实用性。
与此同时,很高兴看到这些系统如何工作,而不必自己花时间测试。只是为了避免误解,我感谢这篇文章,并期待接下来的文章……
我认为您可以将自己的内容掩盖为精选内容——从自己的属性中精选内容!
Beylikdüzü evden eve nakliyat hizmetleri
http://www.beylikduzuevdenevenakliyati.com
我认为您可以将自己的内容掩盖为精选内容——从自己的属性中精选内容!
Bahçeşehir evden eve nakliyat hizmetleri
http://www.bahcesehirnakliyati.com
很棒的文章——节奏适中、写得清晰、范围合理。谢谢!
很棒的文章!Lynn 的图形使它非常清晰。赞赏您考虑文章随着时间的推移而发生的变化,例如,当某些人阅读您的文章时,浏览器可能会原生处理 ES6。
您知道如何处理相对链接吗?我一直关注 这里的讨论,但至今尚未找到解决方案。所以我想知道生产环境中的应用程序现在是如何处理这种情况的?
注意:活动链接部分的“此功能”不可点击。上一部分“IndexRoute 的文档”也是如此。我认为是 CSS 问题。从 h4 中移除“position: relative”吗?
感谢 Tyler。并且您提到的有关相对链接的讨论很有意义。我并不知道最新的更新。看来讨论仍然很活跃。
至于文章中损坏的链接,我在 Chrome 48 上可以使用。您使用的是什么浏览器?我还会告诉 Chris,因为他刚刚重新设计了这个网站。
我在 Linux 上使用的是 Chrome 49。出乎意料,但现在对我来说效果很好。
好吧,我不知道为什么之前不行。
现在可以使用 react-router 团队提供的此库来实现相对链接。
https://github.com/ryanflorence/react-router-relative-links
很酷的模型。你是用模型工具还是完全自己做的?
Lynn Fisher 为我做了这些,推特:@lynnandtonic。她是一位很棒的插画家和 CSS 专家。看看她的网站 a.singlediv.com
很棒的文章,Brad。@Chris,很高兴看到 CSS-Tricks 上有 React 文章。希望看到更多!
Egghead 上刚刚发布了一个新的 React-Router 课程。还没有看过,但从我看到的内容来看,该网站上的大多数内容都非常棒。 https://egghead.io/series/getting-started-with-react-router
嗨,伙计!
很棒的文章,感谢你抽出时间写作!我刚看完 Egghead 的视频才读这篇文章。它们互相补充得很好,因为在 Egghead 上,他很快地介绍了原则(但有更多代码示例),而你在文章中更侧重于概念。
再次感谢你对主题的解释。非常感谢!
我在 main-layout.js 中的导航栏中添加了一个新的 li 元素,但我在刷新页面后看不到它。我错过了什么?
在一个选项卡中打开
gulp
,在另一个选项卡中执行gulp watch
以重新构建 Webpack(React 内容)谢谢!
嗨,
我想知道你(或任何其他人)是否能在客户端的 JSX 中使用 ReactRouter.browserHistory。我的意思是,如果你将所有内容都写在一个
<script type="text/babel">
元素中,并且让浏览器处理所有转译,它对你来说是否有效?对我来说不行。通过查看我的示例代码来解释这一点最容易:http://codepen.io/etitcombe/pen/pyWXEG?editors=1000
关键行是 66 和 67。如果你取消注释 66 并注释掉 67,页面将正常工作 - 它使用的是 hashHistory。如果你保持 66 注释掉并取消注释 67,页面将不会渲染任何内容。在这种情况下,它使用的是 browserHistory。
我还使用 webpack 构建了相同的“网站”来转译所有内容并提供网站服务,在这种情况下,browserHistory 工作得很好。当然,在这种情况下,代码有点不同。从 react-router 导入 browserHistory 后,我就可以直接在 Router 组件的 history 属性中使用它,无需中间变量或用 ReactRouter 作为前缀。
在这两种情况下,我使用的是 react-router 2.0.1 版本。
有人可以解释一下区别吗?如果它在浏览器中转译时不应该工作,你知道为什么吗?我错过了什么?
非常感谢。
嘿,好文章!
我使用 babel 和 browserHistory 扩展了你的示例,这是我的笔的链接:http://codepen.io/bluurn/pen/mPjgaV
任何评论都将不胜感激!