React Router(react-router&react-router-dom)相关整理

 2019年12月27日    2115     声明


React Router是用于React的声明式路由组件,可以与你的应用声明式的组合使用。其即可用于Web应用中(通过react-router-dom),也可以用于React Native中(通过react-router-native)。

相关说明:本文基于官方文档(v5.1.2)整理,因笔者为Web使用环境,所以本文暂时只涵盖核心Web两部分,Native部分并未涉及。

  1. React Router核心相关(Core)
  2. React Router Web相关

1. React Router核心相关(Core)

1.1 指南

1.1.1 设计思想(Philosophy)

React Router使用了称之为“动态路由”的设计思想,它与我们所熟知的“静态路由”完全不同。

静态路由

如果您使用过Rails、Express、Ember、Angular等,那么就使用了静态路由。在这些框架中,需要在进行任何渲染之前将路由声明为应用初始化的一部分。React Router pre-v4也是静态的(大部分是)。

以下是一个Express的路由配置:

// Express 路由配置
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();

在这个路由配置中,应该在启动应用监听之前声明路由。我们在客户端使用的路由也类似。 在Angular中,也需要先声明路路,然后在渲染之前将其导入顶级AppModule:

// Angular 路由配置
const appRoutes: Routes = [
  {
    path: "crisis-center",
    component: CrisisListComponent
  },
  {
    path: "hero/:id",
    component: HeroDetailComponent
  },
  {
    path: "heroes",
    component: HeroListComponent,
    data: { title: "Heroes List" }
  },
  {
    path: "",
    redirectTo: "/heroes",
    pathMatch: "full"
  },
  {
    path: "**",
    component: PageNotFoundComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(appRoutes)]
})
export class AppModule {}

Ember具有一个惯用的route.js文件,其会为被读取并导入到应用程序中。同样,这也是在你应用渲染之前发生的:

// Ember 路由:
Router.map(function() {
  this.route("about");
  this.route("contact");
  this.route("rentals", function() {
    this.route("show", { path: "/:rental_id" });
  });
});

export default Router;

以上这些示例中,虽然各框架提供的API不同,但它们都是“静态路由”模型。React Router也同样使用,直到v4。


动态路由

我们所说的动态路由,是指在应用渲染时发生的路由,而不是在运行的应用之外的配置或约定中进行。这意味着几乎所有内容都是React Router中的一个组件。以下是对该API的简短介绍,以了解其工作原理。

首先,在你的使用环境中获取一个Router组件,并将其放在在应用程序顶部:

// react-native
import { NativeRouter } from "react-router-native";

// react-dom (我们在这里使用)
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  el
);

接下来,获取Link组件以链接到新位置:

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
  </div>
);

最后,渲染一个Route以在用户访问/dashboard时显示一些UI:

const App = () => (
  <div>
    <nav>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
    <div>
      <Route path="/dashboard" component={Dashboard} />
    </div>
  </div>
);

这个Route将渲染<Dashboard {...props}/>,其中props是路由所需的内容,类似{ match, location, history }。如果用户不在/dashboard路径上,则Route将渲染null


嵌套路由

很多路由都有“嵌套路由”的概念。如果你使用了v4之前的React Router版本,那么你同样也会知道它是啥。当你从静态路由配置转移到动态渲染的路由时,那么应该如何“嵌套路由”?好吧,就跟嵌套div一样样的。

const App = () => (
  <BrowserRouter>
    {/* here's a div */}
    <div>
      {/* here's a Route */}
      <Route path="/tacos" component={Tacos} />
    </div>
  </BrowserRouter>
);

// 当 url 匹配到 `/tacos` 时,组件将会渲染
const Tacos = ({ match }) => (
  // 嵌套 div
  <div>
    {/* 这里是一个嵌套路由,
        match.url 会帮助我们建立相对路径 */}
    <Route path={match.url + "/carnitas"} component={Carnitas} />
  </div>
);

如上所示,路由(Route)仅是一个组件,就像div,也就可以像div一样嵌套使用。


响应路由

考虑一个到/invoices的用户导航。应用需要适应不同的屏幕尺寸,如果屏幕尺寸较小,只向他们显示发票清单和发票仪表板的链接。他们可以从那里进入更深层的导航:

小尺寸屏幕
url: /invoices

+----------------------+
|                      |
|      Dashboard       |
|                      |
+----------------------+
|                      |
|      Invoice 01      |
|                      |
+----------------------+
|                      |
|      Invoice 02      |
|                      |
+----------------------+
|                      |
|      Invoice 03      |
|                      |
+----------------------+
|                      |
|      Invoice 04      |
|                      |
+----------------------+

而在较大的屏幕上,我们可以显示一个主从视。其中导航在左侧,仪表板或特定发票信息在右侧:

大尺寸屏幕
url: /invoices/dashboard

+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |   Unpaid:             5   |
+----------------------+                           |
|                      |   Balance:   $53,543.00   |
|      Invoice 01      |                           |
|                      |   Past Due:           2   |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |                           |
|                      |   +-------------------+   |
+----------------------+   |                   |   |
|                      |   |  +    +     +     |   |
|      Invoice 03      |   |  | +  |     |     |   |
|                      |   |  | |  |  +  |  +  |   |
+----------------------+   |  | |  |  |  |  |  |   |
|                      |   +--+-+--+--+--+--+--+   |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

现在考虑两种尺寸的/invoices,它会是大尺寸屏幕的有效路径吗?我在右边应该放啥?

大尺寸屏幕
url: /invoices
+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 01      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |             ???           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 03      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

在大尺寸屏上,/invoices不是一个有效路径,但在小屏上是。这时,我们应该考虑使用大屏手机的人。他们可能会纵向查看/invoices,然后将手机转为横向。这时,又有足够的空间来显示主从界面了,所以你应该立即进行重定向。

React Router以前版本的静态路由并没有真正解决这个问题的方法。但是,当路由是动态的时,就可以声明性地组合此功能。如果开始考虑将路由做为UI,而不是静态配置,那么你的直觉将引导你编写以下代码:

const App = () => (
  <AppLayout>
    <Route path="/invoices" component={Invoices} />
  </AppLayout>
);

const Invoices = () => (
  <Layout>
    {/* nav 总是显示 */}
    <InvoicesNav />

    <Media query={PRETTY_SMALL}>
      {screenIsSmall =>
        screenIsSmall ? (
          // 小尺寸屏不重定向
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
          </Switch>
        ) : (
          // 大尺寸屏时重定向
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
            <Redirect from="/invoices" to="/invoices/dashboard" />
          </Switch>
        )
      }
    </Media>
  </Layout>
);

当用户将手机从纵向旋转为横向时,以上代码会自动将其重定向到仪表板。有效路径会根据用户手中移动设备的动态变化。

这仅是一个示例。我们可以讨论更多其他内容,但我们将总结以下建议:为了使你的直觉与React Router的相符,请考虑使用组件而不是静态路由。就像使用React声明式的、组合性的解决问题,几乎每个“React Router问题”都可能是“ React问题”。


1.1.2 快速上手

请根据你的使用环境参考以下文档之一:

安装

React Router Core发布为npm包,所以可以使用npmyarn安装:

npm install react-router
# or
yarn add react-router

react-router(React Router Core)仅提供了React Router的核心路由功能,大多数情况下不需要直接安装。如果是在编写浏览器环境中运行的应用,则应安装react-router-dom;如果正在编写React Native应用,则应安装react-router-native。这两个包都会将react-router作为依赖项安装。


1.1.3 测试

React Router依靠React上下文工作,这同样会影响我们路由组件的测试。

上下文

如果你在尝试对所渲染的<Link><Route>等的组件进行单元测试,那么会收到一些相关上下文错误和警告。虽然可以自己尝试添加路由上下文,但是我们建议将单元测试包装在以下Router组件中:有history属性的Router、或<StaticRouter><MemoryRouter><BrowserRouter>( 如果window.history,则在测试环境中可用作全局变量)。

建议使用<MemoryRouter>或自定义history,以便能够在两次测试之间重置路由。

