愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

ReactRouter4学习笔记(二):API

一、ReactRouter的三种路由

1、BrowserRouter

<BrowserRouter>使用HTML5的history API(pushStatereplacestatepopstate事件)来同步URL和UI,用法:

import { BrowserRouter } from 'react-router-dom'
<BrowserRouter
    basename={optionalString}
    forceRefresh={optionalBool}
    getUserConfirmation={optionalFunc}
    keyLength={optionalNumber}
>
    <App />
</BrowserRouter>

属性介绍:

  • basename: string
    基准URL,适用于当应用置于服务器上子目录的情况下。basename的属性值应以/开头,但无需/结尾,如:
<BrowserRouter basename="/calendar" />
<Link to="/today" /> // 渲染为 <a href="/calendar/today">
  • getUserConfirmation: func
    可以传入一个函数,用来确定导航前的用户确认行为,默认使用window.confirm,如:
const getConfirmation = (message, callback) => {
    const allowTransition = window.confirm(message)
    callback(allowTransition)
}

<BrowserRouter getUserConfirmation={getConfirmation} />
  • forceRefresh: bool
    当设为true时,每次跳转都会刷新页面。一般只在浏览器不支持HTML history API的情况下使用,如:
const supportsHistory = 'pushState' in window.history
<BrowserRouter forceRefresh={!supportsHistory} />
  • keyLength: number
    设置location.key的长度,默认为6位:
<BrowserRouter keyLength={12} />
  • children: node
    子元素里只能渲染单一的元素

2、HashRouter

使用URL中的hash部分(即window.location.hash)来保持UI和URL同步:

import { HashRouter } from 'react-router-dom'
<HashRouter>
    <App/>
</HashRouter>

特别注意: HashHistory不支持location.keylocation.state(虽然先前版本中官方曾尝试实现这个行为但是会带来一些无法解决的边界情况),所以任何需要这两个属性的代码或者插件将不能起作用。由于HashHistory这种技术的主要目的是支持老式浏览器,所以官方更推荐的是做法是对服务器进行配置,然后采用<BrowserHistory>代替
属性介绍:
<HashHistory>同样支持basename属性和getUserConfirmation属性,其他的属性有:

  • hashType: string
    设置呈现在window.location.hash里的编码类型,可以取的值有:
    • slash 默认值,创建类似于#/#/sunshine/lollipops这种hash
    • noslash 创建类似于##sunshine/lollipops这种hash
    • hashbang 创建AJAX可爬(ajax crawlable)的hash(不过这种技术已经被Google给废弃了),类似于#!#!/sunshine/lollipops

3、MemoryRouter

在内存中记录history的路由组件(这种路由不会读取或者写入地址栏),适合用于测试或者非浏览器环境(如ReactNative):

import { MemoryRouter } from 'react-router'
<MemoryRouter>
    <App/>
</MemoryRouter>

属性介绍:

  • initialEntries: array
    记录历史栈的一个数组。可以是完全展开的location对象({ pathname, search, hash, state }),也可以是简单的URL字符串,如:
<MemoryRouter
    initialEntries={[
        '/one',
        '/two',
        { pathname: '/three' }
    ]}
    initialIndex={1}
>
    <App/>
</MemoryRouter>
  • initialIndex: number
    指定initialEntries数组里初始的location索引
  • getUserConfirmation: func
    和之前两种路由里的概念一致。这里需要注意的是:直接结合<Prompt>使用<MemoryRouter>时必须设置这个选项
  • keyLength: number


二、导航跳转

1、Link

为应用提供声明式、可访问的导航的一个组件:

import { Link } from 'react-router-dom'
<Link to="/about">About</Link>

参数介绍:

  • to: string
    要跳转到的pathname或者location
  • to: object
    用法如:
<Link to={{
    pathname: '/courses',
    search: '?sort=name',
    hash: '#the-hash',
    state: { fromDashboard: true }
}} />
  • replace: bool
    当这个值为true时,会替换掉history栈里当前的history,而不是在栈里新增一条history,如:
<Link to="/courses" replace />

2、NavLink

<Link>组件的特殊版本,当当前URL匹配时,会在元素上增加样式相关的属性(从而达到高亮标记的效果),如:

import { NavLink } from 'react-router-dom'
<NavLink to="/about">About</NavLink>

参数介绍:

  • activeClassName: string
    规定处于激活状态时的类名,默认值是active,会自动和className属性合并:
<NavLink to="/faq" activeClassName="selected">FAQs</NavLink>
  • activeStyle: object
    规定激活状态下应用到元素上的样式:
