三、使用链接和导航链接组件导航到路由

React Router 提供<Link><NavLink>组件,允许您导航到应用中定义的不同路由。这些导航组件可以看作是页面上的锚链接,允许您导航到站点中的其他页面。在传统网站中,当您使用锚链接浏览应用时,会导致页面刷新,页面中的所有组件都会重新呈现。使用<Link><NavLink>创建的导航链接不会导致页面刷新,只有使用<Route>定义并匹配 URL 路径的页面特定部分才会更新。

<Route>组件类似,导航组件<Link><NavLink>是 React 组件,允许您以声明方式定义导航链接。

在本章中,我们将了解用于导航到应用中定义的路由的各种选项。这包括:

  • <Link>组件及其道具
  • <NavLink>组件及其道具
  • 使用match道具导航到嵌套路由
  • 使用history以编程方式导航到路由
  • 使用高阶组件withRouter
  • 使用<Prompt>组件防止路由转换

组件

<Link>组件用于导航到使用<Route>组件定义的<indexentry content=" component:about">现有路由。要导航到路由,请将路由中使用的路径名指定为to属性的值:

import { Link } from 'react-router-dom';

class App extends Component {
    render() {
        return (
            <div class="container">
                <nav>
                    <Link to="/">Home</Link>
                    <Link to="/dashboard">Dashboard</Link>
                </nav>
                <Route
                    path="/"
                    component={HomeComponent}
                    exact 
                />
                <Route
                    path="/dashboard"
                    component={DashboardComponent} 
                />
            </div>
        );
    }
}

请注意,to道具的值与<Route>中分配给path道具的值相同。该页面现在呈现两个链接:

当您单击主页时,您将看到主页路由内的文本显示,当您单击仪表板时,您将被导航到路由,其path道具设置为/dashboard

当您使用<Link>导航到路由时,会调用history.push(),这会在历史堆栈中添加一个条目。因此,当您单击浏览器的“后退”按钮时,将导航到您正在访问的上一条路由(主路由)。如前一章所述,React Router 使用history库来维护应用的状态,因为用户在应用旅程中通过各种路由。

<Link>组件还有另外两个道具——replaceinnerRef

更换支柱

<Link>中的replace道具调用history.replace(),用to道具中提到的新路径名替换历史堆栈中的当前条目:

<Link to="/dashboard" replace>Dashboard</Link>

例如,如果您在路径/home处访问页面,访问前面的链接会将历史堆栈中的当前条目替换为/dashboard,这基本上将条目/home替换为/dashboard

内参照道具