class Sidebar extends Component {
  // ...
  render() {
    return (
      <div>
        <button onClick={this.toggleExpand}>expand</button>
        <ul>
          {users.map(user => (
            <li>
              <Link to={user.path}>{user.name}</Link>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

// broken
test("it expands when the button is clicked", () => {
  render(<Sidebar />);
  click(theButton);
  expect(theThingToBeOpen);
});

// fixed!
test("it expands when the button is clicked", () => {
  render(
    <MemoryRouter>
     <Sidebar />
    </MemoryRouter>
  );
  click(theButton);
  expect(theThingToBeOpen);
});


在指定路径上启动

<MemoryRouter>支持initialEntriesinitialIndex属性,所以可以在指定位置启动应用(或应用的任一部分)。

test("current user is active in sidebar", () => {
  render(
    <MemoryRouter initialEntries={["/users/2"]}>
      <Sidebar />
    </MemoryRouter>
  );
  expectUserToBeActive(2);
});


导航

官方已进行了很多测试,以检查在修改路由位置时是否有效,所以一般不需要测试这些东西。但是,如果需要在应用中测试导航,则可以这样进行:

// app.js (a component file)
import React from "react";
import { Route, Link } from "react-router-dom";

// our Subject, the App, but you can test any sub
// section of your app too
const App = () => (
  <div>
    <Route
      exact
      path="/"
      render={() => (
        <div>
          <h1>Welcome</h1>
        </div>
      )}
    />
    <Route
      path="/dashboard"
      render={() => (
        <div>
          <h1>Dashboard</h1>
          <Link to="/" id="click-me">
            Home
          </Link>
        </div>
      )}
    />
  </div>
);
// you can also use a renderer like "@testing-library/react" or "enzyme/mount" here
import { render, unmountComponentAtNode } from "react-dom";
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from "react-router-dom";

// app.test.js
it("navigates home when you click the logo", async => {
  // in a real test a renderer like "@testing-library/react"
  // would take care of setting up the DOM elements
  const root = document.createElement('div');
  document.body.appendChild(root);

  // Render app
  render(
    <MemoryRouter initialEntries={['/my/initial/route']}>
      <App />
    <MemoryRouter>,
    root
  );

  // Interact with page
  act(() => {
    // Find the link (perhaps using the text content)
    const goHomeLink = document.querySelector('#nav-logo-home');
    // Click it
    goHomeLink.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  // Check correct page content showed up
  expect(document.body.textContent).toBe('Home');
});


在测试中查看location

如果想在测试中查看locationhistory对象(如,验证是否在URL栏中设置了新的查询参数),则可以添加一条路由来更新测试中的变量:

// app.test.js
test("clicking filter links updates product query params", () => {
  let history, location;
  render(
    <MemoryRouter initialEntries={["/my/initial/route"]}>
      <App />
      <Route
        path="*"
        render={({ history, location }) => {
          history = history;
          location = location;
          return null;
        }}
      />
    </MemoryRouter>,
    node
  );

  act(() => {
    // example: click a <Link> to /products?id=1234
  });

  // assert about url
  expect(location.pathname).toBe("/products");
  const searchParams = new URLSearchParams(location.search);
  expect(searchParams.has("id")).toBe(true);
  expect(searchParams.get("id")).toEqual("1234");
});

可选方案:

  1. 如果测试环境中有浏览器全局变量window.locationwindow.history(通过JSDOM在Jest中的设置默认值,但无法重置测试之间的历史记录),也可以使用BrowserRouter
  2. 还可以将基本路由与history中的历史记录一起使用,而不是将自定义路由传给MemoryRouter
// app.test.js
import { createMemoryHistory } from "history";
import { Router } from "react-router";

test("redirects to login page", () => {
  const history = createMemoryHistory();
  render(
    <Router history={history}>
      <App signedInUser={null} />
    </Router>,
    node
  );
  expect(history.location.pathname).toBe("/login");
});


React测试库(React Testing Library)

更多示例请参考官方文档:使用React Testing Library测试React路由


1.1.4 Redux集成

Redux是React生态中重要的一部分。对于需要同时使用React Router和Redux的人,我们希望可以无缝集成。

阻止更新

通常,React Router和Redux可以很好地协同工作。不过,有时应用的组件可能会在路径更改时(子路由或活动的导航链接不更新)不更新。

在以下情况下会发生这种状况:

  1. 组件通过connect()(Comp)连接到Redux
  2. 组件不是“路由组件”,也就是说其渲染方式不是这样:<Route component = {SomeConnectedThing} />

问题在于Redux实现了shouldComponentUpdate,如果没有从路由中接收到props,则表明没有发生任何变化。这也很容易解决:查找连接组件的位置,然后使用withRouter对其进行包装。

// before
export default connect(mapStateToProps)(Something)

// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))
Deep integration


深度集成

有些人会想:

  1. store同步,并从store获取路由数据
  2. 能够通过dispatch触发的action进行导航
  3. 在Redux devtools中支持路径修改,以进行时间维度的调试

所有这些都需要更深入的集成。

官方建议是,不要将路由完全放到Redux的store中,因为:

  1. 路由数据已经被大多数关心它的组件的支持。无论是来自store还是router,你组件的代码都基本相同。
  2. 在大多数情况下,可以使用LinkNavLinkRedirect来进行导航操作。有时,在某些由最初action启动的异步任务之后,可能还需要以编程方式导航。例如,可以在用户提交登录表单时调度操作。然后,然后,你所使用的thunksaga或其他异步处理程序会对凭据进行身份验证,如果成功,则需要以某种方式导航到新页面。此处的解决方案只是将history对象(提供给所有路由组件)包括所在action中的有效负载,并且异步处理程序可以在适当的时候使用此对象进行导航。
  3. 路由修改对于时间维度的调试不太重要。唯一明显的情况是调试router/store同步中的问题,如果根本不同步二者,则该问题将消失。

但是,如果你非常望与store同步路由,则可以尝试使用Connected React Router,这是一个React Router v4和Redux的第三方绑定。


1.1.5 静态路由

以前版本的React Router使用静态路由来配置应用路由。这样可以在渲染之前检查和匹配路径。自v4开始转为使用动态组件而不是路由配置,因此一些以前的用例变得不那么明显和有针对性。

官方目前正在开发一个可与静态路由配置和React Router配合使用的软件包,以继续满足这些用例。

更多相关参考,请查阅:React Router Config


1.2 API

1.2.1 钩子(Hooks)

React Router中有一些钩子,你可以通过它们访问路由state并在组件内部执行导航。

请注意:要使用这些钩子,需要 React >= 16.8

useHistory

useHistory钩子使你可以访问可用于导航的history实例。

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
}


useLocation

useLocation钩子会返回代表当前URL的location对象。可以像useState一样考虑它,只要URL发生更改,它就会返回一个新的location

这个钓子非常有用,例如 在你希望每次加载新页面时都使用Web分析工具触发新的“页面浏览”事件时,如以下示例所示:

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  useLocation
} from "react-router-dom";

function usePageViews() {
  let location = useLocation();
  React.useEffect(() => {
    ga.send(["pageview", location.pathname]);
  }, [location]);
}

function App() {
  usePageViews();
  return <Switch>...</Switch>;
}

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  node
);


useParams

useParams会返回一个包含URL参数的键/值对对象,用于访问当前<Route>match.params

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  useParams
} from "react-router-dom";

function BlogPost() {
  let { slug } = useParams();
  return <div>Now showing post {slug}</div>;
}

ReactDOM.render(
  <Router>
    <Switch>
      <Route exact path="/">
        <HomePage />
      </Route>
      <Route path="/blog/:slug">
        <BlogPost />
      </Route>
    </Switch>
  </Router>,
  node
);


useRouteMatch

useRouteMatch钩子尝试以与<Route>相同的方式匹配(match)当前URL。可以在无需实际呈现<Route>的情况下访问match数据。

现在将前面示例替换为:

import { Route } from "react-router-dom";

function BlogPost() {
  return (
    <Route
      path="/blog/:slug"
      render={({ match }) => {
        // Do whatever you want with the match...
        return <div />;
      }}
    />
  );
}

只需要:

import { useRouteMatch } from "react-router-dom";

function BlogPost() {
  let match = useRouteMatch("/blog/:slug");

  // Do whatever you want with the match...
  return <div />;
}


1.2.2 <MemoryRouter>

一个将“URL”的历史记录(history)保存在内存中(不读取或写入地址栏)<Router>。在测试和非浏览器环境(如:React Native)中很有用。

<MemoryRouter
  initialEntries={optionalArray}
  initialIndex={optionalNumber}
  getUserConfirmation={optionalFunc}
  keyLength={optionalNumber}
>
  <App />
</MemoryRouter>
initialEntries: array

历史记录堆栈中的位置(location)数组。这些可能是带有{ pathname, search, hash, state }或简单字符串URL的location对象。

<MemoryRouter
  initialEntries={["/one", "/two", { pathname: "/three" }]}
  initialIndex={1}
>
  <App />
</MemoryRouter>


initialIndex: number

初始位置在initialEntries数组中的索引。


getUserConfirmation: func

用于确认导航的函数。将<MemoryRouter><Prompt>直接一起使用时,必须使用此选项


keyLength: number

location.key的长度。默认为6


children: node

要渲染的子元素。

注意:用于React <16时,必须使用单个子元素,因为render方法不能返回多个元素。如果需要多个元素,可以尝试将它们包装在额外的<div>中。


1.2.3 <Prompt>

用于在离开页面之前提示用户。当你的应用进入应阻止用户导航的状态时(如,表单已在录入中),渲染<Prompt>

<Prompt
  when={formIsHalfFilledOut}
  message="Are you sure you want to leave?"
/>
message: string

当用户尝试离开时提示用户的消息。

<Prompt message="Are you sure you want to leave?" />


message: func

将与用户尝试导航到的下一个位置(location)和操作(action)一起调用。会返回一个字符串以向用户显示提示,或者返回true以允许过渡。

<Prompt
  message={location =>
    location.pathname.startsWith("/app")
      ? true
      : `Are you sure you want to go to ${location.pathname}?`
  }
/>


when: bool

用于替代条件渲染<Prompt>,你可以始终渲染它,并通过when={true}when={false}来阻止或允许进行相应的导航。

<Prompt when={formIsHalfFilledOut} message="Are you sure?" />


1.2.4 <Redirect>

渲染一个<Redirect>将导航到的新位置(location)。新location将覆盖历史堆栈中的当前location,类似于服务器端重定向(HTTP 3xx)。

<Route exact path="/">
  {loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>
to: string

到重定向到的URL,可以是任何被path-to-regexp@^1.7.0所解析的有效路径。to中使用的所有URL参数必须由from覆盖。

<Redirect to="/somewhere/else" />


to: object

要重定向的location,其中pathname是任何被path-to-regexp@^1.7.0所解析的有效路径。

<Redirect
  to={{
    pathname: "/login",
    search: "?utm=your+face",
    state: { referrer: currentLocation }
  }}
/>

以上示例中,state对象可以在重定向后组件的this.props.location.state中访问。然后,可以通过路径名/login指向的Login组件中的this.props.location.state.referrer访问此新的referrer来源关键字(不是特殊名称)。


push: bool

设置为true时,重定向会将新条目推入历史记录(history),而不是替换当前条目


from: string

要重定向的路径名。可以是任何被path-to-regexp@^1.7.0所解析的有效路径。所有匹配的URL参数都提供给to,且必须包含to中所需的所有参数,to中不使用的其它参数将被忽略。

<Switch>
  <Redirect from='/old-path' to='/new-path' />
  <Route path='/new-path'>
    <Place />
  </Route>
</Switch>

// Redirect with matched parameters
<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id'/>
  <Route path='/users/profile/:id'>
    <Profile />
  </Route>
</Switch>

注意:仅当在<Switch>内渲染<Redirect>时,才能用于匹配位置。更多相关详细信息,请参见


exact: bool

完全匹配,等价于Route.exact

<Switch>
  <Redirect exact from="/" to="/home" />
  <Route path="/home">
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</Switch>

注意:仅当在<Switch>内渲染<Redirect>时,才能用于匹配位置。更多相关详细信息,请参见


strict: bool

严格匹配,等价于Route.strict

<Switch>
  <Redirect strict from="/one/" to="/home" />
  <Route path="/home">
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</Switch>

注意:仅当在<Switch>内渲染<Redirect>时,才能用于匹配位置。更多相关详细信息,请参见


sensitive: bool

区分大小写,等价于Route.sensitive


1.2.5 <Route>

<Route>组件可能是React Router中需要了解和使用的最重要组件。它的最基本职责是在路径与当前URL匹配时渲染UI。

考虑以下代码:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

ReactDOM.render(
  <Router>
    <div>
      <Route exact path="/">
        <Home />
      </Route>
      <Route path="/news">
        <NewsFeed />
      </Route>
    </div>
  </Router>,
  node
);

当应用的位置是/时,UI的层次结构将类似于:

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

而应用的位置是/news时,UI的层次结构将类似于:

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

上面示例中的“react-empty”注释是React空渲染时实现细节。但是这是有用的,可以使我们能更好的理解。从技术角度上讲,即使为空,也会对其进行“渲染”。当<Route>路径与当前URL匹配时,它将渲染其子节点(子组件)。

Route的渲染方法

建议使用<Route>渲染相关内容的方法是使用子元素,如前所示。但是,还有一些其他方法可用于通过<Route>渲染相关内容。提供这些方法是为了支持在引入钩子之前使用早期版本路由构建的应用。

可以在指定的<Route>上仅使用这些属性(props)之一。请参阅下面的说明以了解它们之间的区别。


Route的props

所有这些属性都可以传入以下三个路由属性(props):


component

一个仅在位置(location)匹配时才呈现的React组件,其会与路由属性(props)一起渲染。

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

// All route props (match, location and history) are available to User
function User(props) {
  return <h1>Hello {props.match.params.username}!</h1>;
}

ReactDOM.render(
  <Router>
    <Route path="/user/:username" component={User} />
  </Router>,
  node
);

当使用组件(而不是下述的<render或子组件)时,路由会使用React.createElement从指定的组件中创建一个新的React元素。这意味着,如果你向组件属性提供内置函数,则将在每个渲染中创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不是仅更新现有组件。使用内置函数进行内联渲染时,应使用renderchildren属性(如下)


render: func

本方法可以方便地进行内置渲染和包装,而无需进行上面不必要的重新安装。

不使用组件的prop来创建新的React元素,而是可以传递<location匹配时要调用的函数。渲染属性方法可以访问与组件渲染属性相同的所有路由属性(matchlocationhistory)。

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";

// convenient inline rendering
ReactDOM.render(
  <Router>
    <Route path="/home" render={() => <div>Home</div>} />
  </Router>,
  node
);

// wrapping/composing
// You can spread routeProps to make them available to your rendered Component
function FadingRoute({ component: Component, ...rest }) {
  return (
    <Route
      {...rest}
      render={routeProps => (
        <FadeIn>
          <Component {...routeProps} />
        </FadeIn>
      )}
    />
  );
}

ReactDOM.render(
  <Router>
    <FadingRoute path="/cool" component={Something} />
  </Router>,
  node
);

注意:<Route component>优先于<Route render>,所以不要在同一<Route>中同时使用两者。


children: func

有时需要判断渲染路径是否与位置匹配。这种情况下,可以使用children属性。它与render完全一样,除了会判断是否匹配之外。

children的属性会接收与componentrender方法相同的所有路由属性,除非当路径未能与URL匹配,则matchnull。这样就可以根据路由是否匹配来动态调整UI。

以下示例中,如果路径匹配,我们会添加一个active的类:

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Link,
  Route
} from "react-router-dom";

function ListItemLink({ to, ...rest }) {
  return (
    <Route
      path={to}
      children={({ match }) => (
        <li className={match ? "active" : ""}>
          <Link to={to} {...rest} />
        </li>>
      )}
    />
  );
}

ReactDOM.render(
  <Router>
    <ul>
      <ListItemLink to="/somewhere" />
      <ListItemLink to="/somewhere-else" />
    </ul>
  </Router>,
  node
);

这对于动画同样适用:

<Route
  children={({ match, ...rest }) => (
    {/* Animate will always render, so you can use lifecycles
        to animate its child in and out */}
    <Animate>
      {match && <Something {...rest}/>}
    </Animate>
  )}
/>

注意:<Route children>优先于<Route component><Route render>,所以不要在同一<Route>中使用多个。


path: string | string[]

可以被path-to-regexp@^1.7.0所解析的路径数组。

<Route path="/users/:id">
  <User />
</Route>

没有path的路由将总是匹配的。


exact: bool

当为true时,仅在路径与location.pathname完全匹配时才匹配。

<Route exact path="/one">
  <About />
</Route>
path location.pathname exact 是否匹配?
/one /one/two true no
/one /one/two false yes


strict: bool

设置为true时,带有斜杠的路径将与location.pathname斜杠相匹配,仅当location.pathname中有其它URL语法时会无效。

<Route strict path="/one/">
  <About />
</Route>
path location.pathname 是否匹配?
/one/ /one no
/one/ /one/ yes
/one/ /one/two yes

strict可以用来强制location.pathname中不带斜杠,要做到这一点需要strictexact必须都为true

<Route strict path="/one">
  <About />
</Route>
path location.pathname 是否匹配?
/one /one yes
/one /one/ no
/one /one/two no


location: object

<Route>元素会尝试将其路径与当前历史位置(通常是当前浏览器URL)匹配。 但是,也可以传递路径名不同的位置进行匹配。

如需果要匹配<Route>当前历史位置以外的位置时,这会很有用。

如果<Route>元素包装在<Switch>中并且与传递给<Switch>的位置(或当前历史位置)相匹配,则传递给<Route>的位置属性将被<Switch>



sensitive: bool

设置为true时,将会区分大小写。

<Route sensitive path="/one">
  <About />
</Route>
path location.pathname sensitive 是否匹配?
/one /one true yes
/One /one true no
/One /one false yes


1.2.6 <Router>

所有路由组件的通用底层接口。通常,应用将使用以下高级路由接口之一代替:

使用底层<Router>的最常见用例是将自定义历史与状态管理库(如:ReduxMobx)进行同步。请注意,不需要将状态管理库与React Router一起使用,其仅用于深度集成。

import React from "react";
import ReactDOM from "react-dom";
import { Router } from "react-router";
import { createBrowserHistory } from "history";

const history = createBrowserHistory();

ReactDOM.render(
  <Router history={history}>
    <App />
  </Router>,
  node
);
history-object

用于导航的location对象。

import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";

const customHistory = createBrowserHistory();

ReactDOM.render(<Router history={customHistory} />, node);


children: node

要渲染的子元素。

<Router>
  <App />
</Router>


1.2.7 <StaticRouter>

静态的<Router>,其永远不会修改location

在用户实际上没有点击时,这在服务器端渲染方案中很有用,因此位置永远不会发生实际变化。所以,名称为:static。其在简单测试中也很有用,如:你只需要插入一个位置并在渲染输出中进行断言时。

以下是一个示例的Node服务端,它会为<Redirect>发送302状态代码,并为其他请求发送常规HTML:

import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router";

http
  .createServer((req, res) => {
    // This context object contains the results of the render
    const context = {};

    const html = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );

    // context.url will contain the URL to redirect to if a <Redirect> was used
    if (context.url) {
      res.writeHead(302, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(html);
      res.end();
    }
  })
  .listen(3000);
basename: string

用于所有location的基本URL。正确格式的基本名称应以斜杠(/)开头,但不能以斜杠结尾。

<StaticRouter basename="/calendar">
  <Link to="/today"/> // renders <a href="/calendar/today">
</StaticRouter>


location: string

服务端所收到的URL,其在Node服务端可能是req.url

<StaticRouter location={req.url}>
  <App />
</StaticRouter>


location: object

位置对象,其类似于{ pathname, search, hash, state }

<StaticRouter location={{ pathname: "/bubblegum" }}>
  <App />
</StaticRouter>


context: object

一个普通的JavaScript对象。在渲染期间,组件可以向对象添加属性以存储有关渲染的信息。

const context = {}
<StaticRouter context={context}>
  <App />
</StaticRouter>

<Route>匹配时,其会把上下文对象传递给它作为staticContext属性渲染的组件。查看服务端渲染指南,以获取有关执行此操作的更多信息。渲染后,这些属性可用于配置服务器的响应。

if (context.status === "404") {
  // ...
}


children: node

要渲染的子元素。

注意:用于React <16时,必须使用单个子元素,因为render方法不能返回多个元素。如果需要多个元素,可以尝试将它们包装在额外的<div>中。


1.2.8 <Switch>

渲染与location相匹配的第一个<Route><Redirect>子元素。

那么,这与仅使用一些<Route>有什么不同呢?

<Switch>的独特之处在于它专门渲染唯一的路由。相反,每个与该位置匹配的<Route>都将进行包含性渲染。考虑以下路由:

import { Route } from "react-router";

let routes = (
  <div>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/:user">
      <User />
    </Route>
    <Route>
      <NoMatch />
    </Route>
  </div>
);

如果URL是/about,那么<About><User><NoMatch>都会渲染,因为其路径全部匹配。这是设计使然,这允许我们以多种方式将<Route>组合到我们的应用中,例如:侧边栏和面包屑、引导程序选项卡等。

但是,有时我们只选择一个<Route>进行渲染。例如,我们位于/about,但不想同时匹配/:user(或显示"404"页面)。使用Switch的方法如下:

import { Route, Switch } from "react-router";

let routes = (
  <Switch>
    <Route exact path="/">
      <Home />
    </Route>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/:user">
      <User />
    </Route>
    <Route>
      <NoMatch />
    </Route>
  </Switch>
);

现在,如果我们位于/about<Switch>将开始查找所匹配的<Route><Route path="/about"/>会被匹配,然后<Switch>停止继续匹配并渲染<About>。类似的,如果我们位于/michael<User>会被渲染。

这对于动画过渡也同样适用,因为所匹配的<Route>会与上一个组件渲染在同一位置。

let routes = (
  <Fade>
    <Switch>
      {/* there will only ever be one child here */}
      <Route />
      <Route />
    </Switch>
  </Fade>
);

let routes = (
  <Fade>
    {/* there will always be two children here,
        one might render null though, making transitions
        a bit more cumbersome to work out */}
    <Route />
    <Route />
  </Fade>
);
location: object

用于匹配子元素的location对象,而不是当前历史位置(通常是当前浏览器URL)。


children: node

<Switch>的所有为<Route><Redirect>子元素。仅第一个与当前位置所匹配的子元素会被渲染。

<Route>元素会使用其路径属性进行匹配,而<Redirect>元素会使用其from属性进行匹配。没有路径属性的<Route>或没有from属性的<Redirect>将始终与当前位置匹配。

当在<Switch>中包含<Redirect>时,它可以使用任何与<Route>位置相匹配属性:pathexactstrictfrom仅是路径属性的别名。

如果为<Switch>指定位置属性,其将会覆盖所匹配的子元素位置属性。

import { Redirect, Route, Switch } from "react-router";

let routes = (
  <Switch>
    <Route exact path="/">
      <Home />
    </Route>

    <Route path="/users">
      <Users />
    </Route>
    <Redirect from="/accounts" to="/users" />

    <Route>
      <NoMatch />
    </Route>
  </Switch>

);


1.2.9 history

本文档中的所说“历史”(history)和“历史对象”(history 对象)是指历史包,它是React Router仅有的两个主要依赖项之一(除React本身外),它提供了几种不同的实现来管理JavaScript中各种会话历史。

此外,还使用了以下术语:

  • “浏览器历史”("browser history") - 特定于DOM的实现,在支持HTML5历史记录API的Web浏览器中很有用
  • “哈希历史”("hash history") - 遗留Web浏览器的DOM特定实现
  • “内存历史”("memory history") - 内存历史记录实现,可用于测试和非DOM环境(如:React Native)

history对象通常有以下属性和方法:

  • length - (number) 历史记录堆栈中的条目数
  • action - (string) 当前动作 (PUSHREPLACEPOP)
  • location - (object) 当前位置。其可能包含以下属性:
    • pathname - (string) URL中的路径
    • search - (string) URL中的查询字符串
    • hash - (string) URL中的哈希段
    • state - (object) 特定于当前位置的状态,如,当前位置被压入堆栈时push(path, state)。仅浏览器和内存历史记录中可用。
  • push(path, [state]) - (function) 向历史堆栈中添加一个条目
  • replace(path, [state]) - (function) 替换历史堆栈中的当前条目
  • go(n) - (function) 跳转到历史堆栈中第n个条目
  • goBack() - (function) 等价于go(-1)
  • goForward() - (function) 等价于go(1)
  • block(prompt) - (function) 阻止导航(请参阅:history 文档


历史是可变的

history对象是可变的,因此,建议从<Route>的渲染属性而不是history.location访问location。这可以确保你对React的假设在生命周期钩子中是正确的。例如:

class Comp extends React.Component {
  componentDidUpdate(prevProps) {
    // will be true
    const locationChanged =
      this.props.location !== prevProps.location;

    // INCORRECT, will *always* be false because history is mutable.
    const locationChanged =
      this.props.history.location !== prevProps.history.location;
  }
}

<Route component={Comp} />;

根据你所使用的实现方式,可能还会显示其他属性。请参阅history 文档以查看更多详细信息。


1.2.10 location

位置(location)表示该应用现在的位置、你希望其运行的位置、甚至是以前的位置。其看起来像下面这样:

{
  key: 'ac3df4', // not with HashHistory!
  pathname: '/somewhere',
  search: '?some=search-string',
  hash: '#howdy',
  state: {
    [userDefined]: true
  }
}

路由会在以下几个地方为提供位置对象:

还可以在history.location上找到它,但不应使用它,因为它是可变的。可以在history 文档中查看更多相关信息。

位置对象永远不会发生变化,因此可以在生命周期钩子中使用它来确定何时进行导航,这对于数据获取和动画处理非常有用。

componentWillReceiveProps(nextProps) {
  if (nextProps.location !== this.props.location) {
    // navigated!
  }
}

可以在导航的各个位置中提供location对象而不是字符串:

通常,你会只使用字符串,但是如果需要添加一些“位置状态”以使应用可返回到该特定位置,则可以使用位置对象代替。如果你要基于导航历史而不是仅基于路径来区分UI,这将非常有用。

// usually all you need
<Link to="/somewhere"/>

// but you can use a location instead
const location = {
  pathname: '/somewhere',
  state: { fromDashboard: true }
}

<Link to={location}/>
<Redirect to={location}/>
history.push(location)
history.replace(location)

最后,你还可向以下组件中传入location

这样可以防止他们在路由状态中使用实际位置。这对于动画和待处理的导航、或者在你想要使组件在与真实位置不同的位置进行渲染时会很有用。


1.2.11 match

匹配(match)对象包含有关<Route path>如何与URL匹配的信息。匹配对象包含以下属性:

  • params - (object) 从URL路径的动态段(如:/user/:id中的:id)解析的键/值对
  • isExact - (boolean) 如果整个URL都匹配,则为true(没有结尾字符)
  • path - (string) 用于匹配路径的匹配模式。用于构建嵌套<Route>
  • url - (string) URL的匹配部分。对于构建嵌套的<Link>很有用

你可以各个地方获取到match

如果<Route>没有路径,将始终匹配,则将获得与其最接近的父匹配项。withRouter也是如此。

空匹配

即使子路径的路径与当前位置不匹配,使用子元素属性的<Route>也会调用其子函数。在这种情况下,匹配将为null。能够在匹配时渲染<Route>的内容可能会很有用,但是这种情况会有一些问题。

“解析”URL的默认方法会将match.url字符串连接到“相对”路径。

let path = `${match.url}/relative-path`;

如果在匹配为null时尝试执行此操作,则最终将出现TypeError。这意味着在使用子属性时尝试在<Route>内部添加“相对”路径是不安全的。

当在生成空匹配对象的<Route>中使用无路径<Route>时,会发生类似但更微妙的情况:

// location.pathname = '/matches'
<Route path="/does-not-match"
  children={({ match }) => (
    // match === null
    <Route
      render={({ match: pathlessMatch }) => (
        // pathlessMatch === ???
      )}
    />
  )}
/>

无路径<Route>会从其父级继承match对象。如果其父匹配项为null,则其匹配项也将为null。这意味着:

  1. 任何子路由/链接都必须是绝对的,因为没有父级可以解析
  2. 并且,父级match是为null的无路径路由时,将需要使用子prop进行渲染


1.2.12 withRouter

可以通过withRouter高阶组件访问history对象属性和最接近的<Route>match项。每当渲染时,withRouter都会将更新的matchlocationhistory并传递给包装组件。

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

// A simple component that shows the pathname of the current location
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>You are now at {location.pathname}</div>;
  }
}

// Create a new component that is "connected" (to borrow redux
// terminology) to the router.
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);

重要提示:

withRouter不像React Redux的connect那样订阅位置更改以进行状态更改。而是在位置更改后从<Router>组件传播出去后重新渲染。这意味着withRouter不会在路由转换时重新渲染,除非其父组件重新渲染。

静态方法和属性

包装组件的所有非特定于React的静态方法和属性将自动复制到“已连接”组件。

Component.WrappedComponent

包装的组件在返回的组件上作为静态属性WrappedComponent公开,其可以用于隔离测试组件等。

// MyComponent.js
export default withRouter(MyComponent)

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


wrappedComponentRef: func

该函数将作为ref prop传递给包装的组件。

class Container extends React.Component {
  componentDidMount() {
    this.component.doSomething();
  }

  render() {
    return (
      <MyComponent wrappedComponentRef={c => (this.component = c)} />
    );
  }
}


1.2.13 generatePath

generatePath函数可用于生成路由的URL。其内部使用path-to-regexp库。

import { generatePath } from "react-router";

generatePath("/user/:id/:entity(posts|comments)", {
  id: 1,
  entity: "posts"
});
// Will return /user/1/posts

将路径编译为正则表达式的结果会被缓存,因此生成具有相同模式的多个路径不会产生任何开销。

该函数包含以下两个参数:

pattern: string

作为路径属性提供给Route组件的匹配模式。

params: object

第二个参数是一个有要使用的匹配模式的相应参数的对象。

如果提供的参数和路径不匹配,则会引发错误:

generatePath("/user/:id/:entity(posts|comments)", { id: 1 });
// TypeError: Expected "entity" to be defined


2. React Router Web相关

2.1 指南


2.1.1 快速上手-Quick Start

要在Web应用中使用React Router,需要一个React Web应用。建议使用Create React App工具来创建应用,这是一个React官方提供的工具,可以与React Router很好地配合使用。

首先,安装create-react-app并使用它来创建应用:

npm install -g create-react-app
create-react-app demo-app
cd demo-app


初始化

可以使用npmyarnnpm公用注册库中安装React Router。由于我们正在构建Web应用,因此在本指南中将使用react-router-dom

npm install react-router-dom

接下来,将以下两个示例之一复制/粘贴到src/App.js中。


示例一:基本路由

在此示例中,路由处理了3个“页面”:主页(/)、关于页(/about)、用户页(/users)。当单击不同的<Link>时,路由将渲染与之匹配的<Route>

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>

        {/* A <Switch> looks through its children <Route>s and
            renders the first one that matches the current URL. */}
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

注意:在背后的实现中,<Link>会使用真实的href渲染出<a>,因此使用键盘进行导航或屏幕的人仍然可以使用此应用。


示例二:嵌套路由

本示例将介绍嵌套路由是如何工作的。/topics路由将会加载Topics,这会按path:id值有条件地渲染任何其他<Route>

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch,
  useParams
} from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Topics() {
  let match = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>
            Props v. State
          </Link>
        </li>
      </ul>

      {/* The Topics page has its own <Switch> with more routes
          that build on the /topics URL path. You can think of the
          2nd <Route> here as an "index" page for all topics, or
          the page that is shown when no topic is selected */}
      <Switch>
        <Route path={`${match.path}/:topicId`}>
          <Topic />
        </Route>
        <Route path={match.path}>
          <h3>Please select a topic.</h3>
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let { topicId } = useParams();
  return <h3>Requested topic ID: {topicId}</h3>;
}


2.1.2 主要组件-Primary Components

React Router中的组件主要分为三类:

一般,我们喜欢将导航组件称为“路由修改器”。

在Web应用中使用的所有组件都应从react-router-dom导入。

import { BrowserRouter, Route, Link } from "react-router-dom";
Routers


路由

每个React Router应用的核心是路由组件。对于Web项目来说,react-router-dom提供<BrowserRouter>和<HashRouter>路由。两者之间的主要区别在于它们存储URL的形式及与Web服务端通信的方式:

  • <BrowserRouter>使用普通URL路径。这种形式通常是外观看起来最友好的URL,但是要求正确配置服务器。也就是说,在Web服务端需要在所有由React Router客户端管理的URL上提供相同的页面。Create React App在开发中已即开即用地支持此功能,并提供了有关如果配置生产服务器的相关说明
  • <HashRouter>将当前位置存储在URL的哈希部分中,URL看起来类似于http://example.com/#/your/page。由于哈希不会发送到服务器,这意味着不需要特殊的服务器配置。

要使用路由,只需确保将其渲染在元素层次结构的根目录下即可。通常,会将顶级<App>元素包装在路由中,如下所示:

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

function App() {
  return <h1>Hello React Router</h1>;
}

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);


路由匹配器

有两个路由匹配组件:SwitchRoute。渲染<Switch>时,它将搜索其子<Route>元素以查找路径与当前URL匹配的元素。当找到一个时,将会渲染该<Route>并忽略所有其他路由。这意味着你需要在<Route>中包含更多指定路径(通常较长)的路径放在不非特定路径之前。

如果<Route>未匹配,<Switch>会渲染null

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  Route
} from "react-router-dom";

function App() {
  return (
    <div>
      <Switch>
        {/* If the current URL is /about, this route is rendered
            while the rest are ignored */}
        <Route path="/about">
          <About />
        </Route>

        {/* Note how these two routes are ordered. The more specific
            path="/contact/:id" comes before path="/contact" so that
            route will render when viewing an individual contact */}
        <Route path="/contact/:id">
          <Contact />
        </Route>
        <Route path="/contact">
          <AllContacts />
        </Route>

        {/* If none of the previous routes render anything,
            this route acts as a fallback.

            Important: A route with path="/" will *always* match
            the URL because all URLs begin with a /. So that's
            why we put this one last of all */}
        <Route path="/">
          <Home />
        </Route>
      </Switch>
    </div>
  );
}

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);