<NavLink to="/faq" activeStyle={{
    fontWeight: 'bold',
    color: 'red'
}}>FAQs</NavLink>
  • exact: bool
    设为true时,只有当location完全匹配的时候才会添加类名或者样式:
<NavLink exact to="/profile">Profile</NavLink>
  • strict: bool
    当设为true时,location中pathname末尾的/就会在匹配URL的时候被考虑:
<NavLink strict to="/events/">Events</NavLink>
  • isActive: func
    用来为link添加判断激活状态额外逻辑的函数,如:
// 只有当事件ID为奇数时才可能为激活状态
const oddEvent = (match, location) => {
    if (!match) {
        return false
    }
    const eventID = parseInt(match.params.eventID)
    return !isNaN(eventID) && eventID % 2 === 1
}
<NavLink to="/events/123" isActive={oddEvent}>Event 123</NavLink>
  • location: object
    isActive函数里用来比较用的值(即传入的第二个参数),通常情况下取值为当前浏览器的URL。如果要和不同的location比较,就可以用这个属性来传值。


三、Router、Route与Switch

1、Router

通用的基础路由组件。通常在应用里会采用派生的路由代替,有:

  • <BrowserRouter>
  • <HashRouter>
  • <MemoryRouter>
  • <NativeRouter>
  • <StaticRouter>

最常用的直接采用<Router>的场景是用状态管理库(Redux、Mobx)来同步一个自定义的history。不过应当注意的是:这不是说必须结合状态管理库使用ReactRouter,只是在深度集成的时候需要用到:

import { Router } from 'react-router'
import createBrowserHistory from 'history/createBrowserHistory'

const history = createBrowserHistory()
<Router history={history}>
    <App />
</Router>

参数介绍:

  • history: object
    用来导航用的history对象:

2、Route

<Route>组件也许是ReactRouter里最需要好好理解和使用的最重要的一个组件。它的基本职责是在location匹配路由的path参数值时渲染特定的UI:

import { BrowserRouter as Router, Route } from 'react-router-dom'
<Router>
    <div>
        <Route exact path="/" component={Home} />
        <Route path="/news" component={NewsFeed} />
    </div>
</Router>

当location是/时,UI将会渲染为:

<div>
    <Home />
    <!-- react-empty: 2 -->
</div>

而如果当UI是/news时,UI将会渲染为:

<div>
    <!-- react-empty: 1 -->
    <NewsFeed />
</div>

react-empty注释,只是React的null渲染的实现。但是这种做法是有益的,从技术上而言一个Route应该总是被渲染出来即使结果是渲染null,而只要应用的location和Route的path匹配,那么对应的组件就会得到渲染。

2-1、路由渲染方法

渲染一个路由有三种方式:

  • <Route component>
  • <Route render>
  • <Route children>

每一种方式在不同的情景下都是有用的,但是在<Route>里只能同时用一个属性,下面将会介绍为什么需要提供这3种方式,不过大部分情况下用的是component
首先需要知道的是,这三种方式都会得到matchlocationhistory这三个路由属性。
以下是对这三种方式的说明:

  • component方式
    表明location匹配时要渲染的React组件,组件可以使用路由属性来进行渲染:
<Route path="/user/:username" component={User} />

const User = ({ match }) => {
    return <h1>Hello, {match.params.username}!</h1>
}

当使用这种方式时,路由系统会使用React.createElement来从给定的组件里创建一个新的React元素。这意味着如果传给component属性的是一个内联的函数,那么每次渲染时就都会 创建一个新的组件。这就会导致现有的组件卸载,然后挂载新的组件。而非更新已有的组件。所以要使用内联函数来进行内联渲染时,可以使用render方式或者children方式

  • render: func方式
    这种方式将方便于内联渲染,并且不会有上述的重新挂载问题。我们可以传入一个函数,那么当location匹配的时候这个函数就会被调用,从而不会进行新的React元素创建过程。render属性接收和component属性一致的路由属性:
<Route path="/home" render={() => <div>Home</div>} />

const FadingRoute = ({ component: Component, ...rest }) => (
    <Route {...rest} render={props => (
        <FadeIn>
            <Component {...props} />
        </FadeIn>
    )} />
)

<FadingRoute path="/cool" component={Something} />
  • children: func方式
    有时候,无论path是否成功匹配都需要进行渲染。那么这种情况下,可以使用children属性方式,这种方式无论匹配是否成功都会调用对应的函数。
    children渲染属性接收和componentrender方式一样的属性,不过当路由不匹配的时候,match的值是null。所以采用这种方式的好处是,我们可以灵活地动态调整UI,无论路由是否匹配。举例如:
    路由匹配时添加激活状态的类名:
<ul>
    <ListItemLink to="/somewhere" />
    <ListItemLink to="/somewhere-else" />
</ul>

const ListItemLink = ({to, ...rest}) => (
    <Route path={to} children={({ match }) => (
        <li className={match ? 'active' : ''}>
            <Link to={to} {...rest} />
        </li>
    )}/>
)

对于动画而言,这种方式也是有用的:

<Route children={({ match, ...rest }) => (
    // 因为动画会一直渲染,所以可以使用生命周期来让子元素动画进出
    <Animate>
        {match && <Something {...rest}/>}
    </Animate>
)}

警告: 由于优先级上:component > render > children,所以在<Route>里最多只能选用一种。

2-2、参数说明

  • path: string
    ReactRouter的path采用path-to-regexp解析,所以这里的path值要能够被库识别才是合法的,此外,不带path属性的Route总是匹配的
  • exact: bool
    当设为true时,会精确匹配path和location.pathname,如:
+---------+-------------------+------------+------------+
| path    | location.pathname | exact      | 是否匹配?  |
+---------+-------------------+------------+------------+
| /one    | /one/two          | true       | 否         |
+---------+-------------------+------------+------------+
| /one    | /one/two          | false      | 是         |
+---------+-------------------+------------+------------+
  • strict: bool
    当设为true时,在匹配时,会将尾/纳入考虑,如:
+---------+-------------------+------------+
| path    | location.pathname | 是否匹配?  |
+---------+-------------------+------------+
| /one/   | /one              | 否         |
+---------+-------------------+------------+
| /one/   | /one/             | 是         |
+---------+-------------------+------------+
| /one/   | /one/two          | 是         |
+---------+-------------------+------------+

警告: strict可以用来强制location.pathname没有尾/,但如果要精确匹配且不能有尾/,可以同时使用exact,如:

+---------+-------------------+------------+
| path    | location.pathname | 是否匹配?  |
+---------+-------------------+------------+
| /one/   | /one              | 是         |
+---------+-------------------+------------+
| /one/   | /one/             | 否         |
+---------+-------------------+------------+
| /one/   | /one/two          | 否         |
+---------+-------------------+------------+
  • location: object
    <Route>元素会尝试用它的path去匹配当前的location。不过,也可以传入一个不同的pathname来进行匹配。当我们不想用当前history里的location进行匹配时,这个参数就很有用。
    如果<Route>元素包含在<Switch>里,并且匹配了传给<Switch>的location(或当前的location),那么传给<Route>的location属性会被<Switch>所采用的那个location给覆盖

3、Switch

渲染子元素里第一个匹配的<Route>或者<Redirect>,那么,和只使用一堆<Route>不同的是什么?
<Switch>独一无二的是它只渲染一个路由。而反过来,每个匹配location的<Route>都会被得到渲染,参考如下代码:

<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />

如果URL是/about,那么<About><User><NoMatch>都会渲染,因为它们的path都得到了匹配。而这种设计是有意的,因为这使得我们可以用多种方式来用<Route>组成我们的应用,如侧边栏、面包屑、引导Tab等。
不过有时候,我们只想要拾取一个<Route>来渲染,就比如URL处于/about时就不希望它匹配/:user(或者展示404页面),如:

import { Switch, Route } from 'react-router'
<Switch>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
    <Route path="/:user" component={User} />
    <Route component={NoMatch} />
</Switch>

现在,如果URL是/about,那么<Switch>就会开始寻找匹配的<Route><Route path="/about"/>将会匹配,从而<Switch>就会停止匹配然后渲染出<About>组件。相似地,如果URL处于/michael,那么<User>组件就会渲染。
对于动画过渡而言,这也是一种有用的方式,因为匹配的<Route>需要渲染在先前相同的位置:

<Fade>
    <Switch>
        {/* 这里只会有一个子节点 */}
        <Route/>
        <Route/>
    </Switch>
</Fade>

<Fade>
    <Route/>
    <Route/>
    {/* 
        这里一直都会有两个子节点,尽管其中一个可能为null,
        这会使得过渡效果起效果有点难处理
    */}
</Fade>

参数说明:

  • location: object 和之前其他地方里含义一致
  • children: node
    <Switch>组件的子元素都应该是<Router>或者<Redirect>,并且只有匹配当前URL的第一个子元素会被渲染。其中,<Route>元素使用path属性来进行匹配,而<Redirect>使用from属性进行匹配。而不带path属性的<Route>元素或者不带from属性的<Redirect>元素会总是匹配当前location
    所以当在<Switch>里使用<Redirect>组件时,它可以使用和<Route>一样的匹配属性:pathexactstrict,而from只是path属性的别名。
    当给<Switch>传入location属性时,会覆盖当前匹配的子元素中的location
