React16 高级教程(十)
原文:Pro React 16
二十一、使用 URL 路由
目前,显示给用户的内容选择是由应用的状态数据控制的。有些状态数据是特定于单个组件的,比如管理产品和供应商数据选择的Selector组件。其余的数据位于 Redux 数据存储中,连接的组件使用这些数据来决定是否需要数据表或编辑器组件,并获取数据来填充这些组件的内容。
在这一章中,我将介绍一种构建应用的不同方法,这种方法是基于浏览器的 URL 选择内容,称为 URL 路由。我将呈现导航到新 URL 的锚元素,并通过选择内容并将其呈现给用户来响应这些 URL,而不是按钮元素的事件处理程序调度 Redux 操作。对于复杂的应用,URL 路由可以使构建项目变得更容易,并使扩展和维护功能变得更容易。表 21-1 将 URL 路由放在上下文中。
表 21-1
将 URL 路由置于上下文中
|问题
|
回答
| | --- | --- | | 这是什么? | URL 路由使用浏览器的当前 URL 来选择呈现给用户的内容。 | | 为什么有用? | URL 路由允许在不需要共享状态数据的情况下构建应用,共享状态数据被编码在 URL 中,这也使得更改应用的结构变得更加容易。 | | 如何使用? | 呈现的导航元素会更改浏览器的 URL,而不会触发新的 HTTP 请求。新的 URL 用于选择呈现给用户的内容。 | | 有什么陷阱或限制吗? | 需要进行彻底的测试,以确保用户可以导航到的所有 URL 都得到正确处理,并显示适当的内容。 | | 有其他选择吗? | URL 路由完全是可选的,还有其他方式来组成应用及其数据,如前面的章节所演示的。 |
表 21-2 总结了本章内容。
表 21-2
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 创建导航元素 | 使用Link组件 | 4, 13 |
| 响应导航 | 使用Route组件 | 5–6 |
| 匹配特定的 URL | 使用Route组件的exact属性 | seven |
| 匹配多个 URL | 在Route组件的path属性中将 URL 指定为数组,或者使用正则表达式 | 8–9 |
| 选择一条路线 | 使用Switch组件 | Ten |
| 定义后备路线 | 使用Redirect组件 | 11, 12 |
| 指示活动航路 | 使用NavLink组件 | 14, 15 |
| 选择用于在 URL 中表示路由的机制 | 选择路由组件 | Sixteen |
为本章做准备
在这一章中,我继续使用在第十八章创建的productapp项目,最近一次使用是在第二十章。为了准备本章,打开一个新的命令提示符,导航到productapp文件夹,运行清单 21-1 中所示的命令,将一个包添加到项目中。React 路由包可用于一系列应用类型。清单 21-1 中安装的包是用于 web 应用的。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
npm install react-router-dom@4.3.1
Listing 21-1Adding a Package to the Project
为了简化呈现给用户的内容,我删除了一些由App组件呈现的内容,如清单 21-2 所示。
import React, { Component } from "react";
import { Provider } from "react-redux";
import dataStore from "./store";
import { Selector } from "./Selector";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export default class App extends Component {
render() {
return <Provider store={ dataStore }>
<Selector>
<ProductDisplay name="Products" />
<SupplierDisplay name="Suppliers" />
</Selector>
</Provider>
}
}
Listing 21-2Simplifying Content in the App.js File in the src Folder
保存对组件 JavaScript 文件的更改,并使用命令提示符运行productapp文件夹中清单 21-3 中所示的命令,以启动 React 开发工具。
npm start
Listing 21-3Starting the Development Tools
项目将被编译,开发 HTTP 服务器将被启动。一个新的浏览器窗口将打开并显示应用,如图 21-1 所示。
图 21-1
运行示例应用
URL 路由入门
首先,我将在Selector组件中使用 URL 路由,这样它就不需要自己的状态数据来跟踪用户是否想要与产品或供应商合作。
设置 URL 路由有两个步骤。第一步是创建链接,用户单击这些链接可以导航到应用的不同部分。第二步是选择将为用户可以导航到的每个 URL 显示的内容。这些步骤使用 React-Router 包提供的 React 组件来执行,如清单 21-4 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
// constructor(props) {
// super(props);
// this.state = {
// selection: React.Children.toArray(props.children)[0].props.name
// }
// }
// setSelection = (ev) => {
// ev.persist();
// this.setState({ selection: ev.target.name});
// }
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/products">Products</Link></div>
<div><Link to="/suppliers">Suppliers</Link></div>
</div>
<div className="col">
<Route path="/products" component={ ProductDisplay } />
<Route path="/suppliers" component={ SupplierDisplay} />
</div>
</div>
</div>
</Router>
}
}
Listing 21-4Adding URL Routing in the Selector.js File in the src Folder
设置基本路由配置需要三个组件。Router组件用于提供对 URL 路由特性的访问。使用 URL 进行导航有不同的方式,每种方式都有自己的 React-Router 组件,我在“选择和配置路由”一节中对此进行了描述。约定是导入您需要的组件,在本例中是BrowserRouter,并为其指定名称Router,然后将其用作需要访问路由特性的内容的容器。
选择替代路由包
React-Router 是目前为止 React 项目中使用最广泛的路由包,对于大多数应用来说是一个很好的起点。还有其他可用的路由包,但并不是所有的路由包都是专门针对 React 的,可能需要进行笨拙的改编。
如果你无法和 React-Router 相处,那么最好的替代品就是 Backbone ( https://backbonejs.org )。这个广受好评的包为任何 JavaScript 应用提供了路由,并且与 React 配合得很好。
链接组件入门
Link组件呈现一个元素,用户可以单击它来导航到一个新的 URL,如下所示:
...
<div><Link to="/products">Products</Link></div>
...
使用to属性指定导航 URL,这个Link将导航到/products URL。导航 URL 是相对于应用的起始 URL 指定的,起始 URL 是开发过程中的http://localhost:3000。这意味着为Link组件的to属性指定/products会告诉它呈现一个将导航到http://localhost:3000/products的元素。当应用被部署并具有公共 URL 时,这些相对 URL 将继续工作。
路线组件入门
添加到清单 21-4 的最后一个组件是Route,它等待浏览器导航到一个特定的 URL,然后显示其内容,如下所示:
...
<Route path="/products" component={ ProductDisplay } />
...
这个Route组件已经被配置为等待,直到浏览器导航到/products URL,这时它将显示ProductDisplay组件。对于所有其他 URL,这个Route组件不会呈现任何内容。
如图 21-2 所示,清单 21-4 中变化的结果在视觉上并不令人印象深刻,但它展示了 URL 路由的基本性质。当浏览器显示应用的起始 URLhttp://localhost:3000时,不会显示任何内容。点击Products或Suppliers链接,浏览器导航到http://localhost:3000/products或http://localhost:3000/suppliers,显示ProductDisplay或SupplierDisplay组件。
图 21-2
添加导航元素
右键单击由Link组件创建的任一导航元素,并从弹出菜单中选择 Inspect 或 Inspect Element,您将看到已经呈现的 HTML,如下所示:
...
<div><a href="/products">Products</a></div>
<div><a href="/suppliers">Suppliers</a></div>
...
已经呈现了Link组件以产生锚(标签为a的元素)元素,并且to属性的值已经转换为锚元素的href属性的 URL。当您单击其中一个锚元素时,浏览器会导航到一个新的 URL,相应的Route组件会显示其内容。如果浏览器导航到一个没有配置Route组件的 URL,那么就不会显示任何内容,这就是为什么在单击其中一个链接之前组件不会显示的原因。
警告
不要试图为导航创建自己的锚元素,因为它们会导致浏览器向服务器发送一个 HTTP 请求,请求您指定的 URL,从而导致应用被重新加载。由Link组件呈现的锚元素具有使用 HTML5 历史 API 更改 URL 的事件处理程序,而不会触发新的 HTTP 请求。
响应导航
Route组件用于实现应用的路由方案,它通过等待浏览器导航到一个特定的 URL 并在它到达时显示一个组件来实现。在真实的应用中,URL 和组件之间的映射可能很复杂,URL 的匹配和组件对内容的选择可以使用表 21-3 中描述的属性进行配置,我将在下面的章节中演示。
表 21-3
路线组件属性
|名字
|
描述
|
| --- | --- |
| path | 属性用于指定组件应该等待的一个或多个 URL。 |
| exact | 当这个属性为true时,只有与路径属性完全相同的 URL 才会被匹配,如“限制与属性的匹配”一节中所示。 |
| sensitive | 当这个属性是true时,匹配的 URL 是区分大小写的。 |
| strict | 当该属性为true时,以/结尾的path值将只匹配其对应段也以/结尾的 URL。 |
| component | 该属性用于指定当path属性与浏览器的当前 URL 匹配时将显示的单个组件。 |
| render | 该属性用于指定一个函数,该函数返回当path属性与浏览器的当前 URL 匹配时将显示的内容。 |
| children | 这个属性用于指定一个总是呈现内容的函数,即使 path 属性指定的 URL 不匹配。这对于显示派生组件中的内容或者不响应 URL 变化而呈现的组件非常有用,如第二十二章所述。 |
选择组件和内容
component属性用于指定当当前 URL 与path属性匹配时将显示的单个组件。组件类型被直接指定为component属性值,如下所示:
...
<Route path="/products" component={ ProductDisplay } />
...
属性的值不应该是一个函数,因为它会导致应用每次更新时创建一个指定组件的新实例。
使用渲染属性
component属性的优点是简单,它适用于具有独立组件的项目,这些组件可以呈现所有需要的内容,并且不需要属性。Route组件为更复杂的内容提供了render属性,并传递属性,如清单 21-5 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/products">Products</Link></div>
<div><Link to="/suppliers">Suppliers</Link></div>
</div>
<div className="col">
<Route path="/products" render={ (routeProps) =>
<ProductDisplay myProp="myValue" /> } />
<Route path="/suppliers" render={ (routeProps) =>
<React.Fragment>
<h4 className="bg-info text-center text-white p-2">
Suppliers
</h4>
<SupplierDisplay />
</React.Fragment>
} />
</div>
</div>
</div>
</Router>
}
}
Listing 21-5Using the render Prop in the Selector.js File in the src Folder
函数的结果是应该由Route组件显示的内容。在清单中,我将一个属性传递给了ProductDisplay组件,并将SupplierDisplay组件包含在一个更大的内容片段中,如图 21-3 所示。
图 21-3
使用路线组件的渲染属性
小费
传递给render prop 的函数接收一个提供路由系统状态信息的对象,我将在第二十二章中描述。
匹配 URL
使用 URL 路由最困难的一个方面是确保您想要支持的 URL 与Route组件正确匹配。Route组件提供了一系列特性,允许您扩大或缩小将要匹配的 URL 的范围,我将在接下来的章节中对此进行描述。
使用线段匹配
匹配 URL 最简单的方法是向Route组件的path属性提供一个或多个目标段。这将匹配以您指定的段开头的任何 URL,如清单 21-6 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
//import { ProductDisplay } from "./ProductDisplay";
//import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/data/one">Link #1</Link></div>
<div><Link to="/data/two">Link #2</Link></div>
<div><Link to="/people/bob">Bob</Link></div>
</div>
<div className="col">
<Route path="/data"
render={ () => this.renderMessage("Route #1") } />
<Route path="/data/two"
render={ () => this.renderMessage("Route #2") } />
</div>
</div>
</div>
</Router>
}
}
Listing 21-6Matching URLs in the Selector.js File in the src Folder
我用一种叫做renderMessage的方法生成的内容替换了ProductDisplay和SupplierDisplay组件。有三个链接组件,分别指向 URL/data/one、data/two和/people/bob。
第一个Route组件配置有/data作为其path支柱。这将匹配第一段是data的任何 URL,这意味着它将匹配/data/one和/data/twoURL,但不匹配/people/bob。第二个Route组件将/data/two作为其path属性的值,因此它将只匹配/data/two URL。每个Route组件独立评估它的path属性,你可以通过点击导航链接看到它们是如何匹配 URL 的,如图 21-4 所示。
图 21-4
将 URL 与路由组件匹配
一个Route组件匹配/data/one URL,两个都匹配/data/two URL,两个都不匹配/people/bob,因此不显示任何内容。
用属性限制比赛
Route组件的默认行为会导致过度匹配,即组件在您不希望的时候匹配 URL。例如,我可能想要区分/data和/data/oneURL,这样第一个 URL 显示数据项列表,第二个显示特定对象的细节。默认匹配使这变得困难,因为/data的path属性匹配任何第一段是/data的 URL,不管 URL 总共包含多少段。
为了帮助限制路径匹配的 URL 的范围,Route组件支持三个额外的属性:exact、strict和sensitive。三个属性中最有用的是exact,只有当它与path属性值完全匹配时,它才会匹配一个 URL,因此/data/one的 URL 不会与/data的路径匹配,如清单 21-7 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/data">Data</Link></div>
<div><Link to="/data/one">Link #1</Link></div>
<div><Link to="/data/two">Link #2</Link></div>
<div><Link to="/people/bob">Bob</Link></div>
</div>
<div className="col">
<Route path="/data" exact={ true }
render={ () => this.renderMessage("Route #1") } />
<Route path="/data/two"
render={ () => this.renderMessage("Route #2") } />
</div>
</div>
</div>
</Router>
}
}
Listing 21-7Making Exact Matches in the Selector.js File in the src Folder
设置exact属性只影响它所应用的Route组件。在示例中,exact prop 阻止第一个Route组件匹配/data/one和/data/twoURL,如图 21-5 所示。
图 21-5
进行精确匹配
当设置为true时,strict属性用于将有斜杠的path的匹配限制到也有斜杠的 URL,因此/data/的path将只匹配/data/ URL 而不是/data。然而,strict prop 确实匹配带有附加段的 URL,因此路径/data/将匹配/data/one。
sensitive属性用于控制区分大小写。当true时,只有当path属性的大小写与 URL 的大小写匹配时,它才允许匹配,因此/data的路径不会与/Data的 URL 匹配。
在一个路径中指定多个 URL
组件的属性的值可以是一个 URL 数组,如果其中任何一个匹配,就会显示内容。当响应不具有公共结构的 URL 需要相同的内容时(例如响应/data/list和/people/list显示相同的组件),或者当需要特定数量的精确匹配时,例如匹配/data/one和/data/two,但不匹配以/data开头的任何其他 URL 时,这可能很有用,如清单 21-8 所示。
注意
在编写本文时,与Route组件所期望的属性类型不匹配,导致在使用数组时出现 JavaScript 控制台警告。此警告可以忽略,在您阅读本章时可能会得到解决。参见第十章和第十一章了解如何指定组件对其属性的数据类型。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/data">Data</Link></div>
<div><Link to="/data/one">Link #1</Link></div>
<div><Link to="/data/two">Link #2</Link></div>
<div><Link to="/people/bob">Bob</Link></div>
</div>
<div className="col">
<Route path={["/data/one", "/people/bob" ] } exact={ true }
render={ () => this.renderMessage("Route #1") } />
<Route path={["/data", "/people" ] }
render={ () => this.renderMessage("Route #2") } />
</div>
</div>
</div>
</Router>
}
}
Listing 21-8Using an Array of Paths in the Selector.js File in the src Folder
path数组是使用花括号作为表达式提供的。第一个Route组件的path属性被设置为包含/data/one和/people/bob的数组。这些路径与exact属性结合起来限制组件匹配的 URL。第二个Route组件被配置为更广泛地匹配,并将响应任何第一段是data或people的 URL,如图 21-6 所示。
图 21-6
使用数组指定路径
用正则表达式匹配 URL
并不是所有的 URL 组合都可以用单独的段来表达,Route组件在它的path属性中支持正则表达式来进行更复杂的匹配,如清单 21-9 所示。
正则表达式清晰与简洁
大多数程序员倾向于用尽可能少的正则表达式来表示路由,但结果可能是路由配置难以阅读,并且在需要更改时很容易被破坏。当决定如何匹配 URL 时,保持表达式简单,并使用一个path数组来扩展一个Route可以匹配的 URL 的范围,而不用使用难以理解的正则表达式。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/data">Data</Link></div>
<div><Link to="/data/one">Link #1</Link></div>
<div><Link to="/data/two">Link #2</Link></div>
<div><Link to="/data/three">Link #3</Link></div>
<div><Link to="/people/bob">Bob</Link></div>
<div><Link to="/people/alice">Alice</Link></div>
</div>
<div className="col">
<Route path={["/data/(one|three)", "/people/b*" ] }
render={ () => this.renderMessage("Route #1") } />
</div>
</div>
</div>
</Router>
}
}
Listing 21-9Using a Regular Expression in the Selector.js File in the src Folder
路径数组中的第一项匹配第一段是data而第二段是one或three的 URL。第二项匹配第一段为 people,第二段以b开头的 URL。结果是Route组件将匹配/data/one、/data/two和/people/bobURL,但不匹配/data/two和/people/aliceURL。
注意
参见 https://github.com/pillarjs/path-to-regexp 了解可用于匹配 URL 的全部正则表达式特性。
进行单一路线匹配
每个Route组件独立评估其path属性;这可能是有用的,但如果您希望基于当前 URL 只显示一个组件,这并不理想。对于这些情况,Redux-Router 包提供了Switch组件,它充当多个Route组件的包装器,按顺序查询它们,并显示第一个组件呈现的内容以匹配当前 URL。清单 21-10 显示了Switch组件的使用。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route, Switch } from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/">Default URL</Link></div>
<div><Link to="/products">Products</Link></div>
<div><Link to="/suppliers">Suppliers</Link></div>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route render={ () =>
this.renderMessage("Fallback Route")} />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 21-10Using the Switch Component in the Selector.js File in the src Folder
Switch组件按照它们被定义的顺序检查它的子组件,这意味着Route组件必须被安排成最具体的 URL 最先出现。没有path属性的Route组件将总是匹配当前的 URL,并且可以被Switch组件用作默认值,类似于常规 JavaScript switch 语句中的default子句。
清单中的变化将/products URL 与ProductDisplay组件相关联,将/suppliers URL 与SupplierDisplay组件相关联。任何其他的 URL 都会使用renderMessage方法呈现一条消息,如图 21-7 所示。
图 21-7
使用开关组件
使用Switch组件允许我在用户点击其中一个导航链接之前应用第一次启动时呈现内容。然而,这只是为默认 URL 选择内容的一种方式,更好的方式是使用Redirect组件,如下一节所述。
使用重定向作为备用路由
对于某些应用,引入一个单独的 URL 作为后备是没有意义的,在这种情况下,Redirect组件可以用来自动触发导航到一个可以由Route组件处理的 URL。在清单 21-11 中,我用重定向到/product URL 替换了现有的回退。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/">Default URL</Link></div>
<div><Link to="/products">Products</Link></div>
<div><Link to="/suppliers">Suppliers</Link></div>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 21-11Using a Redirection in the Selector.js File in the src Folder
to属性指定了Redirect组件将导航到的 URL。如果Route组件能够匹配当前 URL,就不会使用Redirect组件。但是如果Switch组件到达Redirect组件而没有找到匹配的Route,那么将执行到/products的重定向。
执行选择性重定向
使用Redirect组件最常见的方式是只使用to属性,但是还有其他可用的属性可以用来限制何时执行重定向,如表 21-4 中所述。
表 21-4
重定向组件属性
|名字
|
描述
|
| --- | --- |
| to | 这个属性指定了浏览器应该被重定向到的位置。 |
| from | 该属性限制重定向,以便仅当当前 URL 与指定路径匹配时才执行重定向。 |
| exact | 当true时,该属性限制重定向,以便仅当当前 URL 与from属性完全匹配时才执行重定向,执行与Route组件的exact属性相同的角色。 |
| strict | 当true时,该属性限制重定向,因此只有当当前 URL 以/结尾并且path也以/结尾时才执行重定向,执行与Route组件的strict属性相同的角色。 |
| push | 当true时,重定向将向浏览器的历史记录中添加一个新项目。当false时,重定向将替换当前位置。 |
有选择地重定向到一个新的 URL 是保持对不再由Route直接处理的 URL 的支持的一种有用的方式,如清单 21-12 所示。(对Route使用path数组可以达到类似的效果,但是在匹配 URL 参数时会导致复杂化,如第二十二章所述。)
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<div><Link to="/">Default URL</Link></div>
<div><Link to="/products">Products</Link></div>
<div><Link to="/suppliers">Suppliers</Link></div>
<div><Link to="/old/data">Old Link</Link></div>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Redirect from="/old/data" to="/suppliers" />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 21-12Selectively Redirecting URLs in the Selector.js File in the src Folder
新的Redirect将执行从/old/data URL 到/suppliers的重定向。选择性Redirect组件的顺序很重要,必须放在非选择性重定向之前;否则,Switch将无法到达它们,因为它会遍历路由组件列表。
呈现导航链接
Link组件负责生成导航到新 URL 的元素,这是通过使用事件处理程序呈现锚元素来实现的,该事件处理程序在不重新加载应用的情况下更改浏览器的 URL。为了配置其行为,Link组件接受表 21-5 中描述的属性。
表 21-5
链接组件属性
|名字
|
描述
|
| --- | --- |
| to | 该属性用于指定单击链接将导航到的位置。 |
| replace | 该属性用于指定单击导航链接是将一个条目添加到浏览器的历史记录中还是替换当前条目,这决定了用户是否能够使用 back 按钮返回到上一个位置。默认值为 false。 |
| innerRef | 这个属性用于访问底层 HTML 元素的引用。参考文献详情见第十六章。 |
Link组件将把任何其他属性传递给它所呈现的锚元素。这个特性的主要用途是将className属性应用到Link来设计导航链接的样式,如清单 21-13 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Link, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<Link className="m-2 btn btn-block btn-primary"
to="/">Default URL</Link>
<Link className="m-2 btn btn-block btn-primary"
to="/products">Products</Link>
<Link className="m-2 btn btn-block btn-primary"
to="/suppliers">Suppliers</Link>
<Link className="m-2 btn btn-block btn-primary"
to="/old/data">Old Link</Link>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Redirect from="/old/data" to="/suppliers" />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 21-13Applying Classes in the Selector.js File in the src Folder
Bootstrap CSS 框架能够将锚元素样式化为按钮,我在清单 21-13 中应用的类应用了一个按钮样式,它填充了可用的水平空间,并允许我删除用于垂直堆叠导航链接的div元素。当Link组件呈现它们的内容时,结果是一个显示为按钮的导航链接,如图 21-8 所示。
图 21-8
将类传递给导航元素
指示活动航路
NavLink组件建立在基本的Link特性之上,但是当它的to属性的值与当前 URL 匹配时,它会向锚元素添加一个类或样式。表 21-6 描述了NavLink组件提供的属性,这些属性是对表 21-5 中描述的属性的补充。在清单 21-14 中,我介绍了应用active类的NavLink组件。
表 21-6
NavLink 组件属性
|名字
|
描述
|
| --- | --- |
| activeClassName | 这个属性指定了当链接处于活动状态时将被添加到锚元素中的类。 |
| activeStyle | 该属性指定当链接激活时将添加到锚元素的样式。样式被指定为 JavaScript 对象,其属性是样式名称。 |
| exact | 当true时,这个属性执行精确匹配,如“匹配 URL”一节所述。 |
| strict | 当true时,该属性强制严格匹配,如“匹配 URL”一节所述。 |
| isActive | 该属性可用于指定一个自定义函数,以确定链接是否处于活动状态。该函数接收match和location参数,如第二十二章所述。默认行为是将当前 URL 与to属性进行比较。 |
import React, { Component } from "react";
import { BrowserRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
renderMessage = (msg) => <h5 className="bg-info text-white m-2 p-2">{ msg }</h5>
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/">Default URL</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/old/data">Old Link</NavLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Redirect from="/old/data" to="/suppliers" />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 21-14Using NavLink Components in the Selector.js File in the src Folder
当浏览器的 URL 与组件的to属性的值匹配时,锚元素被添加到active类中,为用户提供一个有用的指示器,如图 21-9 所示。
图 21-9
响应路由激活
请注意,默认的 URL 按钮总是高亮显示。NavLink组件依赖于Route URL 匹配,这意味着/的to属性将匹配任何 URL。表 21-6 中描述的exact和strict属性与应用于Route时的目的相同,清单 21-15 显示了使用exact属性来限制匹配。
...
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active" exact={ true }
to="/">Default URL</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/old/data">Old Link</NavLink>
</div>
...
Listing 21-15Restricting NavLink Matching in the Selector.js File in the src Folder
结果是NavLink不再突出显示,如图 21-10 所示。
图 21-10
限制突出显示的 URL 匹配
注意
当应用activeClassName值时,NavLink组件不允许移除类,这意味着我不能准确地从示例项目中重新创建原始效果。我将在第二十二章中演示如何用自定义导航组件创建这一功能。
选择和配置路由
URL 路由依赖于操纵浏览器的 URL 来执行导航,而无需向服务器发送 HTTP 请求。在 web 应用中,核心路由功能由BrowserRouter或HashRouter组件提供;两者在进口时都被习惯性地命名为Router,就像这样:
...
import { BrowserRouter as Router, Link, Switch, Route, Redirect }
from "react-router-dom";
...
BrowserRouter使用 HTML5 历史 API。这个 API 为路由提供了自然的 URL,比如http://localhost:3000/products,这是你在本章的例子中看到的 URL 类型。BrowserRouter组件可以接受一系列配置其行为的属性,如表 21-7 所述。props 的默认值适用于大多数应用。
表 21-7
浏览器路由属性
|名字
|
描述
|
| --- | --- |
| basename | 当应用不在它的 URL 的根目录时,例如http://localhost:3000/myapp,使用这个属性。 |
| getUserConfirmation | 该属性用于指定通过Prompt组件获得用户导航确认的功能,如第二十二章所述。 |
| forceRefresh | 当true时,这个 prop 在导航过程中使用发送到服务器的 HTTP 请求强制进行完全刷新。这削弱了富客户端应用的作用,应该只用于测试和浏览器不能使用历史 API 的时候。 |
| keyLength | 导航中的每个变化都有一个唯一的键。该属性用于指定密钥的长度,默认为六个字符。密钥包含在识别每个导航位置的location对象中,如第二十二章所述。 |
| history | 这个属性允许使用一个自定义的history对象。第二十二章中描述了history对象。 |
使用 HashRouter 组件
老版本的浏览器不支持历史 API,导航细节必须作为一个片段添加到 URL 的末尾,跟在#字符后面。使用 URL 片段的路由由HashRouter组件提供,如清单 21-16 所示。
import React, { Component } from "react";
import { HashRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
// ...methods omitted for brevity...
}
Listing 21-16Using the HashRouter Component in the Selector.js File in the src Folder
使用as关键字导入路由组件意味着只有import语句需要修改。将修改保存到文件中,导航到http://localhost:3000,你会看到 URL 的样式已经改变,如图 21-11 所示。
图 21-11
使用哈希路由
小费
当浏览器首次重新加载时,您可能会看到类似于http://localhost:3000/suppliers/#/suppliers的 URL。发生这种情况是因为浏览器从其当前 URL 重新加载,然后该 URL 被假定为应用的基本 URL。手动导航到http://localhost:3000,您应该会看到如图所示的 URL。
URL 中用于路由的部分现在跟在#字符之后。URL 路由仍然以同样的方式工作,但是与由BrowserRouter组件生成的 URL 相比,URL 不那么自然。HashRouter组件可以配置表 21-8 所示的属性。
表 21-8
HashRouter 组件属性
|名字
|
描述
|
| --- | --- |
| basename | 当应用不在它的 URL 的根目录时,例如http://localhost:3000/myapp,使用这个属性。 |
| getUserConfirmation | 该属性用于指定通过Prompt组件获得用户导航确认的功能,如第二十二章所述。 |
| hashType | 该属性设置用于在 URL 中编码路由的样式。选项有slash,创建如图 21-11 所示的 URL 样式;noslash,省略了#字符后的前导/;还有hashbang,它通过在#字符后插入感叹号来创建像#!/products这样的 URL。 |
摘要
在本章中,我向您展示了如何使用 React-Router 包向 React 应用添加 URL 路由。我解释了路由如何通过将状态数据移入 URL 来简化应用,以及如何使用Link和Route组件来创建导航元素并响应 URL 的变化。在下一章,我将描述高级 URL 路由特性。
二十二、高级 URL 路由
在这一章中,我描述了 React-Router 包中 URL 路由可用的高级特性。我将向您展示如何创建可以参与路由过程的组件,如何以编程方式导航,如何以编程方式生成路由,以及如何在连接到数据存储的组件中使用 URL 路由。表 22-1 将高级 URL 路由功能放在上下文中。
表 22-1
将高级 URL 路由放在上下文中
|问题
|
回答
| | --- | --- | | 这是什么? | 高级路由功能提供对 URL 路由系统的编程访问。 | | 为什么有用? | 这些功能允许组件知道路由系统和当前活动的路由。 | | 如何使用? | props 提供对高级路由功能的访问。 | | 有什么陷阱或限制吗? | 这些都是高级功能,需要注意确保它们正确集成到组件中。 | | 还有其他选择吗? | 这些是可选功能。应用可以使用第二十一章中描述的标准功能,或者完全避免 URL 路由。 |
表 22-2 总结了本章内容。
表 22-2
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 接收组件中路由系统的详细信息 | 使用Route组件提供的属性或使用withRouter高阶组件 | 3, 4, 10–12, 19–23 |
| 获取当前导航位置的详细信息 | 使用location属性 | five |
| 从当前路线获取 URL 段 | 向 URL 添加参数 | 6–9 |
| 以编程方式导航 | 使用由history属性定义的方法 | 13, 14 |
| 导航前提示用户 | 使用Prompt组件 | 15–17 |
为本章做准备
在本章中,我继续使用第二十一章中的productapp项目。为了准备本章,将应用使用的路由从HashRouter更改为BrowserRouter,以便使用 HTML5 历史 API 进行导航,并简化了Link和Router组件,如清单 22-1 所示。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
import React, { Component } from "react";
import { BrowserRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-1Changing Routers and Routes in the Selector.js File in the src Folder
打开命令提示符,导航到productapp文件夹,运行清单 22-2 中所示的命令来启动开发工具。
npm start
Listing 22-2Starting the Development Tools
应用编译完成后,开发 HTTP 服务器将启动并显示如图 22-1 所示的内容。
图 22-1
运行示例应用
创建路由感知组件
当一个Route显示一个组件时,它向组件提供描述当前路线的上下文数据,并提供对可用于导航的 API 的访问,允许组件知道当前位置并参与路线选择。当使用component属性时,Route将数据和 API 传递给它显示为属性的组件,名为match、location和history。当使用render属性时,渲染函数被传递一个具有match、location和history属性的对象,这些属性的值与用作render属性的对象相同。表 22-3 中描述了match、location和history对象。
表 22-3
路由组件提供的属性
|名字
|
描述
|
| --- | --- |
| match | 此属性提供有关路由组件如何匹配当前浏览器 URL 的信息。 |
| location | 该属性提供了当前位置的表示,可用于导航,而不是表示为字符串的 URL。 |
| history | 这个属性提供了一个可用于导航的 API,如“以编程方式导航”一节中所示。 |
了解比赛属性
match prop 提供了一个组件,其中包含了父节点Route如何匹配当前 URL 的细节。正如我在第二十一章中所演示的,单个Route可以用来匹配一系列 URL,路由感知组件通常需要关于当前 URL 的细节,这些细节可以通过表 22-4 中所示的属性获得。
表 22-4
匹配属性
|名字
|
描述
|
| --- | --- |
| url | 该属性返回与Route匹配的 URL。 |
| path | 该属性返回用于匹配 URL 的path值。 |
| params | 该属性返回路由参数,这些参数允许将 URL 段映射到变量,如“使用 URL 参数”一节中所述。 |
| isExact | 如果路由路径与 URL 完全匹配,该属性返回true。 |
为了演示路由属性的使用,我创建了src/routing文件夹,并添加了一个名为RouteInfo.js的文件,其组件如清单 22-3 所示,显示了match属性的值。
import React, { Component } from "react";
export class RouteInfo extends Component {
renderTable(title, prop, propertyNames) {
return <React.Fragment>
<tr><th colSpan="2" className="text-center">{ title }</th></tr>
{ propertyNames.map(p =>
<tr key={p }>
<td>{ p }</td>
<td>{ JSON.stringify(prop[p]) }</td>
</tr>)
}
</React.Fragment>
}
render() {
return <div className="bg-info m-2 p-2">
<h4 className="text-white text-center">Route Info</h4>
<table className="table table-sm table-striped bg-light">
<tbody>
{ this.renderTable("Match", this.props.match,
["url", "path", "params", "isExact"] )}
</tbody>
</table>
</div>
}
}
Listing 22-3The Contents of the RouteInfo.js File in the src/routing Folder
RouteInfo组件在一个表格中显示匹配属性的url、path、params和isExact属性,并允许我稍后轻松地添加其他路由属性的附加细节。属性是序列化的,因为值是对象和布尔值的混合,如果按字面意思使用,可能会导致显示问题。在清单 22-4 中,我添加了一个到Selector组件的导航链接,以及一个显示RouteInfo组件的Route。
import React, { Component } from "react";
import { BrowserRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active" to="/info">Route Info</NavLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-4Adding a Route in the Selector.js File in the src Folder
保存更改并点击Route Info链接,您将看到match属性的详细信息,如图 22-2 所示。显示的值表明Route组件的路径属性是/info,它匹配新Link组件指向的/info URL。随着我引入更高级的路由特性,特别是当我在“使用 URL 参数”一节中引入 URL 参数时,match prop 提供的信息将变得更加有用。
图 22-2
匹配路由属性提供的详细信息
了解位置属性
location对象用于描述导航位置。作为属性提供的location对象描述了当前位置,具有表 22-5 中描述的属性。
表 22-5
位置属性
|名字
|
描述
|
| --- | --- |
| key | 此属性返回标识位置的键。 |
| pathname | 此属性返回位置的路径。 |
| search | 该属性返回位置 URL 的搜索词(URL 中跟在?字符后面的部分)。 |
| hash | 该属性返回位置 URL 的 URL 片段(跟在#字符后面的部分)。 |
| state | 该属性用于将任意数据与位置相关联。 |
location属性提供了一些与match属性的重叠,但是其思想是组件可以保留一个位置对象,并使用它来引用一个位置,而不是使用字符串作为Link、NavLink和Redirect组件的to属性的值。在清单 22-5 中,我将 location prop 添加到了由RouteInfo显示的数据中,同时添加了一个使用location对象作为导航目标的Link元素。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class RouteInfo extends Component {
renderTable(title, prop, propertyNames) {
return <React.Fragment>
<tr><th colSpan="2" className="text-center">{ title }</th></tr>
{ propertyNames.map(p =>
<tr key={p }>
<td>{ p }</td>
<td>{ JSON.stringify(prop[p]) }</td>
</tr>)
}
</React.Fragment>
}
render() {
return <div className="bg-info m-2 p-2">
<h4 className="text-white text-center">Route Info</h4>
<table className="table table-sm table-striped bg-light">
<tbody>
{ this.renderTable("Match", this.props.match,
["url", "path", "params", "isExact"] )}
{ this.renderTable("Location", this.props.location,
["key", "pathname", "search", "hash", "state"] )}
</tbody>
</table>
<div className="text-center m-2 bg-light">
<Link className="btn btn-primary m-2"
to={ this.props.location }>Location</Link>
</div>
</div>
}
}
Listing 22-5Using the Location Prop in the RouteInfo.js File in the src/routing Folder
图 22-3 显示了定位支柱和新Link组件的细节。
图 22-3
显示位置路由属性的详细信息
使用location属性作为Link组件的to属性的值目前并不是特别有用,因为它只能导航到当前位置。正如您将看到的,组件可用于响应多条路线,并可能随着时间的推移接收一系列位置,这使得使用location对象既有用又比使用字符串表示的 URL 更方便。
使用 URL 参数
当组件知道 URL 路由系统时,它通常需要调整其行为以适应当前的 URL。React-Router 包支持 URL 参数,该参数将 URL 段的内容分配给一个可由组件读取的变量,从而允许组件响应当前位置,而无需解析 URL 或理解其结构。清单 22-6 显示添加了一个Route,它的路径包括一个 URL 参数和指向它的Link组件。
import React, { Component } from "react";
import { BrowserRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/info/match">Match</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/info/location">Location</NavLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-6Defining a URL Parameter in the Selector.js File in the src Folder
URL 参数被指定为以冒号(:字符)开头的path属性段。在这个例子中,RouteInfo组件的Route有一个带有名为datatype的 URL 参数的path属性。
...
<Route path="/info/:datatype" component={ RouteInfo } />
...
当Route匹配一个 URL 时,它会将第二段的值赋给一个名为datatype的 URL 参数,该参数将通过match prop 的params属性传递给RouteInfo组件。如果您点击添加到清单 22-6 中示例的导航链接,您将看到为params属性显示不同的值,如图 22-4 所示。
图 22-4
通过匹配属性接收 URL 参数
当 URL 为/info/match时,datatype参数的值为match。当 URL 为/info/location时,datatype 参数的值为location。在清单 22-7 中,我已经更新了RouteInfo组件,使用datatype属性来选择呈现给用户的上下文数据。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class RouteInfo extends Component {
renderTable(title, prop, propertyNames) {
return <React.Fragment>
<tr><th colSpan="2" className="text-center">{ title }</th></tr>
{ propertyNames.map(p =>
<tr key={p }>
<td>{ p }</td>
<td>{ JSON.stringify(prop[p]) }</td>
</tr>)
}
</React.Fragment>
}
render() {
return <div className="bg-info m-2 p-2">
<h4 className="text-white text-center">Route Info</h4>
<table className="table table-sm table-striped bg-light">
<tbody>
{ this.props.match.params.datatype ==="match"
&& this.renderTable("Match", this.props.match,
["url", "path", "params", "isExact"] )}
{ this.props.match.params.datatype === "location"
&& this.renderTable("Location", this.props.location,
["key", "pathname", "search", "hash", "state"] )}
</tbody>
</table>
<div className="text-center m-2 bg-light">
<Link className="btn btn-primary m-2"
to={ this.props.location }>Location</Link>
</div>
</div>
}
}
Listing 22-7Using a URL Parameter Prop in the RouteInfo.js File in the src/routing Folder
该组件接收 URL 参数作为路由属性的一部分,并像使用任何其他属性一样使用它们。在清单中,datatype URL 参数的值用于显示匹配或位置对象的内联表达式,如图 22-5 所示。
图 22-5
通过选择内容来响应 URL 参数
了解不透明的 URL 结构
URL 参数不仅仅是组件接收 URL 段内容的一种便捷方式。它们还将 URL 的结构从它所针对的组件中分离出来,允许在不修改组件的情况下修改 URL 的结构或多个 URL 针对相同的内容。例如,清单 22-7 中的组件依赖于datatype URL 参数,但是不依赖于获取它的 URL 部分。这意味着组件将与路径如/info/:datatype一起工作,但是也可以与路径如/diagnostics/routing/:datatype匹配,而不需要改变组件的代码。
URL 参数的优点是组件只需要知道它所需要的 URL 参数的名称,而不需要知道它们出现在 URL 中的详细位置。
使用可选的 URL 参数
添加 URL 参数意味着/info URL 将不再与Route组件匹配。我可以通过添加另一个Route来解决这个问题,但是一个更好的方法是使用一个可选参数,这将允许 URL 匹配path,即使没有相应的段。在清单 22-8 中,我添加了一个导航到/info URL 的NavLink,并更改了Route组件的路径,因此datatype参数是可选的。
import React, { Component } from "react";
import { BrowserRouter as Router, NavLink, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/products">Products</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/suppliers">Suppliers</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/info/match">Match</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active"
to="/info/location">Location</NavLink>
<NavLink className="m-2 btn btn-block btn-primary"
activeClassName="active" to="/info">All Info</NavLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype?" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-8Using an Optional URL Parameter in the Selector.js File in the src Folder
可选的 URL 参数在参数名称后用一个问号(?字符)表示,因此datatype?表示一个可选参数,如果在 URL 中有相应的段,它将被命名为datatype。如果没有段,路径仍然是match,但是没有datatype值。在清单 22-9 中,我已经更新了RouteInfo组件,如果没有datatype值,它将显示match和location对象的详细信息。
小费
关于指定 URL 参数的不同方式的完整列表,请参见 https://github.com/pillarjs/path-to-regexp ,它是处理 URL 的包的 GitHub 存储库。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class RouteInfo extends Component {
renderTable(title, prop, propertyNames) {
return <React.Fragment>
<tr><th colSpan="2" className="text-center">{ title }</th></tr>
{ propertyNames.map(p =>
<tr key={p }>
<td>{ p }</td>
<td>{ JSON.stringify(prop[p]) }</td>
</tr>)
}
</React.Fragment>
}
render() {
return <div className="bg-info m-2 p-2">
<h4 className="text-white text-center">Route Info</h4>
<table className="table table-sm table-striped bg-light">
<tbody>
{ (this.props.match.params.datatype === undefined ||
this.props.match.params.datatype ==="match")
&& this.renderTable("Match", this.props.match,
["url", "path", "params", "isExact"] )}
{ (this.props.match.params.datatype === undefined ||
this.props.match.params.datatype === "location")
&& this.renderTable("Location", this.props.location,
["key", "pathname", "search", "hash", "state"] )}
</tbody>
</table>
<div className="text-center m-2 bg-light">
<Link className="btn btn-primary m-2"
to={ this.props.location }>Location</Link>
</div>
</div>
}
}
Listing 22-9Handling an Optional URL Parameter in the RouteInfo.js File in the src Folder
如果 URL 中没有匹配的段,datatype参数的值将是undefined。清单中的变化和可选 URL 参数的添加允许组件响应更广泛的 URL,而不需要使用额外的Route组件。
访问其他组件中的路由数据
A Route会给它显示的组件添加属性但是不能直接提供给其他组件,包括它显示的组件的后代。为了避免正确的线程化,React-Router 包提供了两种不同的方法来访问后代组件中的路由数据,如下面几节所述。
直接在组件中访问路由数据
访问路由数据最直接的方法是在render方法中使用Route。为了演示,我在src/routing文件夹中添加了一个名为ToggleLink.js的文件,并用它来定义清单 22-10 中所示的组件。
小费
这是我在第一部分的 SportsStore 应用中用来突出显示活动路线的相同组件。
import React, { Component } from "react";
import { Route, Link } from "react-router-dom";
export class ToggleLink extends Component {
render() {
return <Route path={ this.props.to } exact={ this.props.exact }
children={ routeProps => {
const baseClasses = this.props.className || "m-2 btn btn-block";
const activeClass = this.props.activeClass || "btn-primary";
const inActiveClass = this.props.inActiveClass || "btn-secondary"
const combinedClasses =
`${baseClasses} ${routeProps.match ? activeClass : inActiveClass}`
return <Link to={ this.props.to } className={ combinedClasses }>
{ this.props.children }
</Link>
}} />
}
}
Listing 22-10The Contents of the ToggleLink.js File in the src/routing Folder
不管当前 URL 是什么,Route组件的children属性用于呈现内容,并被分配一个接收路由上下文数据的函数。path属性用于表示对一个 URL 的兴趣,当当前 URL 与path匹配时,传递给子函数的routeProps对象包含一个match对象,该对象定义了表 22-4 中描述的属性。
ToggleLink组件允许我解决在NavLink组件和引导 CSS 框架之间出现的一个小问题。NavLink的工作原理是,当路径匹配时,向它呈现的锚元素添加一个类,其余时间删除它。这给一些引导类的组合带来了问题,因为它们在 CSS 样式表中定义的顺序意味着一些类,比如btn-primary,在一个相关的类,比如btn-secondary被移除之前不会生效。
ToggleLink组件通过在有match对象时添加一个活动类,在没有match时添加一个非活动类来解决这个问题。
...
const combinedClasses =
`${baseClasses} ${routeProps.match ? activeClass : inActiveClass}`
...
一个Link仍然被用来生成导航元素和响应点击,但是被ToggleLink组件设计成这样,我可以自由使用引导 CSS 类。在清单 22-11 中,我用一个ToggleLink替换了每个NavLink。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
import { ToggleLink } from "./routing/ToggleLink";
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/products">Products</ToggleLink>
<ToggleLink to="/suppliers">Suppliers</ToggleLink>
<ToggleLink to="/info/match">Match</ToggleLink>
<ToggleLink to="/info/location">Location</ToggleLink>
<ToggleLink to="/info" exact={ true }>All Info</ToggleLink>
</div>
<div className="col">
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype?" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-11Replacing Navigation Components in the Selector.js File in the src Folder
我依赖于清单 22-10 中指定的默认类,结果是导航按钮在活动时被添加到引导程序btn-primary类,在不活动时被添加到btn-secondary类,如图 22-6 所示。
图 22-6
直接在组件中访问路由数据
使用高阶组件访问路由数据
withRouter函数是一个高阶组件,它提供对路由系统的访问,而不直接使用Route(尽管这是在withRouter函数中使用的技术)。当一个组件被传递给withRouter时,它接收match、location和history对象作为属性,就好像它是由Route使用component属性直接渲染的一样。对于编写呈现Route的组件来说,这是一种方便的替代方式。在清单 22-12 中,我使用了withRouter函数来允许在Route之外使用RouteInfo组件。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect, withRouter }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
import { ToggleLink } from "./routing/ToggleLink";
const RouteInfoHOC = withRouter(RouteInfo)
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/products">Products</ToggleLink>
<ToggleLink to="/suppliers">Suppliers</ToggleLink>
<ToggleLink to="/info/match">Match</ToggleLink>
<ToggleLink to="/info/location">Location</ToggleLink>
<ToggleLink to="/info" exact={ true }>All Info</ToggleLink>
</div>
<div className="col">
<RouteInfoHOC />
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype?" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-12Creating a Routing HOC in the Selector.js File in the src Folder
withRouter函数用于向RouteInfo组件提供它所需要的数据,即使它没有被Route显示。结果是match和location对象的细节总是被显示,如图 22-7 所示。
图 22-7
使用 withrouter HOC
withRouter函数不提供对匹配路径的支持,这意味着 match 对象用处不大。然而,location对象提供了应用当前位置的细节,而history对象可以用于编程导航,这将在下一节中描述。
以编程方式导航
并不是所有的导航都可以使用Link或NavLink组件来处理,特别是当应用需要执行一些内部动作来响应一个事件,然后才执行导航时。提供给组件的history对象提供了一个 API,允许编程访问路由系统,使用表 22-6 中描述的方法。history对象为导航提供了一致的接口,不管应用是使用 HTML5 历史 API 还是 URL 片段。
表 22-6
历史方法
|名字
|
描述
|
| --- | --- |
| push(path) | 此方法导航到指定的路径,并在浏览器的历史记录中添加一个新条目。可以通过location.state属性提供一个可选的状态属性。 |
| replace(path) | 此方法导航到指定的路径,并替换浏览器历史记录中的当前位置。可以通过location.state属性提供一个可选的状态属性。 |
| goBack() | 此方法导航到浏览器历史记录中的上一个位置。 |
| goForward() | 此方法导航到浏览器历史记录中的下一个位置。 |
| go(n) | 该方法从当前位置导航到历史位置n处。使用正值向前移动,负值向后移动。 |
| block(prompt) | 该方法会阻止导航,直到用户对提示做出响应,如“导航前提示用户”一节中所述。 |
在清单 22-13 中,我用一个按钮替换了ToggleLink组件中的Link,该按钮的事件处理程序以编程方式导航。
import React, { Component } from "react";
import { Route } from "react-router-dom";
export class ToggleLink extends Component {
handleClick = (history) => {
history.push(this.props.to);
}
render() {
return <Route path={ this.props.to } exact={ this.props.exact }
children={ routeProps => {
const baseClasses = this.props.className || "m-2 btn btn-block";
const activeClass = this.props.activeClass || "btn-primary";
const inActiveClass = this.props.inActiveClass || "btn-secondary"
const combinedClasses =
`${baseClasses} ${routeProps.match ? activeClass : inActiveClass}`
return <button className={ combinedClasses }
onClick={ () => this.handleClick(routeProps.history) }>
{this.props.children}
</button>
}} />
}
}
Listing 22-13Navigating Programmatically in the ToggleLink.js File in the src/router Folder
onClick处理程序将从Route组件接收的history对象传递给handleClick方法,后者使用push方法导航到由to属性指定的位置。没有明显的区别,因为由Link组件呈现的锚元素已经被设计成按钮,但是ToggleLink组件现在直接处理它的导航。
使用组件以编程方式导航
使用history对象的另一种方法是呈现执行导航的组件。在清单 22-14 中,我已经更改了ToggleLink组件,因此单击button元素会更新状态数据,从而导致呈现一个Redirect。
import React, { Component } from "react";
import { Route, Redirect } from "react-router-dom";
export class ToggleLink extends Component {
constructor(props) {
super(props);
this.state = {
doRedirect: false
}
}
handleClick = () => {
this.setState({ doRedirect: true},
() => this.setState({ doRedirect: false }));
}
render() {
return <Route path={ this.props.to } exact={ this.props.exact }
children={ routeProps => {
const baseClasses = this.props.className || "m-2 btn btn-block";
const activeClass = this.props.activeClass || "btn-primary";
const inActiveClass = this.props.inActiveClass || "btn-secondary"
const combinedClasses =
`${baseClasses} ${routeProps.match ? activeClass : inActiveClass}`
return <React.Fragment>
{ this.state.doRedirect && <Redirect to={ this.props.to } /> }
<button className={ combinedClasses } onClick={ this.handleClick }>
{this.props.children}
</button>
</React.Fragment>
}} />
}
}
Listing 22-14Navigating Using Components in the ToggleLink.js File in the src/router Folder
单击该按钮会将doRedirect属性设置为true,这将触发呈现Redirect组件的更新。doRedirect属性被自动设置回false,以便组件的正常内容被再次呈现。结果和清单 22-13 一样,选择一种方式是个人喜好和个人风格的问题。
导航前提示用户
可以通过呈现一个Prompt来延迟导航,它允许用户确认或取消导航,通常用于避免意外放弃表单数据。Prompt组件支撑表 22-7 中所述的支柱。
表 22-7
提示组件属性
|名字
|
描述
|
| --- | --- |
| message | 这个属性定义了显示给用户的消息。它可以表示为一个字符串,也可以表示为接受一个location对象并返回一个字符串的函数。 |
| when | 只有当这个属性的值等于true时,它才会提示用户,并且可以用来有条件地阻止导航。 |
只使用了一个Prompt,但是它呈现在哪里并不重要,因为它不会执行任何操作,直到应用更改到一个新位置,这时用户将被要求确认导航。在清单 22-15 中,我给Selector组件添加了一个Prompt。
小费
只需要一个Prompt,并且您不应该在执行导航的组件中呈现额外的Prompt实例,例如示例应用中的ToggleLink组件。如果您呈现多个Prompt组件,您将在 JavaScript 控制台中收到一个警告。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect, withRouter, Prompt }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
import { ToggleLink } from "./routing/ToggleLink";
const RouteInfoHOC = withRouter(RouteInfo)
export class Selector extends Component {
render() {
return <Router>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/products">Products</ToggleLink>
<ToggleLink to="/suppliers">Suppliers</ToggleLink>
<ToggleLink to="/info/match">Match</ToggleLink>
<ToggleLink to="/info/location">Location</ToggleLink>
<ToggleLink to="/info" exact={ true }>All Info</ToggleLink>
</div>
<div className="col">
<Prompt message={ loc =>
`Do you want to navigate to ${loc.pathname}`} />
<RouteInfoHOC />
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype?" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-15Prompting the User in the Selector.js File in the src Folder
要查看Prompt的效果,单击由ToggleLink组件呈现的按钮元素之一。你将被要求确认导航,如图 22-8 所示。
图 22-8
导航前提示用户
小费
如果您喜欢使用history对象进行导航,那么可以使用block方法来设置一个提示,该提示将呈现给用户,如下一节所示。
呈现自定义导航提示
BrowserRouter和HashRouter组件提供了一个getUserConfirmation属性,用于用自定义函数替换默认提示。为了向用户显示与应用其余内容一致的提示,我在src/routing文件夹中添加了一个名为CustomPrompt.js的文件,并用它来定义清单 22-16 中所示的组件。
import React, { Component } from "react";
export class CustomPrompt extends Component {
render() {
if (this.props.show) {
return <div className="alert alert-warning m-2 text-center">
<h4 className="alert-heading">Navigation Warning</h4>
{ this.props.message }
<div className="p-1">
<button className="btn btn-primary m-1"
onClick={ () => this.props.callback(true) }>
Yes
</button>
<button className="btn btn-secondary m-1"
onClick={ () => this.props.callback(false )}>
No
</button>
</div>
</div>
}
return null;
}
}
Listing 22-16The Contents of the CustomPrompt.js File in the src/routing Folder
CustomPrompt组件负责向用户显示一条消息,并提供 Yes 和 No 按钮,这些按钮调用一个回调函数来确认或阻止导航。在清单 22-17 中,我在Selector组件中应用了CustomPrompt,以及管理提示过程所需的状态数据。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect, withRouter, Prompt }
from "react-router-dom";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
import { RouteInfo } from "./routing/RouteInfo";
import { ToggleLink } from "./routing/ToggleLink";
import { CustomPrompt } from "./routing/CustomPrompt";
const RouteInfoHOC = withRouter(RouteInfo)
export class Selector extends Component {
constructor(props) {
super(props);
this.state = {
showPrompt: false,
message: "",
callback: () => {}
}
}
customGetUserConfirmation = (message, navCallback) => {
this.setState({
showPrompt: true, message: message,
callback: (allow) => { navCallback(allow);
this.setState({ showPrompt: false}) }
});
}
render() {
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/products">Products</ToggleLink>
<ToggleLink to="/suppliers">Suppliers</ToggleLink>
<ToggleLink to="/info/match">Match</ToggleLink>
<ToggleLink to="/info/location">Location</ToggleLink>
<ToggleLink to="/info" exact={ true }>All Info</ToggleLink>
</div>
<div className="col">
<CustomPrompt show={ this.state.showPrompt }
message={ this.state.message }
callback={ this.state.callback } />
<Prompt message={ loc =>
`Do you want to navigate to ${loc.pathname}?`} />
<RouteInfoHOC />
<Switch>
<Route path="/products" component={ ProductDisplay} />
<Route path="/suppliers" component={ SupplierDisplay } />
<Route path="/info/:datatype?" component={ RouteInfo } />
<Redirect to="/products" />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-17Appling a Custom Prompt in the Selector.js File in the src Folder
由BrowserRouter和HashRouter支持的getUserConfirmation prop 被分配一个函数,该函数接收一个显示给用户的消息和一个由用户决定调用的回调:true处理导航,falseblock它。在清单中,getUserConfirmation属性将调用customGetUserConfirmation方法,该方法更新用于CustomPrompt属性的状态数据,结果是提示用户,如图 22-9 所示。
图 22-9
使用自定义提示
小费
注意,我仍然需要使用一个Prompt,它负责触发显示CustomPrompt的流程。
以编程方式生成路线
Selector组件使用ToggleLink和Route组件来建立应用支持的 URL 和它们相关的内容之间的映射,但是在我添加对 URL 路由的支持之前,这不是应用的工作方式。相反,App组件将Selector视为一个容器,并为其提供子组件进行显示,如下所示:
import React, { Component } from "react";
import { Provider } from "react-redux";
import dataStore from "./store";
import { Selector } from "./Selector";
import { ProductDisplay } from "./ProductDisplay";
import { SupplierDisplay } from "./SupplierDisplay";
export default class App extends Component {
render() {
return <Provider store={ dataStore }>
<Selector>
<ProductDisplay name="Products" />
<SupplierDisplay name="Suppliers" />
</Selector>
</Provider>
}
}
在 React 开发中,使用提供服务的容器组件而不使用其子组件的硬编码知识是很重要的,并且在使用 React-Router 时可以很容易地应用,因为路由是使用组件定义和处理的。在清单 22-18 中,我修改了Selector组件,删除了本地定义的路线,改为从children属性中生成。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect, Prompt }
from "react-router-dom";
// import { ProductDisplay } from "./ProductDisplay";
// import { SupplierDisplay } from "./SupplierDisplay";
//import { RouteInfo } from "./routing/RouteInfo";
import { ToggleLink } from "./routing/ToggleLink";
import { CustomPrompt } from "./routing/CustomPrompt";
//const RouteInfoHOC = withRouter(RouteInfo)
export class Selector extends Component {
constructor(props) {
super(props);
this.state = {
showPrompt: false,
message: "",
callback: () => {}
}
}
customGetUserConfirmation = (message, navCallback) => {
this.setState({
showPrompt: true, message: message,
callback: (allow) => { navCallback(allow);
this.setState({ showPrompt: false}) }
});
}
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<CustomPrompt show={ this.state.showPrompt }
message={ this.state.message }
callback={ this.state.callback } />
<Prompt message={ loc =>
`Do you want to navigate to ${loc.pathname}?`} />
<Switch>
{ routes.map( r => <Route key={ r.url } path={ r.url }
render={ () => r.component } />)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-18Generating Routes from Children in the Selector.js File in the src Folder
Selector处理其子进程以建立 URL 和组件之间的映射,并生成所需的ToggleLink和Route组件,我用一个Redirect组件对其进行了补充,产生了如图 22-10 所示的结果。
图 22-10
以编程方式生成路线
对连接的数据存储组件使用路由
为了在示例应用中完成路由的采用,我将把协调组件的剩余状态数据移出数据存储,并使用表 22-8 中描述的一组 URL 对其进行管理。
表 22-8
示例应用的 URL
|名字
|
描述
|
| --- | --- |
| /products/table | 该 URL 将显示产品列表。 |
| /products/create | 该 URL 将显示编辑器,允许创建新产品。 |
| /products/edit/4 | 此 URL 将显示编辑器,允许编辑现有产品,其中最后一个 URL 段标识要更改的产品。 |
| /suppliers/table | 该 URL 将显示供应商列表。 |
| /suppliers/create | 该 URL 将显示编辑器,允许创建新的供应商。 |
| /suppliers/edit/4 | 该 URL 将显示编辑器,允许编辑现有的供应商,其中最后一个 URL 段标识要更改的供应商。 |
应用需要的 URL 可以用带有 URL 参数的单一路径来处理,如下所示:
...
/:datatype/:mode?/:id?
...
在接下来的小节中,我将更新应用中的组件,以便数据存储仅用于模型数据,而应该向用户显示的内容的细节在 URL 中表示。(这种硬分离只是一种方法,如果适合您的项目,您可以采取更柔和的方法,以便一些状态数据在数据存储中处理,一些通过 URL 处理。在 React 开发中,没有绝对正确的方法。)
更换显示器组件
ProductDisplay和SupplierDisplay组件负责决定是否为特定的数据类型显示表格或编辑器。由于示例应用中增加了一些功能,这些组件之间的差异已经减少,URL 路由的引入意味着单个组件可以轻松处理两种类型数据的内容选择。我在src/routing文件夹中添加了一个名为RoutedDisplay.js的文件,并用它来定义清单 22-19 中所示的组件。
import React, { Component } from "react";
import { ProductTable } from "../ProductTable"
import { ProductEditor } from "../ProductEditor";
import { EditorConnector } from "../store/EditorConnector";
import { PRODUCTS } from "../store/dataTypes";
import { TableConnector } from "../store/TableConnector";
import { Link } from "react-router-dom";
import { SupplierEditor } from "../SupplierEditor";
import { SupplierTable } from "../SupplierTable";
export const RoutedDisplay = (dataType) => {
const ConnectedEditor = EditorConnector(dataType, dataType === PRODUCTS
? ProductEditor: SupplierEditor);
const ConnectedTable = TableConnector(dataType, dataType === PRODUCTS
? ProductTable : SupplierTable);
return class extends Component {
render() {
const modeParam = this.props.match.params.mode;
if (modeParam === "edit" || modeParam === "create") {
return <ConnectedEditor key={ this.props.match.params.id || -1 } />
} else {
return <div className="m-2">
<ConnectedTable />
<div className="text-center">
<Link to={`/${dataType}/create`}
className="btn btn-primary m-1">
Create
</Link>
</div>
</div>
}
}
}
}
Listing 22-19The Contents of the RoutedDisplay.js File in the src/routing Folder
该组件执行与ProductDisplay和SupplierDisplay组件相同的任务,但是接收它负责的数据类型作为参数,这允许创建EditorConnector和TableConnector组件。
更新连接的编辑器组件
EditorConnector组件负责创建一个连接到 Redux 数据存储的ProductEditor或SupplierEditor。在清单 22-20 中,我使用了withRouter函数来创建一个组件,该组件提供了路由数据,但仍然保持与数据存储的连接。
import { connect } from "react-redux";
//import { endEditing } from "./stateActions";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";
import { saveAndEndEditing } from "./multiActionCreators";
import { withRouter } from "react-router-dom";
export const EditorConnector = (dataType, presentationComponent) => {
const mapStateToProps = (storeData, ownProps) => {
const mode = ownProps.match.params.mode;
const id = Number(ownProps.match.params.id);
return {
editing: mode === "edit" || mode === "create",
product: (storeData.modelData[PRODUCTS].find(p => p.id === id)) || {},
supplier:(storeData.modelData[SUPPLIERS].find(s => s.id === id)) || {}
}
}
const mapDispatchToProps = {
//cancelCallback: endEditing,
saveCallback: (data) => saveAndEndEditing(data, dataType)
}
const mergeProps = (dataProps, functionProps, ownProps) => {
let routedDispatchers = {
cancelCallback: () => ownProps.history.push(`/${dataType}`),
saveCallback: (data) => {
functionProps.saveCallback(data);
ownProps.history.push(`/${dataType}`);
}
}
return Object.assign({}, dataProps, routedDispatchers, ownProps);
}
return withRouter(connect(mapStateToProps,
mapDispatchToProps, mergeProps)(presentationComponent));
}
Listing 22-20Using Routing in the EditorConnector.js File in the src/store Folder
组件不再使用数据存储来判断用户是在编辑还是在创建一个对象,而是从 URL 获取该信息,以及编辑对象时的id值。
小费
注意,我使用Number来解析id URL 参数,它以字符串的形式出现。我需要id值是一个数字,以便定位对象。
我已经使用了第二十章中描述的合并属性的能力,来围绕数据存储动作创建器创建包装器,以便数据被保存到存储中,然后history对象被用于导航。不再需要取消动作,并且可以通过导航离开当前位置来直接处理。
避免被阻止的更新
withRouter和connect函数都使用shouldComponentUpdate方法产生试图最小化更新的组件,这将在第十三章中描述。当withRouter和connect函数一起使用时,结果可能是一个组件并不总是更新,因为 React-Router 和 React-Redux 包对 props 执行简单的比较,并没有意识到已经发生了变化。要避免这个问题,请简化 props 结构,使更改更容易被检测到。
更新连接的表组件
必须在将显示对象的表连接到数据存储的组件上执行相同的过程,如清单 22-21 所示。
import { connect } from "react-redux";
//import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";
import { withRouter } from "react-router-dom";
export const TableConnector = (dataType, presentationComponent) => {
const mapStateToProps = (storeData, ownProps) => {
if (dataType === PRODUCTS) {
return { products: storeData.modelData[PRODUCTS] };
} else {
return {
suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
...supp,
products: supp.products.map(id =>
storeData.modelData[PRODUCTS]
.find(p => p.id === Number(id)) || id)
.map(val => val.name || val)
}))
}
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
if (dataType === PRODUCTS) {
return {
//editCallback: (...args) => dispatch(startEditingProduct(...args)),
deleteCallback: (...args) => dispatch(deleteProduct(...args))
}
} else {
return {
//editCallback: (...args) => dispatch(startEditingSupplier(...args)),
deleteCallback: (...args) => dispatch(deleteSupplier(...args))
}
}
}
const mergeProps = (dataProps, functionProps, ownProps) => {
let routedDispatchers = {
editCallback: (target) => {
ownProps.history.push(`/${dataType}/edit/${target.id}`);
},
deleteCallback: functionProps.deleteCallback
}
return Object.assign({}, dataProps, routedDispatchers, ownProps);
}
return withRouter(connect(mapStateToProps,
mapDispatchToProps, mergeProps)(presentationComponent));
}
Listing 22-21Using Routing in the TableConnector.js File in the src/store Folder
我再次使用了withRouter和connect函数来生成一个可以访问路由数据和数据存储的组件。通过导航到指示数据类型和id值的 URL 来处理编辑功能。删除数据是完全由数据存储处理的任务,不需要导航。
完成路由配置
最后一步是更新路由配置,以支持表 22-8 中定义的 URL。在清单 22-22 中,我更新了Selector组件,使其在render函数中应用RoutedDisplay组件。(为了简洁起见,我还删除了导航提示组件和代码。)
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
//import { CustomPrompt } from "./routing/CustomPrompt";
import { RoutedDisplay } from "./routing/RoutedDisplay";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 22-22Changing the Routing Configuration in the Selector.js File in the src Folder
父组件提供的子组件不再是组件,它们的存在只是为了给Selector提供适当的值,以便它可以设置Route组件。在清单 22-23 中,我在App组件中反映了这一变化,它现在使用一个定制的 HTML 元素来配置Selector,而不是直接使用特定于数据的组件。
import React, { Component } from "react";
import { Provider } from "react-redux";
import dataStore from "./store";
import { Selector } from "./Selector";
// import { ProductDisplay } from "./ProductDisplay";
// import { SupplierDisplay } from "./SupplierDisplay";
import { PRODUCTS, SUPPLIERS } from "./store/dataTypes";
export default class App extends Component {
render() {
return <Provider store={ dataStore }>
<Selector>
<data name="Products" datatype={ PRODUCTS } />
<data name="Suppliers" datatype ={ SUPPLIERS } />
</Selector>
</Provider>
}
}
Listing 22-23Completing the Routing Configuration in the App.js File in the src Folder
结果是数据存储不再用于组件之间的协调,现在完全通过 URL 来处理,如图 22-11 所示。
图 22-11
使用 URL 路由来协调组件
摘要
在本章中,我向您展示了如何使用 React-Router 包提供的高级特性。我演示了如何创建能够识别路由系统的组件,如何使用 URL 参数为组件提供对当前路由数据的轻松访问,以及如何以编程方式使用路由功能。我还演示了组件如何参与路由系统,同时还连接到 Redux,允许通过 URL 处理状态数据,而应用的模型数据由数据存储管理。在下一章,我将向您展示如何使用 RESTful web 服务。