需要注意的是<Route path>匹配URL的开头,而不是完整路径。所以,<Route path="/">将始终与所有URL匹配,通常我们会将此<Route>放在<Switch>的最后。另一种解决方案是使用exactURL匹配的<Route exact path="/">

注意:虽然React Router支持在<Switch>之外渲染<Route>元素,但是自5.1版本开始,建议改用useRouteMatch钩子。此外,不建议渲染没有路径的<Route>,而是使用钩子来访问所需的任何变量。


导航(或路由修改器)

React Route提供了<Link>组在应用中创建链接。无论在何处渲染<Link>,锚点(<a>)都将渲染到HTML文档中。

<Link to="/">Home</Link>
// <a href="/">Home</a>

<NavLink>是一种特殊类型的<Link>,当其prop与当前位置匹配时,可以将其自身设置为“active”。

<NavLink to="/react" activeClassName="hurray">
  React
</NavLink>

// When the URL is /react, this renders:
// <a href="/react" className="hurray">React</a>

// When it's something else:
// <a href="/react">React</a>

当需要强制导航时,就使用<Redirect>。渲染<Redirect>时,它将使用其prop进行导航。

<Redirect to="/login" />


2.1.3 服务端渲染-Server Rendering

由于服务端都是无状态的,因此在服务端渲染会有些不同。基本思想是将应用程序包装在无状态的<StaticRouter>中,而不是<BrowserRouter>中。并从服务端传入请求的url,以使路由可以匹配,然后我们将讨论上下文支持。

// client
<BrowserRouter>
  <App/>
</BrowserRouter>

// server (not the complete story)
<StaticRouter
  location={req.url}
  context={context}
>
  <App/>
</StaticRouter>