React 提供ref以获取对呈现 DOM 元素的引用。然后可以使用该参考(ref来执行常规流之外的某些操作,例如聚焦于输入元素、媒体播放等。<Link>是一个复合组件,它在 DOM 上呈现锚元素

前面代码片段中提到的<Link>组件转换为锚元素,如下所示:

..
<nav>
    <a href="/">Home</a>
    <a href="/dashboard">Dashboard</a>
</nav>
..

为了获得对该渲染锚元素的引用,将道具innerRef添加到<Link>

<nav>
    <Link 
        to="/"
        innerRef={this.refCallback}>
        Home
    </Link>
    <Link
        to="/dashboard"
        innerRef={this.refCallback}>
        Dashboard
    </Link>
</nav>

innerRefprop 接受回调函数作为其值;这里,函数refCallback被指定为innerRef属性的值。refCallback获取对<Link>组件内部元素的引用:

refCallback(node) {
    node.onmouseover = () => {
        node.focus();
    }
}

<Link>组件挂载时,调用回调函数refCallback。从前面的代码片段中,我们可以看到两个<Link>组件呈现的锚元素都添加了mouseover处理程序。当用户将鼠标悬停在链接上时,相应的锚点将获得焦点。

用物体支撑

to道具可以是字符串或对象。对象可以包含以下属性:

  • pathname:要导航到的路径
  • search:路径的查询参数,以字符串值表示
  • hash:要添加到 URL 的哈希字符串
  • state:包含呈现组件可以使用的状态信息的对象

使用这些参数,我们添加一个<Link>组件:

<Link
    to={{
        pathname: '/user',
        search: '?id=1',
        hash: '#hash',
 state: { isAdmin: true }
    }}>
    User
</Link>

前面的代码转换为以下内容:

<a href="/user?id=1#hash">User</a>

state信息不包含在 URL 路径中;但是,它可用于作为<Route>匹配结果渲染的组件:

<Route
    path="/user"
    render={({ location }) => {
        const { pathname, search, hash, state } = location;
        return (
            <div>
                Inside User route
                <h5>Pathname: {pathname}</h5>
                <h5>Search: {search}</h5>
                <h5>Hash: {hash}</h5>
                <h5>State: {'{'} 
                    {Object.keys(state).map((element, index) => {
                        return (
                            <span key={index}>
                                {element}: {state[element].toString()}
                            </span>
                        )
                    })} 
                    {'}'}
                </h5>
            </div>
        );
    }}
/>

location对象包含所有先前定义的参数,包括state对象。

当用户在应用中导航时,state对象可用于存储数据,并将该数据提供给<Route>匹配的下一个渲染组件。

组件

<NavLink>组件与<Link>组件类似,只是可以指定几个道具来帮助您有条件地向渲染元素添加样式属性。它接受与<Link>组件(toreplaceinnerRef相同的一组用于导航到路由的道具,并包括用于设置所选路由样式的道具。

让我们来看看这些帮助你风格的组件。

activeClassName 道具

默认情况下,类名active应用于激活的<NavLink>组件。例如,当点击<NavLink>并呈现相应的路由时,所选<NavLink>的类名设置为active。要更改此类名,请在<NavLink>组件上指定activeClassName属性,并将其值设置为要应用的 CSS 类名:

<nav>
    <NavLink to="/">Home</NavLink>
    <NavLink
        to="/dashboard"
        activeClassName="selectedLink">
        Dashboard
    </NavLink>
</nav>

下一步是在应用的 CSS 文件中指定 CSS 类selectedLink的样式。注意,第一个<NavLink>没有指定activeClassName道具。在这种情况下,当点击<NavLink>时,添加active类:

<nav>
    <a class="active" aria-current="page" href="/">Home</a>
    <a aria-current="page" href="/dashboard">Dashboard</a>
</nav>

但是,当点击第二个<NavLink>时,应用selectedLink类:

<nav>
    <a aria-current="page" href="/">Home</a>
    <a class="selectedLink" aria-current="page" href="/dashboard">Dashboard</a>
</nav>

活动式道具

activeStyle道具也用于设置所选<NavLink>的样式。但是,在选择<NavLink>时,可以内联提供 CSS 样式属性,而不是提供要应用的类:

<NavLink
    to="/user"
    activeStyle={{
        background: 'red',
        color: 'white'
    }}>
    User
</NavLink>

精确道具

当您使用to道具/dashboard点击<NavLink>时,active类(或activeStyle道具中指定的内联样式)将应用于页面中的两个<NavLink>组件。与<Route>组件类似,/dashboard中的/匹配to属性中指定的路径,因此活动类应用于<NavLink>组件。

在这种情况下,只有当路径匹配浏览器的 URL 时,exact道具才能用于应用active类或activeStyle

<NavLink
    to="/"
    exact>
    Home
</NavLink>
<NavLink
    to="/dashboard"
    activeClassName="selectedLink">
    Dashboard
</NavLink>

严格道具

<NavLink>组件还支持strict道具,可用于匹配to道具中指定的尾部斜线:

<NavLink
    to="/dashboard/"
    activeClassName="selectedLink"
 strict>
    Dashboard
</NavLink>

这里,仅当浏览器的 URL 路径与路径/dashboard/匹配时,类selectedLink才应用于<NavLink>组件—例如,当 URL 中存在尾随斜杠时。

主动支柱

isActive道具用于确定<NavLink>组件是否应应用active类(或activeStyle道具中指定的内联样式)。指定为isActive属性值的函数应返回布尔值:

<NavLink
    to={{
        pathname: '/user',
        search: '?id=1',
        hash: '#hash',
        state: { isAdmin: true }
    }}
    activeStyle={{
        background: 'red',
        color: 'white'
    }}
    isActive={(match, location) => {
        if (!match) {
            return false;
        }
        const searchParams = new URLSearchParams(location.search);
        return match.isExact && searchParams.has('id');
    }}>
    User
</NavLink>

从前面的示例中,函数接受两个参数-matchlocationactiveStyle道具中定义的样式仅在match.isExact && searchParams.has('id')条件为 true 时应用,因此,仅当matchexact且 URL 有查询参数id时应用。

当浏览器的 URL 为/user时,显示用<Route>定义的对应路由。但是,<NavLink>组件将具有默认样式,而不是activeStyle属性中提到的样式,因为缺少查询参数id

定位道具

<NavLink>中的isActive函数接收浏览器的历史记录location并确定浏览器的location.pathname是否与给定条件匹配。要提供不同的位置,请包括location道具:

<NavLink
    to="/user"
    activeStyle={{
        background: 'red',
        color: 'white'
    }}
    location={{
        search: '?id=2',
    }}
    isActive={(match, location) => {
        if (!match) {
            return false;
        }
        const searchParams = new URLSearchParams(location.search);
        return match.isExact && searchParams.has('id');
    }}>
    User
</NavLink>

注意,to道具没有指定search参数;但是,location属性包含它,因此,当浏览器的位置为/user时,isActive函数返回 true,因为搜索参数包含id属性。

导航到嵌套路由

在上一章中,我们看到了如何使用渲染组件接收的match属性创建嵌套路由。match.url属性包含与<Route>组件路径匹配的浏览器 URL 路径。类似地,<Link><NavLink>组件可用于创建导航链接以访问这些嵌套路由:

<nav>
    <Link
        to={`${match.url}/pictures`}>
        Pictures
    </Link>
    <NavLink
        to={`${match.url}/books`}
        activeStyle={{
            background: 'orange'
        }}>
     Books
    </NavLink>
</nav>

在前面的代码片段中,<Link><NavLink>组件利用match.url获取对当前渲染路由的引用,并添加导航到嵌套路由所需的其他路径值

使用历史记录以编程方式导航到路由

<Link><NavLink>组件在页面上呈现锚定链接,允许您从当前路由导航到新路由。但是,在许多情况下,当事件发生时,应该通过编程方式将用户导航到新路由。例如,在登录表单中单击 Submit 按钮时,应将用户导航到新路由。在以下情况下,可以使用渲染组件可用的history对象:

export const DashboardComponent = (props) => (    
    <div className="dashboard">
        Inside Dashboard route
        <button onClick={() => props.history.push('/user')}>
            User
        </button>
    </div>
);

这里,DashboardComponent接收props作为其参数,其中包含history对象。onClick处理程序使用路径名/user调用props.history.push。此调用向历史堆栈添加一个条目,并使用路径/user将用户导航到<Route>history对象也可以用来替换历史堆栈中的当前条目,方法是使用history.replace代替history.push

使用 withRouter 高阶组件

使用<Route>匹配渲染的组件可以使用history对象。在前面的示例中,DashboardComponent是导航到路径/dashboard的结果。渲染组件接收到包含history对象的props(以及matchlocationstaticContext)。如果页面上呈现的组件不是路由导航的结果,history对象对该组件不可用。

考虑一个包含在 To1 T1:

class FooterComponent extends Component {
    render() {
        return (
            <footer>
                In Footer
                <div>
                    <button 
                        onClick={() => 
                        this.props.history.push('/user')}>
                        User
                    </button>
                    <button 
                        onClick={() => 
                         this.props.history.push('/stocks')}>
                        Stocks
                    </button>        
                </div>
            </footer>
        )
    }
}

FooterComponent有两个按钮,它们调用history.push来导航到应用中的一个页面。点击按钮,抛出错误TypeError: Cannot read property 'push' of undefined。抛出此错误是因为props属性中的history对象不可用,因为导航不会呈现组件。要避免这种情况,请使用高阶组件withRouter

export const Footer = withRouter(FooterComponent);

在这里,react-router包中定义的withRouter函数接受 React 组件作为其参数,并对其进行扩充,以提供props属性上的必要对象—historymatchlocationstaticContext

关于 HOC 的 React 文档:高阶组件是接受组件并返回新组件的函数虽然组件将道具转换为 UI,但高阶组件将一个组件转换为另一个组件。

withRouterHOC 中封装的组件可以使用<Route><Link><NavLink>定义路由和导航链接:

import { withRouter } from 'react-router';

class FooterComponent extends Component {
    render() {
        return (
            <footer>
                In Footer
                <div>
                <button onClick={() => 
                 this.props.history.push('/user')}>User</button>
                <button onClick={() => 
                  this.props.history.push('/stocks')}>Stocks</button>
                <Link to='subroute'>User</Link>
                <Route
                    path='/subroute'
                    render={() => {
                        return <span>Inside Footer Subroute</span>
                    }} />
                </div>
            </footer >
        )
    }
}

export const Footer = withRouter(FooterComponent);

在前面的代码片段中,withRouterHOC 使组件能够获取路由的上下文,从而使LinkNavLinkRoute等组件可用

使用防止过渡

当您在应用中的页面之间导航时,会立即转换到新路由。但是,在某些情况下,您希望根据应用的状态阻止此转换。一个常见的例子是,用户在表单字段中输入数据,并花费数分钟(或数小时)填充表单数据。如果用户意外单击导航链接,则表单中输入的所有数据都将丢失。应将此路由导航通知用户,以便用户有机会保存输入表单的数据。

传统网站跟踪表单的状态,并在用户试图从包含尚未提交到服务器的表单的页面导航时显示确认消息。在这些场景中,将显示一个确认对话框,其中包含两个选项,确定和取消;前一个选项允许用户过渡到下一步,后一个选项取消过渡:

React Router 提供<Prompt>组件,可用于显示确认对话框,防止用户意外离开当前<Route>

import { Prompt } from 'react-router-dom'

<Prompt
    when={this.state.isFormSubmitted}
    message='Are you sure?'
/>

这里的<Prompt>组件接受两个道具—whenmessage。从前面的代码片段可以看出,如果state属性isFormSubmitted的值为true,并且当用户尝试导航离开当前路由时,将向用户显示一个带有消息“您确定吗?”的确认对话框

Please note, the <Prompt> message is shown only when the user tries to navigate away from the current route. No message is shown when the state property is set to true.

分配给when道具的值可以是任何布尔变量或布尔值。在 React 中,组件的state用作视图模型,以维护渲染组件的状态。在这样的情况下,state属性非常理想,可以确定当用户试图离开当前路由时是否应显示<Prompt>

message属性的值可以是字符串或函数:

<Prompt
    when={this.state.isFormSubmitted}
    message={(location) => 'Are you sure you want to navigate to ${location.pathname}?'} />

该函数接收location参数,该参数包括用户试图导航到的路由的位置信息

Similar to other components in the 'react-router-dom' package, the <Prompt> component should be used inside a rendered <Route>. When you try to use a <Prompt> without it having the context of the current route, the message. You should not use <Prompt> outside a <Router> is shown.

当用户试图离开当前路由时(无论应用的state如何),也可以通过不包括when道具来显示消息:

<Prompt message='Are you sure?' />

通常情况下,when道具包含在<Prompt>中,分配给when道具的值用于确定是否应显示确认对话框。

When you're trying these examples, ensure that you have only one <Prompt> for the given <Route>, else the library will report the warning A history supports only one prompt at a time

总结

在本章中,我们了解了如何使用<Link><NavLink>导航组件导航到应用中定义的各种路由。这些组件在页面中呈现anchor链接,当用户点击这些链接时,页面的各个部分都会更新,而不是完全重新加载页面,从而提供清晰的用户体验。<Link>组件接受道具toreplaceinnerRef

<NavLink>组件与<Link>组件类似,它接受<Link>组件使用的所有道具。除了添加页面链接外,<NavLink>组件还接受多个道具—activeClassNameactiveStyleexactstrictisActive

为了创建到嵌套路由的链接,<Link><NavLink>组件可以使用to属性中的前缀match.url。此外,您还可以在事件处理程序函数中使用history.pushhistory.replace以编程方式导航。道具-historymatchlocationstaticContext-可通过withRouter高阶组件提供给路由上下文之外呈现的组件。'react-router-dom'软件包包括一个<Prompt>组件,当用户试图通过意外单击导航链接导航到路由时,该组件可用于显示确认对话框。<Prompt>组件接受whenmessage属性,并且基于分配给when属性的布尔值,message属性中指定的消息将显示给用户

第 4 章中,我们将使用重定向和切换组件来了解<Redirect><Switch>组件。此外,我们还将了解如何使用这些组件来保护路由,并在页面中没有任何路由与请求的 URL 匹配时显示未找到的页面。