<Switch>
    <Route exact path="/" component={Home} />
    <Route path="/users" component={Users} />
    <Redirect from="/accounts" to="/users" />
    <Route component={NoMatch} />
</Switch>


四、其他

1、Prompt

用来在从一个页面导航到另一个页面前提示用户的组件。适用于当应用进入某一状态时需要防止用户跳转走的情况(比如填完一半的表单):

import { Prompt } from 'react-router'
<Prompt when={formIsHalfFilledOut} message="确定离开?" />

参数介绍:

  • message: string
    页面跳转前要展示来提示给用户的信息
  • message: func
    在用户尝试导航到下一个location和action时会被调用,可以返回string来提示用户,或者返回true来允许这一过渡过程:
<Prompt message={location => (
    `确定前往${location.pathname}?`
)} />
  • when: bool
    设置是否需要进行提示:
<Prompt when={formIsHalfFilledOut} message="确定?" />

2、Redirect

进行跳转的一个组件,成功渲染时会进行跳转,并且新的location会覆盖当前location在历史栈中的位置,和服务端的跳转(HTTP 3XX)是类似的:

import { Route, Redirect } from 'react-router'
<Route exact path="/" render={() => (
    loggedIn ? (
        <Redirect to="/dashboard" />
    ): (
        <PublicHomePage />
    )
)} />

参数介绍:

  • to: string
    要跳转的URL
  • to: object
    要跳转的location信息:
<Redirect to={{
    pathname: '/login',
    search: '?utm=your+face',
    state: {
        referer: currentLocation
    }
}} />
  • push: bool
    当设为true时,重定向时会在历史栈里加入一个新的帧,而非替代当前帧
  • from: string
    重定向的源pathname,只能够用来在渲染<Switch>内的<Redirect>时匹配location,参见<Switch>中关于子节点的说明:
<Switch>
    <Redirect from="/old-path" to="/new-path" />
    <Route path="/new-path" component={Place} />
</Switch>


五、重要对象

1、context.router

ReactRouter使用context.router来促进<Router>之间及其子元素(<Route><Link><Prompt>)的通信
context.router是一个公用的API,因为context本身是一个实验性的API并且在将来的React版本中可能会发生变更,所以应该避免直接在组件中访问this.context.router。取而代之的,我们可以通过传给<Route>组件或者用withRouter包装的组件的属性访问context

2、history

本文档中的术语historyhistory object指的都是[history包](https://github.com/ReactTraining/history),即ReactRouter的两个主要依赖包之一。这个包提供了Javascript在不同的环境下的会话历史管理实现。

3、withRouter

可以通过withRouter这一高阶组件访问history object的属性和最近匹配的<Route>withRouter在每次路由变更时都重新渲染其包裹的组件,并传入和<Route>一致的渲染属性:{ match, location, history },如:

import React from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router'

// 以下是一个展示当前location中的pathname的简单组件
class ShowTheLocation extends React.Component {
    static propTypes = {
        match: PropTypes.object.isRequired,
        location: PropTypes.object.isRequired,
        history: PropTypes.object.isRequired
    }
    render() {
        const { match, location, history } = this.props
        return (
            <div>当前处于{location.path}</div>
        )
    }
}
// 创建一个被连接到路由的组件
const ShowTheLocationWithRouter = withRouter(ShowTheLocation)

重点:
如果使用withRouter来阻止被shouldComponentUpdate拦截的更新,那么用withRouter来包装实现了shouldComponentUpdate方法的组件是很重要的。如使用Redux时:

// shouldComponentUpdate能起到作用
withRouter(connect(...)(MyComponent))

// 不起作用
connect(...)(withRouter(MyComponent))

关于静态方法&静态属性:所有包装过的组件中的非React特定的静态方法、属性,都会自动地复制到连接后的组件里

3-1、Component.WrappedComponent

被包装的组件能够作为返回组件的静态属性WrappedComponent暴露出来,这在同构中可以被用来进行组件测试:

// MyComponent.js
export default withRouter(MyComponent)

// MyComponent.test.js
import MyComponent from './MyComponent'
render(<MyComponent.WrappedComponent location={{...}} ... />)

3-2、wrappedComponentRef: func

传给被包装组件,并作为ref属性的一个函数:

class Container extends React.Component {
    componentDidMount() {
        this.component.doSomething()
    }
    render() {
        return (
            <MyComponent wrappedComponentRef={c => this.component = c}/>
        )
    }
}