当在客户端渲染<Redirect>时,浏览器历史会更改状态,我们会得到新的屏幕。在静态服务器环境中,我们无法更改应用程序状态。相反,应使用上下文属性(prop)来得到渲染结果。如果有context.url,则表明该应用已重定向。这使我们能够从服务器发送适当的重定向。

const context = {};
const markup = ReactDOMServer.renderToString(
  <StaticRouter location={req.url} context={context}>
    <App />
  </StaticRouter>
);

if (context.url) {
  // Somewhere a `<Redirect>` was rendered
  redirect(301, context.url);
} else {
  // we're good, send the response
}


为应用程序添加上下文信息

路由仅添加context.url,但是你可能希望将某些URL重定向为301,并将其他重定向为302。或者,如果渲染了UI的某些特定分支,则可能要发送404响应;如果未授权,则要发送401。你可以完全操作上下文的prop,并对其进行修改。这是区分301和302重定向的一种方式:

function RedirectWithStatus({ from, to, status }) {
  return (
    <Route
      render={({ staticContext }) => {
        // there is no `staticContext` on the client, so
        // we need to guard against that here
        if (staticContext) staticContext.status = status;
        return <Redirect from={from} to={to} />;
      }}
    />
  );
}

// somewhere in your app
function App() {
  return (
    <Switch>
      {/* some other routes */}
      <RedirectWithStatus status={301} from="/users" to="/profiles" />
      <RedirectWithStatus
        status={302}
        from="/courses"
        to="/dashboard"
      />
    </Switch>
  );
}

// on the server
const context = {};

const markup = ReactDOMServer.renderToString(
  <StaticRouter context={context}>
    <App />
  </StaticRouter>
);

if (context.url) {
  // can use the `context.status` that
  // we added in RedirectWithStatus
  redirect(context.status, context.url);
}


404、401及其它状态

如前面所示,我们可以创建组件并添加一些上下文,并将其渲染在应用程序中的任何位置以获取不同的状态代码:

function Status({ code, children }) {
  return (
    <Route
      render={({ staticContext }) => {
        if (staticContext) staticContext.status = code;
        return children;
      }}
    />
  );
}

现在,你可以在要将代码添加到staticContext应用的任何位置并渲染Status

function NotFound() {
  return (
    <Status code={404}>
      <div>
        <h1>Sorry, can’t find that.</h1>
      </div>
    </Status>
  );
}

function App() {
  return (
    <Switch>
      <Route path="/about" component={About} />
      <Route path="/dashboard" component={Dashboard} />
      <Route component={NotFound} />
    </Switch>
  );
}


放在一起

这不是一个真正的应用,但是它展示了将它们组合在一起所需的所有操作。

服务端:

import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom";

import App from "./App.js";

http
  .createServer((req, res) => {
    const context = {};

    const html = ReactDOMServer.renderToString(
      >StaticRouter location={req.url} context={context}>
        >App />
      >/StaticRouter>
    );

    if (context.url) {
      res.writeHead(301, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(`
      >!doctype html>
      >div id="app">${html}>/div>
    `);
      res.end();
    }
  })
  .listen(3000);

客户端:

import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

import App from "./App.js";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("app")
);


数据加载

数据加载有多种不同的方式,而且还没有明确的最佳实践,因此本示例力求可与任何一种方法相融合,而不是规定或倾向于任何一种方法。

主要限制是需要在渲染之前加载数据。React Router导出其内部使用的matchPath静态函数以将位置匹配到路由。你可以在服务器上使用此功能来帮助确定渲染之前的数据依赖关系。

这种方法的要旨是依赖静态路由配置,该配置既可以渲染路由,也可以在渲染之前进行匹配以确定数据依赖性。

const routes = [
  {
    path: "/",
    component: Root,
    loadData: () => getSomeData()
  }
  // etc.
];

然后使用此配置在应用中渲染路由:

import { routes } from "./routes.js";

function App() {
  return (
    <Switch>
      {routes.map(route => (
        <Route {...route} />
      ))}
    </Switch>
  );
}

然后在服务端可以像下面这样:

import { matchPath } from "react-router-dom";

// inside a request
const promises = [];
// use `some` to imitate `<Switch>` behavior of selecting only
// the first to match
routes.some(route => {
  // use `matchPath` here
  const match = matchPath(req.path, route);
  if (match) promises.push(route.loadData(match));
  return match;
});

Promise.all(promises).then(data => {
  // do something w/ the data so the client
  // can access it then render the app
});

最后,客户端需要提取数据。同样,我们不为的应用规定数据加载模式,但这是需要实现的接触点。

可以通过React Router Config包了解更多关于数据加载和服务端渲染相关内容。


2.1.4 代码分割-Code Splitting

Web的一项重要功能是,我们无需让用户下载整个应用即可使用。可以将代码拆分视为增量应用下载。为此,我们需要使用webpack@babel/plugin-syntax-dynamic-importloadable-components

webpack内置了对动态导入的支持;但是,如果使用的是Babel(例如,将JSX编译为JavaScript),则需要使用@babel/plugin-syntax-dynamic-import插件。这是仅语法的插件,也就是说Babel不会进行任何其他转换。该插件仅允许Babel解析动态导入,因此webpack可以将它们捆绑为代码拆分。你的.babelrc应该类似如下:

{
  "presets": ["@babel/preset-react"],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

loadable-components是用于通过动态导入加载组件的库。它会自动处理各种边缘情况,并使代码拆分变得简单! 以下是关于loadable-components的使用示例:

import loadable from "@loadable/component";
import Loading from "./Loading.js";

const LoadableComponent = loadable(() => import("./Dashboard.js"), {
  fallback: <Loading />
});

export default class LoadableDashboard extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}

以上示例中,只需通过LoadableDashboard(或任何其它名称的组件)在应用中使用它时,它将自动加载并渲染。fallback是一个占位符组件,用于在加载实际组件时显示。

完整文档请参考:查看详细

代码分割与服务端渲染

loadable-components中提供了服务端渲染操作指引


2.1.5 滚动还原-Scroll Restoration

在早期版本的React Router中,我们提供了对滚动恢复的开箱即用的支持,之后人们一直对其有需求。本文档旨在帮从滚动条和路由中获得所需的信息!

浏览器可以以自己的history.pushState处理滚动还原,其处理方式与使用普通浏览器导航的处理方式相同。且已经可以在Chrome浏览器中使用。这是一个滚动恢复规范

由于浏览器开始处理“默认情况”,并且应用有不同的滚动需求,因此我们不提供默认滚动管理功能。但本指南可以帮你实现任何滚动需求。


滚到顶部

在大多数情况下,我们所需要做的只是“滚动到顶部”,因为会有较长的内容页面,在导航到该页面时始终是向下滚动的。使用<ScrollToTop>组件可以轻松处理这一问题,该组件会在每次导航时向上滚动窗口:

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

export default function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

如果你尚未使用React 16.8,则可以使用React.Component子类执行相同的操作:

import React from "react";
import { withRouter } from "react-router-dom";

class ScrollToTop extends React.Component {
  componentDidUpdate(prevProps) {
    if (
      this.props.location.pathname !== prevProps.location.pathname
    ) {
      window.scrollTo(0, 0);
    }
  }

  render() {
    return null;
  }
}

export default withRouter(ScrollToTop)

然后,可以将其渲染在应用的顶部,但在路由的下方:

function App() {
  return (
    <Router>
      <ScrollToTop />
      <App />
    </Router>
  );
}

如果将标签页接口连接到路由,那么当他们切换标签页时,可能不想滚动到顶部。相反,在需要的特定位置<ScrollToTopOnMount>,该怎么操作?

import { useEffect } from "react";

function ScrollToTopOnMount() {
  useEffect(() => {
    window.scrollTo(0, 0);
  }, []);

  return null;
}

// Render this somewhere using:
// <Route path="..." children={<LongContent />} />
function LongContent() {
  return (
    <div>
      <ScrollToTopOnMount />

      <h1>Here is my long content page</h1>
      <p>...</p>
    </div>
  );
}

同样的,如果尚未使用React 16.8,则可以使用React.Component子类执行相同的操作:

import React from "react";

class ScrollToTopOnMount extends React.Component {
  componentDidMount() {
    window.scrollTo(0, 0);
  }

  render() {
    return null;
  }
}

// Render this somewhere using:
// <Route path="..." children={<LongContent />} />
class LongContent extends React.Component {
  render() {
    return (
      <div>
        <ScrollToTopOnMount />

        <h1>Here is my long content page</h1>
        <p>...</p>
      </div>
    );
  }
}


通用方案

对于通用方案(以及哪些浏览器已开始原生实现),我们需要认论两件事:

  1. 向上滚动导航,这样就不会启动滚动到底部的新屏幕
  2. 恢复窗口的滚动位置和“后退”和“前进”点击上的溢出元素(但不单击“Link”!)

有时,我们希望会有一个通用的API。以下就是一个实现方向:

<Router>
  <ScrollRestoration>
    <div>
      <h1>App</h1>

      <RestoredScroll id="bunny">
        <div style={{ height: "200px", overflow: "auto" }}>
          I will overflow
        </div>
      </RestoredScroll>
    </div>
  </ScrollRestoration>
</Router>

首先,<ScrollRestoration>会在导航时向上滚动窗口。其次,它会将location.key将窗口滚动到的位置和<RestoredScroll>组件的滚动位置保存到sessionStorage。然后,在安装<ScrollRestoration>或<RestoredScroll>组件时,它们可以从sessionStorage查找其位置。

难以处理的部分是为不希望管理窗口滚动的情况定义一个“退出”API。例如,如果在页面内浮动了一些标签导航时,可能不希望滚动到顶部(这些标签可能会滚出视线!)。


2.1.6 设计思想-Philosophy

请参考:Core-指南-设计思想


2.1.7 测试-Testing

请参考:Core-指南-测试


2.1.8 Redux集成-Redux Integration

请参考:Core-指南-Redux集成


2.1.9 静态路由-Static Routes

请参考:Core-指南-静态路由


2.2 示例

本部分摘取了官方示例,对示例中注释进行了简单翻译,请参考官方文档查看示例效果。


2.2.1 基本使用-Basic

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

/* 本示例该站点有3个页面,所有页面均在浏览器中动态渲染(而非服务端渲染)

  虽然页面永远不会刷新,但是请注意当浏览站点时,React Router 如何使URL保持最新。
  这样可以保留浏览器的历史,确保如后退按钮和书签之类的功能正常运行。
*/
export default function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>

        <hr />

        {/*
          <Switch>会遍历其所有子节点 <Route> 元素
          并渲染第一个与当前URL匹配的路径。
          使用<Switch>有多个路由时,
          只有匹配到的第一个会被渲染
        */}
        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// 可以将这些组件视为应用中的“页面”。
function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
}

function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
}


2.2.2 URL参数-URL Parameters

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams
} from "react-router-dom";

// URL参数是URL中以冒号开头的占位符,
// 例如在此示例的路由中定义的`id`参数
// 类似的约定也同样用于匹配其他流行的Web框架(如Rails和Express)中的动态段
export default function ParamsExample() {
  return (
    <Router>
      <div>
        <h2>Accounts</h2>

        <ul>
          <li>
            <Link to="/netflix">Netflix</Link>
          </li>
          <li>
            <Link to="/zillow-group">Zillow Group</Link>
          </li>
          <li>
            <Link to="/yahoo">Yahoo</Link>
          </li>
          <li>
            <Link to="/modus-create">Modus Create</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/:id" children={<Child />} />
        </Switch>
      </div>
    </Router>
  );
}

function Child() {
  // 我们可以在这里使用`useParams`钩子来访问
  // URL的动态段
  let { id } = useParams();

  return (
    <div>
      <h3>ID: {id}</h3>
    </div>
  );
}


2.2.3 嵌套路由-Nesting

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams,
  useRouteMatch
} from "react-router-dom";


// 由于路由是普通的React组件,因此它们
// 可以在应用中的任何位置,包括子元素。
//
// 当需要将应用程序代码拆分为多个包时,
// 这会有所帮助,因为对React Router应用
// 进行代码拆分与对任何其他React应用进行代码拆分相同

export default function NestingExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <hr />

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function Topics() {
  // `path`使我们可以构建相对于父路径的<Route>路径
  // 而`url`使我们可以构建相对链接
  let { path, url } = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>
      <ul>
        <li>
          <Link to={`${url}/rendering`}>Rendering with React</Link>
        </li>
        <li>
          <Link to={`${url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      <Switch>
        <Route exact path={path}>
          <h3>Please select a topic.</h3>
        </Route>
        <Route path={`${path}/:topicId`}>
          <Topic />
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  // 渲染此组件的<Route>的路径为`/topics/:topicId`。 
  // URL的`:topicId`部分表示一个占位符,
  // 我们可以从`useParams()`获取。
  let { topicId } = useParams();

  return (
    <div>
      <h3>{topicId}</h3>
    </div>
  );
}


2.2.4 重定向/认证流程-Redirects (Auth)

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect,
  useHistory,
  useLocation
} from "react-router-dom";

// 本示例包含3个页面:公共页面,受保护页面和登录页面。 
// 为了查看受保护的页面,必须先登录。很标准的东西。
//
// 首先,访问公共页面。然后,访问受保护的页
// 因为尚未登录,因此将会被重定向到登录页
// 登录后,将被重定向回受保护的页面。
//
// 注意每次URL都会更改。如果此时单击“后退”按钮,你是否希望返回登录页面?
// 没有!因为已经登录。尝试一下,将会看到回到登录之前的公共访问页面

export default function AuthExample() {
  return (
    <Router>
      <div>
        <AuthButton />

        <ul>
          <li>
            <Link to="/public">Public Page</Link>
          </li>
          <li>
            <Link to="/protected">Protected Page</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/public">
            <PublicPage />
          </Route>
          <Route path="/login">
            <LoginPage />
          </Route>
          <PrivateRoute path="/protected">
            <ProtectedPage />
          </PrivateRoute>
        </Switch>
      </div>
    </Router>
  );
}

const fakeAuth = {
  isAuthenticated: false,
  authenticate(cb) {
    fakeAuth.isAuthenticated = true;
    setTimeout(cb, 100); // fake async
  },
  signout(cb) {
    fakeAuth.isAuthenticated = false;
    setTimeout(cb, 100);
  }
};

function AuthButton() {
  let history = useHistory();

  return fakeAuth.isAuthenticated ? (
    <p>
      Welcome!{" "}
      <button
        onClick={() => {
          fakeAuth.signout(() => history.push("/"));
        }}
      >
        Sign out
      </button>
    </p>
  ) : (
    <p>You are not logged in.</p>
  );
}

// <Route>包装,如果尚未通过身份验证,
// 则将重定向到登录页
function PrivateRoute({ children, ...rest }) {
  return (
    <Route
      {...rest}
      render={({ location }) =>
        fakeAuth.isAuthenticated ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location }
            }}
          />
        )
      }
    />
  );
}

function PublicPage() {
  return <h3>Public</h3>;
}

function ProtectedPage() {
  return <h3>Protected</h3>;
}

function LoginPage() {
  let history = useHistory();
  let location = useLocation();

  let { from } = location.state || { from: { pathname: "/" } };
  let login = () => {
    fakeAuth.authenticate(() => {
      history.replace(from);
    });
  };

  return (
    <div>
      <p>You must log in to view the page at {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}


mport React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch
} from "react-router-dom";

// 本示例说明了如何创建自定义<Link>,
// 当URL与<Link>指向的URL相同时,
// 它会渲染特殊的外观。

export default function CustomLinkExample() {
  return (
    <Router>
      <div>
        <OldSchoolMenuLink
          activeOnlyWhenExact={true}
          to="/"
          label="Home"
        />
        <OldSchoolMenuLink to="/about" label="About" />

        <hr />

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function OldSchoolMenuLink({ label, to, activeOnlyWhenExact }) {
  let match = useRouteMatch({
    path: to,
    exact: activeOnlyWhenExact
  });

  return (
    <div className={match ? "active" : ""}>
      {match && "> "}
      <Link to={to}>{label}</Link>
    </div>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
}


2.2.6 阻止过渡-Preventing Transitions

import React, { useState } from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Prompt
} from "react-router-dom";

// 有时需要防止用户离开页面。 
// 最常见的用例是当用户在表单中录入一些数据但尚未提交时,
// 我们并不希望用户丢失数据。

export default function PreventingTransitionsExample() {
  return (
    <Router>
      <ul>
        <li>
          <Link to="/">Form</Link>
        </li>
        <li>
          <Link to="/one">One</Link>
        </li>
        <li>
          <Link to="/two">Two</Link>
        </li>
      </ul>

      <Switch>
        <Route path="/" exact children={<BlockingForm />} />
        <Route path="/one" children={<h3>One</h3>} />
        <Route path="/two" children={<h3>Two</h3>} />
      </Switch>
    </Router>
  );
}

function BlockingForm() {
  let [isBlocking, setIsBlocking] = useState(false);

  return (
    <form
      onSubmit={event => {
        event.preventDefault();
        event.target.reset();
        setIsBlocking(false);
      }}
    >
      <Prompt
        when={isBlocking}
        message={location =>
          `Are you sure you want to go to ${location.pathname}`
        }
      />

      <p>
        Blocking?{" "}
        {isBlocking ? "Yes, click a link or the back button" : "Nope"}
      </p>

      <p>
        <input
          size="50"
          placeholder="type something to block transitions"
          onChange={event => {
            setIsBlocking(event.target.value.length > 0);
          }}
        />
      </p>

      <p>
        <button>Submit to stop blocking</button>
      </p>
    </form>
  );
}


2.2.7 未匹配-No Match (404)

import React from "react";
import {
  BrowserRouter as Router,
  Route,
  Link,
  Switch,
  Redirect,
  useLocation
} from "react-router-dom";

// 可以将<Switch>中的最后一个<Route>
// 用作一种“后备”路由,以捕获404错误
//
// 关于本示例的有一些有用的注意事项:
//
// -<Switch>渲染与之匹配的第一个子元素<Route>
// -<Redirect>可用于将旧的URL重定向到新的URL
// -<Route path ="*">将始终匹配

export default function NoMatchExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/old-match">Old Match, to be redirected</Link>
          </li>
          <li>
            <Link to="/will-match">Will Match</Link>
          </li>
          <li>
            <Link to="/will-not-match">Will Not Match</Link>
          </li>
          <li>
            <Link to="/also/will/not/match">Also Will Not Match</Link>
          </li>
        </ul>

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/old-match">
            <Redirect to="/will-match" />
          </Route>
          <Route path="/will-match">
            <WillMatch />
          </Route>
          <Route path="*">
            <NoMatch />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h3>Home</h3>;
}

function WillMatch() {
  return <h3>Matched!</h3>;
}

function NoMatch() {
  let location = useLocation();

  return (
    <div>
      <h3>
        No match for <code>{location.pathname}</code>
      </h3>
    </div>
  );
}


2.2.8 递归路径-Recursive Paths

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect,
  useParams,
  useRouteMatch
} from "react-router-dom";

// 有时我们不知道应用所有可能的路径;
// 例如,在构建文件浏览系统UI或基于数据动态确定URL时。
// 在这些情况下,拥有能够在运行时根据需要生成路由的动态路由会有所帮助。
//
// 在此示例中你可以递归地向下拉取好友列表,
// 并一路查看每个用户的好友列表。
// 向下拉取时,请注意每个语段都已添加到URL。
// 可以将此链接复制/粘贴给其他人,他们将看到相同的UI。
//
// 然后单击“后退”按钮,观察URL的最后一个片语段
// 以及最后一个好友列表消失了。

export default function RecursiveExample() {
  return (
    <Router>
      <Switch>
        <Route path="/:id">
          <Person />
        </Route>
        <Route path="/">
          <Redirect to="/0" />
        </Route>
      </Switch>
    </Router>
  );
}

function Person() {
  let { url } = useRouteMatch();
  let { id } = useParams();
  let person = find(parseInt(id));

  return (
    <div>
      <h3>{person.name}’s Friends</h3>

      <ul>
        {person.friends.map(id => (
          <li key={id}>
            <Link to={`${url}/${id}`}>{find(id).name}</Link>
          </li>
        ))}
      </ul>

      <Switch>
        <Route path={`${url}/:id`}>
          <Person />
        </Route>
      </Switch>
    </div>
  );
}

const PEEPS = [
  { id: 0, name: "Michelle", friends: [1, 2, 3] },
  { id: 1, name: "Sean", friends: [0, 3] },
  { id: 2, name: "Kim", friends: [0, 1, 3] },
  { id: 3, name: "David", friends: [1, 2] }
];

function find(id) {
  return PEEPS.find(p => p.id === id);
}


2.2.9 侧边栏-Sidebar

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

// 每个逻辑“路由”都有两个组件,
// 一个用于侧边栏,一个用于主区域。
// 当路径与当前URL匹配时,我们希望将它们都渲染在不同的位置。

// 我们会在两个地方使用这一路由配置:一次用于边栏,一次在主要内容部分。
// 所有路由的顺序与它们在的<Switch>中出现的顺序相同。
const routes = [
  {
    path: "/",
    exact: true,
    sidebar: () => <div>home!</div>,
    main: () => <h2>Home</h2>
  },
  {
    path: "/bubblegum",
    sidebar: () => <div>bubblegum!</div>,
    main: () => <h2>Bubblegum</h2>
  },
  {
    path: "/shoelaces",
    sidebar: () => <div>shoelaces!</div>,
    main: () => <h2>Shoelaces</h2>
  }
];

export default function SidebarExample() {
  return (
    <Router>
      <div style={{ display: "flex" }}>
        <div
          style={{
            padding: "10px",
            width: "40%",
            background: "#f0f0f0"
          }}
        >
          <ul style={{ listStyleType: "none", padding: 0 }}>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/bubblegum">Bubblegum</Link>
            </li>
            <li>
              <Link to="/shoelaces">Shoelaces</Link>
            </li>
          </ul>

          <Switch>
            {routes.map((route, index) => (
              // 你可以在应用中的任意多个位置渲染<Route>。 
              // 它也会与URL匹配的任何其它<Route>一起渲染。
              // 因此,侧栏、面包屑或其它需要在同一URL的多个位置
              // 渲染的多项内容的也就是多个<Route>而已。
              <Route
                key={index}
                path={route.path}
                exact={route.exact}
                children={<route.sidebar />}
              />
            ))}
          </Switch>
        </div>

        <div style={{ flex: 1, padding: "10px" }}>
          <Switch>
            {routes.map((route, index) => (
              // 使用与上述相同路径渲染更多的<Route>,
              // 但是这次使用不同的组件。
              <Route
                key={index}
                path={route.path}
                exact={route.exact}
                children={<route.main />}
              />
            ))}
          </Switch>
        </div>
      </div>
    </Router>
  );
}


2.2.10 过渡动画-Animated Transitions

import "./packages/react-router-dom/examples/Animation/styles.css";

import React from "react";
import {
  TransitionGroup,
  CSSTransition
} from "react-transition-group";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect,
  useLocation,
  useParams
} from "react-router-dom";

export default function AnimationExample() {
  return (
    <Router>
      <Switch>
        <Route exact path="/">
          <Redirect to="/hsl/10/90/50" />
        </Route>
        <Route path="*">
          <AnimationApp />
        </Route>
      </Switch>
    </Router>
  );
}

function AnimationApp() {
  let location = useLocation();

  return (
    <div style={styles.fill}>
      <ul style={styles.nav}>
        <NavLink to="/hsl/10/90/50">Red</NavLink>
        <NavLink to="/hsl/120/100/40">Green</NavLink>
        <NavLink to="/rgb/33/150/243">Blue</NavLink>
        <NavLink to="/rgb/240/98/146">Pink</NavLink>
      </ul>

      <div style={styles.content}>
        <TransitionGroup>
          {/*
            这与<CSSTransition>的其它用法没有什么不同,
            只需确保将`location`传递给`Switch`即可,
            以便它可以在动画过程中匹配旧位置。
          */}
          <CSSTransition
            key={location.key}
            classNames="fade"
            timeout={300}
          >
            <Switch location={location}>
              <Route path="/hsl/:h/:s/:l" children={<HSL />} />
              <Route path="/rgb/:r/:g/:b" children={<RGB />} />
            </Switch>
          </CSSTransition>
        </TransitionGroup>
      </div>
    </div>
  );
}

function NavLink(props) {
  return (
    <li style={styles.navItem}>
      <Link {...props} style={{ color: "inherit" }} />
    </li>
  );
}

function HSL() {
  let { h, s, l } = useParams();

  return (
    <div
      style={{
        ...styles.fill,
        ...styles.hsl,
        background: `hsl(${h}, ${s}%, ${l}%)`
      }}
    >
      hsl({h}, {s}%, {l}%)
    </div>
  );
}

function RGB() {
  let { r, g, b } = useParams();

  return (
    <div
      style={{
        ...styles.fill,
        ...styles.rgb,
        background: `rgb(${r}, ${g}, ${b})`
      }}
    >
      rgb({r}, {g}, {b})
    </div>
  );
}

const styles = {};

styles.fill = {
  position: "absolute",
  left: 0,
  right: 0,
  top: 0,
  bottom: 0
};

styles.content = {
  ...styles.fill,
  top: "40px",
  textAlign: "center"
};

styles.nav = {
  padding: 0,
  margin: 0,
  position: "absolute",
  top: 0,
  height: "40px",
  width: "100%",
  display: "flex"
};

styles.navItem = {
  textAlign: "center",
  flex: 1,
  listStyleType: "none",
  padding: "10px"
};

styles.hsl = {
  ...styles.fill,
  color: "white",
  paddingTop: "20px",
  fontSize: "30px"
};

styles.rgb = {
  ...styles.fill,
  color: "white",
  paddingTop: "20px",
  fontSize: "30px"
};


2.2.11 路由配置-Route Config

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

// 有些人在集中式路由配置中发现价值。
// 路由配置只是数据,React非常擅长将数据映射到组件中,而<Route>是组件。

// 我们的路由配置只是一个带有"path"和"component"属性的逻辑“路由”数组,
// 其顺序与在`<Switch>`内部的方式相同。
const routes = [
  {
    path: "/sandwiches",
    component: Sandwiches
  },
  {
    path: "/tacos",
    component: Tacos,
    routes: [
      {
        path: "/tacos/bus",
        component: Bus
      },
      {
        path: "/tacos/cart",
        component: Cart
      }
    ]
  }
];

export default function RouteConfigExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/tacos">Tacos</Link>
          </li>
          <li>
            <Link to="/sandwiches">Sandwiches</Link>
          </li>
        </ul>

        <Switch>
          {routes.map((route, i) => (
            <RouteWithSubRoutes key={i} {...route} />
          ))}
        </Switch>
      </div>
    </Router>
  );
}

// <Route>的特殊包装器,
// 通过将"sub"路由传递给`routes`属性传递给它渲染的组件,
// 从而知道如何处理"sub"路由。
function RouteWithSubRoutes(route) {
  return (
    <Route
      path={route.path}
      render={props => (
        // pass the sub-routes down to keep nesting
        <route.component {...props} routes={route.routes} />
      )}
    />
  );
}

function Sandwiches() {
  return <h2>Sandwiches</h2>;
}

function Tacos({ routes }) {
  return (
    <div>
      <h2>Tacos</h2>
      <ul>
        <li>
          <Link to="/tacos/bus">Bus</Link>
        </li>
        <li>
          <Link to="/tacos/cart">Cart</Link>
        </li>
      </ul>

      <Switch>
        {routes.map((route, i) => (
          <RouteWithSubRoutes key={i} {...route} />
        ))}
      </Switch>
    </div>
  );
}

function Bus() {
  return <h3>Bus</h3>;
}

function Cart() {
  return <h3>Cart</h3>;
}


import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useHistory,
  useLocation,
  useParams
} from "react-router-dom";

// 在此示例中,如何在同一URL上渲染两个不同的屏幕显示(或在不同上下文中的同一屏幕),
// 具体取决于你到达那里的方式。
//
// 单击“featured images”,并全屏查看。
// 然后“visit the gallery”并单击颜色。 
// 请注意,URL和组件与以前相同,但是现在我们可以在图库屏幕顶部的模态中看到它们。

export default function ModalGalleryExample() {
  return (
    <Router>
      <ModalSwitch />
    </Router>
  );
}

function ModalSwitch() {
  let location = useLocation();

  //当单击图库链接之一时,将设置此状态。`background`状态是单击图库链接时我们所处的位置。
  // 如果存在,则将其用作<Switch>的位置,以便在模态后台显示图库。
  let background = location.state && location.state.background;

  return (
    <div>
      <Switch location={background || location}>
        <Route exact path="/" children={<Home />} />
        <Route path="/gallery" children={<Gallery />} />
        <Route path="/img/:id" children={<ImageView />} />
      </Switch>

      {/* 当`background`页已设置时,则显示模态框 */}
      {background && <Route path="/img/:id" children={<Modal />} />}
    </div>
  );
}

const IMAGES = [
  { id: 0, title: "Dark Orchid", color: "DarkOrchid" },
  { id: 1, title: "Lime Green", color: "LimeGreen" },
  { id: 2, title: "Tomato", color: "Tomato" },
  { id: 3, title: "Seven Ate Nine", color: "#789" },
  { id: 4, title: "Crimson", color: "Crimson" }
];

function Thumbnail({ color }) {
  return (
    <div
      style={{
        width: 50,
        height: 50,
        background: color
      }}
    />
  );
}

function Image({ color }) {
  return (
    <div
      style={{
        width: "100%",
        height: 400,
        background: color
      }}
    />
  );
}

function Home() {
  return (
    <div>
      <Link to="/gallery">Visit the Gallery</Link>
      <h2>Featured Images</h2>
      <ul>
        <li>
          <Link to="/img/2">Tomato</Link>
        </li>
        <li>
          <Link to="/img/4">Crimson</Link>
        </li>
      </ul>
    </div>
  );
}

function Gallery() {
  let location = useLocation();

  return (
    <div>
      {IMAGES.map(i => (
        <Link
          key={i.id}
          to={{
            pathname: `/img/${i.id}`,
            // This is the trick! This link sets
            // the `background` in location state.
            state: { background: location }
          }}
        >
          <Thumbnail color={i.color} />
          <p>{i.title}</p>
        </Link>
      ))}
    </div>
  );
}

function ImageView() {
  let { id } = useParams();
  let image = IMAGES[parseInt(id, 10)];

  if (!image) return <div>Image not found</div>;

  return (
    <div>
      <h1>{image.title}</h1>
      <Image color={image.color} />
    </div>
  );
}

function Modal() {
  let history = useHistory();
  let { id } = useParams();
  let image = IMAGES[parseInt(id, 10)];

  if (!image) return null;

  let back = e => {
    e.stopPropagation();
    history.goBack();
  };

  return (
    <div
      onClick={back}
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        bottom: 0,
        right: 0,
        background: "rgba(0, 0, 0, 0.15)"
      }}
    >
      <div
        className="modal"
        style={{
          position: "absolute",
          background: "#fff",
          top: 25,
          left: "10%",
          right: "10%",
          padding: 15,
          border: "2px solid #444"
        }}
      >
        <h1>{image.title}</h1>
        <Image color={image.color} />
        <button type="button" onClick={back}>
          Close
        </button>
      </div>
    </div>
  );
}


2.2.13 静态路由内容-StaticRouter Context

import React, { Component } from "react";
import { StaticRouter as Router, Route } from "react-router-dom";

// 本示例在StaticRouter中渲染一条路由,
// 并填充其staticContext,然后将其打印出来。
// 在实际工作中,会使用StaticRouter进行服务器端渲染:
//
// import express from "express";
// import ReactDOMServer from "react-dom/server";
//
// const app = express()
//
// app.get('*', (req, res) => {
//   let staticContext = {}
//
//   let html = ReactDOMServer.renderToString(
//     <StaticRouter location={req.url} context={staticContext}>
//       <App /> (includes the RouteStatus component below e.g. for 404 errors)
//     </StaticRouter>
//   );
//
//   res.status(staticContext.statusCode || 200).send(html);
// });
//
// app.listen(process.env.PORT || 3000);

function RouteStatus(props) {
  return (
    <Route
      render={({ staticContext }) => {
        // 必须检查staticContext是否存在,
        // 因为如果通过BrowserRouter渲染,它会是`undefined`
        if (staticContext) {
          staticContext.statusCode = props.statusCode;
        }

        return <div>{props.children}</div>;
      }}
    />
  );
}

function PrintContext(props) {
  return <p>Static context: {JSON.stringify(props.staticContext)}</p>;
}

export default class StaticRouterExample extends Component {
  // 这是我们传递给StaticRouter的上下文对象。
  // 可以通过路由对其进行修改,以提供服务端渲染的其他信息。
  staticContext = {};

  render() {
    return (
      <Router location="/foo" context={this.staticContext}>
        <div>
          <RouteStatus statusCode={404}>
            <p>Route with statusCode 404</p>
            <PrintContext staticContext={this.staticContext} />
          </RouteStatus>
        </div>
      </Router>
    );
  }
}


2.2.14 查询参数-Query Parameters

import React from "react";
import {
  BrowserRouter as Router,
  Link,
  useLocation
} from "react-router-dom";

// React Router于解析URL查询字符串没有任何问题。
//
// 如果使用简单的 `key=value` 形式的查询字符串,
// 并且不需要支持IE 11,则可以使用浏览器的内置URLSearchParams API。
//
// 如果查询字符串包含数组或对象语法,
// 则可能需要带上自己的查询解析函数。

export default function QueryParamsExample() {
  return (
    <Router>
      <QueryParamsDemo />
    </Router>
  );
}

// 一个基于useLocation的自定义钩子,可用于解析查询字符串。
function useQuery() {
  return new URLSearchParams(useLocation().search);
}

function QueryParamsDemo() {
  let query = useQuery();

  return (
    <div>
      <div>
        <h2>Accounts</h2>
        <ul>
          <li>
            <Link to="/account?name=netflix">Netflix</Link>
          </li>
          <li>
            <Link to="/account?name=zillow-group">Zillow Group</Link>
          </li>
          <li>
            <Link to="/account?name=yahoo">Yahoo</Link>
          </li>
          <li>
            <Link to="/account?name=modus-create">Modus Create</Link>
          </li>
        </ul>

        <Child name={query.get("name")} />
      </div>
    </div>
  );
}

function Child({ name }) {
  return (
    <div>
      {name ? (
        <h3>
          The <code>name</code> in the query string is "{name}
          "
        </h3>
      ) : (
        <h3>There is no name in the query string</h3>
      )}
    </div>
  );
}


2.3 API


2.3.1 钩子-Hooks

React Router中的钩子用于通过它们访问路由state并在组件内部执行导航。react-router-dom中的相关钩子与核心模块(react-router)中的钩子完全一致,请通过点击下方链接查看相关说明:

请注意:要使用这些钩子,需要React >= 16.8


2.3.2 <BrowserRouter>

使用HTML5历史(history)API(pushStatereplaceStatepopstate事件)的<Router>,用于保持UI与URL同步。

<BrowserRouter
  basename={optionalString}
  forceRefresh={optionalBool}
  getUserConfirmation={optionalFunc}
  keyLength={optionalNumber}
>
  <App />
</BrowserRouter>


basename: string

所有位置(location)的基本URL。如果你的应用是通过服务器上的子目录提供的,则需要将其设置为子目录。正确的基本名称格式应以斜杠(/)开头,但不能以斜杠结尾。

<BrowserRouter basename="/calendar" />
<Link to="/today"/> // renders <a href="/calendar/today">


getUserConfirmation: func

用于确认导航的函数。默认使用:window.confirm

<BrowserRouter
  getUserConfirmation={(message, callback) => {
    // this is the default behavior
    const allowTransition = window.confirm(message);
    callback(allowTransition);
  }}
/>


forceRefresh: bool

如果为true,则路由将在页面导航中使用整页刷新。你可以通过它来模仿传统的服务端渲染应用,在页面导航之间刷新整个页面的方式。

<BrowserRouter forceRefresh={true} />


keyLength: number

location.key的长度。默认为:6

<BrowserRouter keyLength={12} />


children: node

要渲染的子元素。

注意:用于React <16时,必须使用单个子元素,因为render方法不能返回多个元素。如果需要多个元素,可以尝试将它们包装在额外的<div>中。


2.3.3 <HashRouter>

使用URL哈希部分的(如:window.location.hash)的<Router>,用于保持UI与URL同步。

注意:哈希history不支持location.keylocation.state。在以前的版本中,React Router尝试对其行为进行统一化处理,但会存在一些无法解决的极端情况。任何需要此功能的代码或插件都无法使用。由于此技术仅旨在支持旧版浏览器,因此建议配置服务端并使用<BrowserHistory>。

<HashRouter
  basename={optionalString}
  getUserConfirmation={optionalFunc}
  hashType={optionalString}
>
  <App />
</HashRouter>>


basename: string

用于所有location的基本URL。正确格式的基本名称应以斜杠(/)开头,但不能以斜杠结尾。

<HashRouter basename="/calendar"/>
<Link to="/today"/> // renders <a href="#/calendar/today">


getUserConfirmation: func

用于确认导航的函数。默认使用:window.confirm

<HashRouter
  getUserConfirmation={(message, callback) => {
    // this is the default behavior
    const allowTransition = window.confirm(message);
    callback(allowTransition);
  }}
/>


hashType: string

用于window.location.hash的编码类型。默认:"slash" 。有效值为:

  • "slash" - 创建的哈希类似于:#/ and #/sunshine/lollipops
  • "noslash" - 创建的哈希类似于:# and #sunshine/lollipops
  • "hashbang" - 创建 “ajax crawlable” (Google弃用) 形式的哈希,类似:#!/#!/sunshine/lollipops


children: node

要渲染的单个子元素


用于应用的声明式、可访问的导航。

<Link to="/about">About</Link>


链接位置的字符串表示形式。其中包含:位置的路径名、查询字符串和哈希属性。

<Link to="/courses?sort=name">About</Link>


链接位置对象表示形式,可以包含以下任何属性:

  • pathname: {string},表示要链接到的路径
  • search: {string},表示查询参数
  • hash: {string},要添加到URL的哈希部分,如:#a-hash
  • state: {object},要持外化到location的状态
<Link
  to={{
    pathname: "/courses",
    search: "?sort=name",
    hash: "#the-hash",
    state: { fromDashboard: true }
  }}
/>


获取链接位置的函数。其接收当前位置(location)参数,并应以字符串或对象的形式返回新位置表示形式。

<Link to={location => ({ ...location, pathname: "/courses" })} />
<Link to={location => `${location.pathname}?sort=name`} />


如果为true,则单击链接将替换历史堆栈中的当前条目,而不是添加新条目。

<Link to="/courses" replace />


从React Router 5.1开始,如果使用的是React 16,则不再需要此属性,因为React Router会将ref转发到基础<a>标签。改用普通ref

允许访问组件的基础引用。

<Link
  to="/"
  innerRef={node => {
    // `node` refers to the mounted DOM element
    // or null when unmounted
  }}
/>


从React Router 5.1开始,如果使用的是React 16,则不再需要此属性,因为React Router会将ref转发到基础<a>标签。改用普通ref

使用React.createRef获取组件的基础引用。

let anchorRef = React.createRef()

<Link to="/" innerRef={anchorRef} />


还可以传入想要在<a>上显示的属性,如titleidclassName等。


<Link>的特殊版本,当它与当前URL匹配时,会为渲染的元素添加指定的样式属性。

<NavLink to="/about">About</NavLink>


元素处于活动状态时使用的CSS类。默认为:active。这会一起添加到className属性。

<NavLink to="/faq" activeClassName="selected">
  FAQs
</NavLink>


元素处于活动状态时使用的样式表。

<NavLink
  to="/faq"
  activeStyle={{
    fontWeight: "bold",
    color: "red"
  }}
>
  FAQs
</NavLink>


是否完全匹配。如果为true,则仅在位置完全匹配时才应用活动的类/样式。

<NavLink exact to="/profile">
  Profile
</NavLink>


是否严格匹配。确定位置是否与当前URL匹配时,将考虑位置路径名上的斜杠。更多相关信息,请参见<Route strict>文档。

<NavLink strict to="/events/">
  Events
</NavLink>


额外逻辑,一个用以确定链接是否处于活动状态的函数。如果不仅仅是验证链接的路径名是否与当前URL的路径名匹配,可使用此选项。

<NavLink
  to="/events/123"
  isActive={(match, location) => {
    if (!match) {
      return false;
    }

    // only consider an event active if its event id is an odd number
    const eventID = parseInt(match.params.eventID);
    return !isNaN(eventID) && eventID % 2 === 1;
  }}
>
  Event 123
</NavLink>


isActive比较的是当前历史位置(通常是当前浏览器URL)。要与其他位置进行比较,可以传递一个location


活动链接上使用的aria-current属性的值。默认为:"page"。可选值为:

  • "page" - 用于指示一组分页链接中的链接
  • "step" - 用于指示基于分步处理的步骤指示器中的链接
  • "location" - 用于指示视觉上辨识的显示图像作为流程图的当前组成部分
  • "date" - 用于指示日历中的当前日期
  • "time" - 用于指示时间表中的当前时间
  • "true" - 用于指示NavLink是否处于活动状态

基于:WAI-ARIA 1.1规范


2.3.6 <Prompt>

请参考:Core-API-<Prompt>


2.3.7 <MemoryRouter>

一个将“URL”的历史记录(history)保存在内存中(不读取或写入地址栏)<Router>。在测试和非浏览器环境(如:React Native)中很有用。

本部分与核心模块(react-router)中的<MemoryRouter>一致,请点击下方链接参考相关内容:


2.3.8 <Redirect>

渲染一个<Redirect>将导航到的新位置(location)。新location将覆盖历史堆栈中的当前location,类似于服务器端重定向(HTTP 3xx)。

本部分与核心模块(react-router)中的<Redirect>一致,请点击下方链接参考相关内容:


2.3.9 <Route>

<Route>组件可能是React Router中需要了解和使用的最重要组件。它的最基本职责是在路径与当前URL匹配时渲染UI。

本部分与核心模块(react-router)中的<Route>一致,请点击下方链接参考相关内容:


2.3.10 <Router>

<Router>是所有路由组件的通用底层接口。本部分与核心模块(react-router)中的<Router>一致,请点击下方链接参考相关内容:


2.3.11 <StaticRouter>

静态的<Router>,其永远不会修改location

本部分与核心模块(react-router)中的<StaticRouter>一致,请点击下方链接参考相关内容:


2.3.12 <Switch>

渲染与location相匹配的第一个<Route><Redirect>子元素。

本部分与核心模块(react-router)中的<Switch>一致,请点击下方链接参考相关内容:


2.3.13 history

请参考:Core-API-<history>


2.3.14 location

请参考:Core-API-<location>


2.3.15 match

请参考:Core-API-<match>


2.3.16 matchPath

matchPath是一个函数,使你可以使用与<Route>相同的匹配代码,但不在正常渲染周期之内,如:在服务器上渲染之前收集数据依赖项。

import { matchPath } from "react-router";

const match = matchPath("/users/123", {
  path: "/users/:id",
  exact: true,
  strict: false
});

该函数接受以下两个参数:

  • pathname:要匹配的路径名。如果在Node.js的服务器上使用,则为req.path
  • props:要匹配的属性,与Route所接受的匹配属性相同。它也可以是字符串或字符串数组,作为{ path }的便捷形式:
    {
      path, // like /users/:id; either a single string or an array of strings
      strict, // optional, defaults to false
      exact, // optional, defaults to false
    }
返回值

当提供的路径名与路径属性匹配时,会返回一个对象:

matchPath("/users/2", {
  path: "/users/:id",
  exact: true,
  strict: true
});

//  {
//    isExact: true
//    params: {
//        id: "2"
//    }
//    path: "/users/:id"
//    url: "/users/2"
//  }

所提供的路径名与路径属性不匹配,则返回null

matchPath("/users", {
  path: "/users/:id",
  exact: true,
  strict: true
});

//  null


2.3.17 withRouter

withRouter一个访问history对象属性和最接近<Route>match项的高阶组件。

本部分与核心模块(react-router)中的<withRouter>一致,请点击下方链接参考相关内容: