React Router 官方文档翻译

3,300 阅读34分钟

版本:最新版 5.2.0

React Router是React的官方路由库,可以用于web端,node.js服务端,和React Native。React Router是一个React组件、hooks和工具函数的集合。

React Router包由三部分组成:

  • react-router:包含了React Router的大多数核心功能,包括路由匹配算法和大多数核心组件以及hooks。
  • react-router-dom:除了react-router的内容之外,还添加了一些DOM相关的API,如<BrowserRouter>, <HashRouter>, <Link>等。
  • react-router-native:除了react-router的内容之外,还添加了一些React Native相关的API,如<NativeRouter>和native版的<Link>

当安装react-router-dom 和 react-router-native时,会自动包含react-router作为依赖,并且还会自动导出react-router里的所有东西。所以使用时只需要安装react-router-dom或react-router-native即可,不要直接使用react-router。

总之,对于web浏览器(包括服务端渲染),使用react-router-dom;对于React Native app,使用react-router-native。

以下文档主要针对浏览器端,即react-router-dom。本文是以实用为导向的意译为主,对于一些无关紧要的部分可能会一语带过。

指南

快速开始

我们推荐使用Create React App来创建React应用,然后再使用React Router。

首先,安装create-react-app,然后创建一个新项目:

npx create-react-app demo-app
cd demo-app

安装

你可以使用npm或yarn安装React Router,由于我们要创建web app,因此需要使用react-router-dom: yarn add react-router-dom

下面是两个示例,可以复制到src/App.js

示例一:基础路由

这个例子中,有三个页面需要路由器处理,当你点击不同的<Link>,路由器会渲染匹配的<Route>。一个<Link>最终会被渲染成一个带有href的<a>标签,以便让那些使用键盘导航和屏幕阅读器的人也能使用这个应用。

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>

        {/* <Switch>会遍历所有子<Route>,然后渲染第一个成功匹配到当前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>;
}

示例二:嵌套路由

这个示例展示了如何嵌套路由,/topics路由会加载Topics组件,Topics组件会根据当前路径的: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>

      {/* Topics 页面有自己的 <Switch>,包含更多建立在 /topics URL路径上的路由。
      	  你可以将这里的第二个<Route>视为所有topics的索引页,或者也可以看做没有topic被选中时展示的页面。 */}
      <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>;
}

基础组件

React Router中包含三种基础类型的组件:

  • 路由器(routers), 如 <BrowserRouter><HashRouter>
  • 路由匹配组件(route matchers), 如 <Route><Switch>
  • 路由导航组件(navigation/route changers), 如 <Link>, <NavLink>, 和 <Redirect>

所有这些组件在使用时都从react-router-dom中引入:

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

Routers

router是React Router的核心组件。web项目可以使用<BrowserRouter><HashRouter>。两者主要区别在于保存URL和与服务器通信的方式不同:

  • <BrowserRouter>使用常规的URL路径,看起来比较美观,但是需要服务器正确设置。具体来讲,就是需要你的web服务器对客户端所有URL都提供同一个页面。Create React App在开发模式下已经设置好了,生产环境服务端的设置可以查看这篇文档
  • <HashRouter>将当前的位置(location)存储在URL的hash部分,所以URL看起来会像这样:http://example.com/#/your/page。由于hash不会发送到服务器,所以服务器不需要做额外的配置。

使用一个Router时,需要将其渲染在根元素,通常会将顶级的<APP>组件包裹在Router中,形如:

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")
);

路由匹配组件

有两个路由匹配组件:Switch 和 Route。当渲染一个<Switch>时,它会遍历所有子组件,也就是<Route>组件,找到第一个path匹配当前URL的组件就会停止查找,然后渲染这个<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>
        {/* 如果当前URL是/about, 这个路由会被渲染,剩下的都会被忽略 */}
        <Route path="/about">
          <About />
        </Route>

        {/* 注意这两个路由的顺序,更具体的path="/contact/:id"排在了path="/contact"的前面 */}
        <Route path="/contact/:id">
          <Contact />
        </Route>
        <Route path="/contact">
          <AllContacts />
        </Route>

        {/* 如果以上任何路由都不匹配,这个路由就来兜底。
        重要提示:path="/"的路由总是会匹配当前URL,因为所有URL都是以一个/开始 */}
        <Route path="/">
          <Home />
        </Route>
      </Switch>
    </div>
  );
}

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

很重要的一点是,一个<Route path>总是匹配URL的开头,而不是整个URL,因此<Route path="/">总是会匹配URL。鉴于此,通常会将<Route path="/">放在<Switch>的最后。可以使用<Route exact path="/">来匹配完整URL。

注意:尽管React Router支持在<Switch>之外渲染<Route>组件,但是5.1版本开始我们推荐你使用useRouteMatch这个hook作为替代。此外,我们也不推荐渲染一个没有path的<Route>,而是使用hook来访问你需要的变量。

导航组件(改变路由的组件)

React Router提供一个<Link>组件来创建导航链接。当渲染一个<Link>时,实际上会在HTML中渲染一个<a>标签。

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

<NavLink>是一个特殊的<Link>,只不过可以定义激活时的样式。

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

// 当 URL 是 /react时, 渲染结果:
// <a href="/react" className="hurray">React</a>

// 当 URL 是别的时:
// <a href="/react">React</a>

任何时候你想使用强制导航,都可以渲染一个<Redirect>。当一个<Redirect>被渲染时,将会导航到它的to属性定义的位置:

<Redirect to="/login" />

服务端渲染

在服务端渲染稍微有些不同,因为它是无状态的。最基本的思路是将app包裹在一个无状态的<StaticRouter>组件中。传递来自服务端的请求url好让路由可以匹配,再传递一个context。

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

当你在客户端渲染一个<Redirect>,浏览器中的history通过改变state来刷新页面。在一个静态的服务端环境我们无法改变app的 state。相反,我们使用context prop来找出渲染的结果是什么。如果发现context.url存在,就说明app需要重定向,然后我们可以从服务端发送一个合适的重定向。

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

if (context.url) {
  // 某个地方渲染了一个 `<Redirect>`
  redirect(301, context.url);
} else {
  // we're good, send the response
}

添加app中具体的context信息

StaticRouter只会添加context.url。但你也许希望一些重定向为301,一些为302。或者,如果一些特定分支的UI被渲染,你会想发送一个404响应。又或者他们没有权限时发送一个401。context prop 是你的,你可以修改。这里提供了一种方法来区分301和302重定向:

function RedirectWithStatus({ from, to, status }) {
  return (
    <Route
      render={({ staticContext }) => {
        // 客户端可没有 `staticContext`,所以要判断一下
        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) {
  // 可以使用在 RedirectWithStatus 中添加的 `context.status`
  redirect(context.status, context.url);
}

404, 401, 或任何其他状态

我们可以继续像上面那样做。创建一个组件,添加一些context,并把这个组件渲染到app中的任何地方来获取不同的状态码。

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

现在你可以在app中任何你希望添加code到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>
  );
}

把他们放一起

这不是一个真实的app,但它展示了所有通用的需要放在一起的部分。

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可以很好地适应你的项目中用到的方法。

最基本的要求是需要在渲染前加载数据。React Router导出了matchPath这个静态方法,这个方法用来在内部匹配location到路由组件。你可以在服务端使用这个方法来明确在渲染前你的数据依赖将会是什么。

这个方法的要点是依赖一个静态路由配置用来渲染路由组件以及通过匹配来确定渲染前的数据依赖。

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

然后使用这个配置来渲染路由:

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

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

然后在服务端,你可能会需要这么做:

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

// 请求内部
const promises = [];
// 使用 `some` 来模仿 `<Switch>` 只会选择第一个匹配成功的路由的行为
routes.some(route => {
  // 这里使用 `matchPath` 
  const match = matchPath(req.path, route);
  if (match) promises.push(route.loadData(match));
  return match;
});

Promise.all(promises).then(data => {
  // 提供一些数据来让浏览器端在渲染的时候能获取
});

最后,浏览器端将需要选出数据。以上都是你需要实现的要点。

你也可以使用 React Router Config 包来协助加载数据以及服务端使用静态路由配置来渲染。

代码分割

代码分割使得用户不需要一次性下载整个应用,可以增量渐进下载。为了实现代码分割,我们使用webpack, @babel/plugin-syntax-dynamic-import, 以及 loadable-components

webpack内置支持动态的import();但是,如果你在使用Babel,你就需要使用@babel/plugin-syntax-dynamic-import插件。这是一个只使用语法的插件,这意味这Babel不会做任何额外的转换。这个插件只是简单地允许Babel解析动态import,好让webpack可以用代码分割的方式打包他们。你的.babelrc文件可能看起来像这样:

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

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是一个占位的组件,在真正的组件显示前会一直显示。查看完整文档

滚动恢复

在早期版本的React Router中,我们就对滚动恢复提供了开箱即用的支持。 浏览器开始使用自己的history.pushState处理滚动恢复,其处理方式和普通浏览器导航时的处理方式相同。chrome早就支持了,而且支持的很好。 由于浏览器开始处理“默认情况”,应用也有不同的滚动需求,所以我们没有提供默认的滚动管理。

滚到顶部

当导航到一个很长的网页并且停留在页面底部时,需要回到顶部:

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;
}

类的写法:

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);

然后在app上面渲染:

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

如果页面是通过tab切换路由的,当切换tab时希望页面能够停留在原先的位置,而不是滚到顶部:

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>
  );
}

类的写法:

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>
    );
  }
}

通用解决方案

有两个要点:

  • 导航到新页面时不要停留在底部,要滚到顶部
  • 点击“后退”和“前进”按钮(而不是点击链接)来恢复到窗口和溢出元素的滚动位置

我们希望提供一个统一的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组件mount时,他们可以在sessionsStorage中查找滚动位置。

棘手的部分是需要为不希望管理窗口滚动的情况定义一个“退出” API。例如,如果你有一些导航的tab悬浮在页面内容中,你或许不想滚到顶部,否则tab就看不到了。

当我们知道了现在Chrome为我们管理了滚动位置,明白了不同app有着不同的滚动需求,我们有点失去了我们需要提供一些东西的信念——特别是当人们只是想滚动到顶部时(你可以直接自行添加)。

此外,我们不想再做这部分工作了。

哲学

这部分解释了我们为什么使用React Router,我们称它为“动态路由”,这不同于你也许更为熟悉的“静态路由”。

静态路由(Static Routing)

如果你用过Rails, Express, Ember, Angular等,那你就用过静态路由了。在这些框架中,你将你的路由声明为app初始化的一部分,在任何渲染发生之前。React Router v4之前大部分都是静态的。让我们看看express是怎么配置路由的:

// Express 风格的 routing:
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();

留意在app listen前路由是怎么被声明的。客户端使用的路由器也类似,在Angular中你先声明你的路由,然后在渲染前import到顶级的AppModule中。

// Angular 风格的 routing:
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有一个约定的routes.js文件供构建工具读取和import进应用中。这些也是在渲染前发生的:

// Ember 风格的 Router:
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版本之前都是这么做的,v4后就不这么做了。

背后的故事

坦白说,我们对React Router从v2版本后采取的方向感到非常沮丧,我们被API限制了,意识到我们正在重新实现React(例如生命周期等),这和React提供给我们的组合UI的思维模型背道而驰。

我们仅用几个小时的开发时间证实了我们未来想要发展的方向。我们最终得到的API并不是脱离于React的,而是React的一部分或者说自然地融入其中的。我想你们应该会喜欢的。

动态路由

当我们在说动态路由的时候,我们实际上在说的是在渲染阶段的路由,不是脱离于一个运行中app的配置。这意味着React Router中几乎所有东西都是一个组件。这里是一个API的快速预览来看看它是怎么工作的:

首先,获取一个目标环境的Router组件,渲染在app外层:

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

// react-dom (what we'll use here)
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组件来展示UI:

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

Route组件将会渲染 <Dashboard {...props}/>,这里的props是一些Router相关的例如{ match, location, history }。如果当前路径不在/dashboard,那么Route将什么都不渲染。

嵌套路由

许多路由器都有一些“嵌套路由”的概念。React Router直到v4版本也有这个概念。当你从一个静态路由配置迁移到动态路由,你怎么“嵌套路由”?很简单,由于Route仅仅是一个组件,所以你可以像嵌套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>
    {/* 这里是一个嵌套的 Route,
        match.url 帮助我们生成一个相对路径 */}
    <Route path={match.url + "/carnitas"} component={Carnitas} />
  </div>
);

响应式路由

想象这样一个场景:在小屏幕上,只显示右侧导航栏;在大屏幕上,显示导航栏和详情信息。这个场景在用户旋转手机从竖屏到横屏时很常见。如果是静态路由,并没有一个真正的解决方案,但是动态路由就好办了:

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

const Invoices = () => (
  <Layout>
    {/* always show the nav */}
    <InvoicesNav />

    <Media query={PRETTY_SMALL}>
      {screenIsSmall =>
        screenIsSmall ? (
          // small screen has no redirect
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
          </Switch>
        ) : (
          // large screen does!
          <Switch>
            <Route
              exact
              path="/invoices/dashboard"
              component={Dashboard}
            />
            <Route path="/invoices/:id" component={Invoice} />
            <Redirect from="/invoices" to="/invoices/dashboard" />
          </Switch>
        )
      }
    </Media>
  </Layout>
);

当用户旋转手机到横屏时,这几行代码将会自动重定向到dashboard。 有效路由集的变化取决于用户手中移动设备的动态特性。

为了让你的直觉与React Router一致,用组件而不是静态路由的方式去思考。解决问题时考虑如何利用React的声明式地组合性,因为几乎每个“React Router问题”都可能是一个“React问题”。

测试

React Router 依赖 React context 来工作。这会影响如何测试组件。

Context

如果你尝试使用单元测试来测试一个会渲染出 <Link><Route> 等的组件,将会得到关于context的error或warning。或许你可能会想自己添加router context,但我们还是建议你将单元测试包裹在以下Router组件之一:基础的Router组件,包括history prop,或<StaticRouter>, <MemoryRouter>, 或 <BrowserRouter>(前提条件是在测试环境中可以访问全局的window.history)。

使用 <MemoryRouter> 或一个自定义的history是比较推荐的做法,目的是为了在测试的时候能够重置router。

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);
});

在特定的route中开始

<MemoryRouter> 支持 initialEntries 和 initialIndex 这两个props,所以你可以在特定的location启动一个app(或app中任意更小的部分)。

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

导航

我们做了很多测试,当location改变时routes可以正常工作,所以你不需要亲自测试这个。但如果你需要测试你的app里的导航时,你可以这样做:

// app.js (一个组件文件)
import React from "react";
import { Route, Link } from "react-router-dom";

// 这里测试的目标是 APP,但你也可以测试你的app里面更小的模块。
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>
);
// 这里你也可以使用一个类似于"@testing-library/react" 或 "enzyme/mount" 的渲染器
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

你不必在测试中经常访问location或history对象,但是如果你这么做了(例如验证url中设置的新的查询参数),你可以在测试中添加一个更新一个变量的route:

// 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(这些在Jest中都通过JSDOM默认提供了,但是无法在测试间重置history),你也可以使用BrowserRouter
  2. 比起传递一个自定义的route 到 MemoryRouter,你可以使用包含从history库中得到的history prop的基础Router
// 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 测试库

见官方文档示例: Testing React Router with React Testing Library

深度Redux集成

Redux是React生态的一个重要组成部分,我们想让React Router 和 Redux无缝集成,好让人们能一起使用。一些人提出的想法:

  • 将路由数据同步到store中,并通过store来访问
  • 可以通过dispatch actions来改变路由
  • 支持在Redux devtools中就路由改变进行时间旅行debug

所有这些都需要深度集成。我们给出的观点是:不要把路由保存在Redux store中。理由如下:

  • 路由数据早就是大部分组件的一个prop了,不管它从store还是router中来,对组件的代码基本没什么变化
  • 大多数情况下,你可以利用Link, NavLink 和 Redirect进行导航。有时,你可能需要在某个操作启动的异步任务后以编码的方式来导航。例如,当用户提交一个登录表单,你可能会dispatch一个action。你的thunk, saga或其他异步处理器完成鉴权等操作后,需要导航到一个新页面。这里的解决方案只是将history对象(提供给所有路由组件)包含在action的payload中,你的异步处理程序可以在适当的时候使用它进行导航。
  • 路由变更对时间旅行debugging来说没那么重要。唯一明显的情况就是调试你的router/store的同步问题,如果你不同步它们,这个问题就会消失。

但如果你强烈地想同步你的路由到store中,你或许可以尝试一下Connected React Router——一个第三方的库用来绑定React Router 和 Redux。

静态路由

之前的版本使用静态的路由配置,这允许在渲染前检查和匹配路由,自从v4变为动态的组件而不是路由配置,一些之前的用例变得很棘手。

我们开发了一个包来处理静态路由配置和React Router来继续满足那些用例。它现在还在开发中,但你可以试试。React Router Config

Hooks

React Router附带了一些hooks来让人可以访问router的状态,以及在组件内部执行导航。 注意,你需要使用React的版本号>=16.8才能使用这些hooks。

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

这个hook返回location对象表示当前的URL。这在有些场景下很有用,例如当你想触发一个新的视图事件,在页面加载时使用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

返回一个URL参数的键值对的对象。使用它来访问当前的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

这个hook会像<Route>那样匹配当前的URL。这在获取match data但是不需要渲染一个<Route>组件的情况时最有用。 例如比起这样写:

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 />;
}

这个hook接收一个参数,就等同于matchPath的props参数。即可以是一个路径字符串,也可以是一个包含Route接收的matching props一样的对象:

const match = useRouteMatch({
  path: "/BLOG/:slug/",
  strict: true,
  sensitive: true
});

组件

<BrowserRouter>

一个使用HTML5 history API(pushState, replaceState 以及 popstate event)来让UI和URL保持同步的<Router>

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

basename: string

所有locations的base URL。如果你的app在服务器上的二级目录,可以将basename设置为二级目录。正确的格式是以斜线开头,结尾没有斜线:

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

getUserConfirmation: func

一个使用确认导航的函数,默认会使用window.confirm

<BrowserRouter
  getUserConfirmation={(message, callback) => {
    // 这是默认行为
    const allowTransition = window.confirm(message);
    callback(allowTransition);
  }}
/>

forceRefresh: bool

值为true时,router将会在页面导航时整页刷新。你可以利用这个方法模仿服务端渲染的app在页面导航时整页刷新。

<BrowserRouter forceRefresh={true} />

keyLength: number

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

<BrowserRouter keyLength={12} />

children: node

要渲染的子元素。注意React < 16必须使用单个子元素,因为render方法不能返回多个元素,如果你需要多个元素,不妨试试将他们包裹在div中。

<HashRouter>

一个使用URL的hash部分(例如window.location.hash)来将页面UI与URL保持同步的<Router>组件。

重要提醒:hash history不支持location.keylocation.state。在早期版本中我们试着提供支持,但是出现了一些无法解决的边缘情况。所有需要支持的代码或插件都不能正常工作。由于<HashRouter>更多用来支持老浏览器,我们推荐你还是配置一下服务端然后使用<BrowserHistory>吧。

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

basename: string

<BrowserRouter>

getUserConfirmation: func

<BrowserRouter>

children: node

<BrowserRouter>

hashType: string

window.locatin.hash使用的编码方式。可选值有:

  • "slash" - 默认值,创建的hash形如 #/#/sunshine/lollipops
  • "noslash" - 创建的hash形如 ##sunshine/lollipops
  • "hashbang" - 创建“可抓取的ajax”(Google已废弃)形如 #!/#!/sunshine/lollipops

<StaticRouter>

一个从不改变location的 <Router>

用于服务端渲染:在服务端渲染的场景下,用户并没有真的点击,所以location从不改变。所以是static,静态的。也被用于简单测试,只需要插入一个location然后对渲染结果进行断言。

下面有个示例:服务器会发送一个302状态码来使用 <Redirect> 进行重定向,对其他请求返回常规的HTML。

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

http
  .createServer((req, res) => {
    // 这个 context 对象包含渲染结果
    const context = {};

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

    // 如果使用了一个 <Redirect>,context.url 会包含重定向到目标的URL
    if (context.url) {
      res.writeHead(302, {
        Location: context.url
      });
      res.end();
    } else {
      res.write(html);
      res.end();
    }
  })
  .listen(3000);

basename: string

所有location的base URL。正确的格式是开头包含下划线,结尾不包含下划线。

<StaticRouter basename="/calendar">
  <Link to="/today"/> // 渲染成 <a href="/calendar/today">
</StaticRouter>

location: string

服务器收到的 URL,对node服务器来说就是req.url。

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

location: object

一个形如 { pathname, search, hash, state } 的 location 对象。

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

context: object

一个普通的 JavaScript 对象。在渲染过程中,组件可以往这个对象中添加属性来保存渲染相关的信息。

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

当一个 匹配时,它把context 对象作为一个staticContext prop传递给它渲染的组件。

更多详情可以查看指南中的“服务端渲染”章节。

渲染后,这些属性可以用来配置服务端的 response。

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

children: node

要渲染的子组件。

注意:在 React < 16 版本中,必需使用单个子元素,因为一个render方法不能返回多个元素。 如果需要多个元素,或许可以试着将他们包裹进 div 元素。

<MemoryRouter>

一个 <Router>,将URL的history保存在内存中(不会读写入地址栏)。用于测试和非浏览器环境如React Native。

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

initialEntries: array

在history栈中的一个location数组。可以是一个完整的location对象如 { pathname, search, hash, state } 或简单的字符串URL。

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

initialIndex: number

initialEntries数组中的location的初始的下标。

getUserConfirmation: func

一个用来确认导航的函数。当你直接使用 <MemoryRouter> 和 一个 <Prompt>时,这个选项是必需的。

keyLength: number

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

<MemoryRouter keyLength={12} />

children: node

要渲染的子组件。

<Router>

所有router组件的公共低级组件,应用的中一般用到的是下面这些高级组件:

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

低级组件<Router>最常用的使用场景就是同步一个自定义的history到一个状态管理库中如 Redux 或 Mobx。使用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

用来导航的history对象。

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>

<Switch>

渲染第一个匹配当前location的<Route><Redirect>

它和只使用一组<Route>有何区别?一组<Route>被包裹在<Switch>中时,只渲染第一个匹配的;没有<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>来构造诸如侧边栏、面包屑、引导tab等组件。

有时候我们只想渲染一个<Route>,当URL为/about时,只想展示<About />,可以这么做:

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>
);

<Switch>会遍历查找匹配的<Route>,找到<Route path="/about"/>后就会停止查找,然后渲染出<About />

这在过渡动画时很有用,因为<Route>会在和上一个相同的位置渲染。

let routes = (
  <Fade>
    <Switch>
      {/* 这里最终只会渲染一个子组件 */}
      <Route />
      <Route />
    </Switch>
  </Fade>
);

let routes = (
  <Fade>
    {/* 这里总是会渲染两个子组件,使得过渡计算起来有点麻烦 */}
    <Route />
    <Route />
  </Fade>
);

location: object

一个location对象被用来匹配子元素,不是当前history location(当前浏览器URL)。

children: node

<Switch>的所有子组件应该是<Route><Redirect>。只有第一个匹配当前location的子组件会被渲染。

<Route>组件使用其path属性来匹配,<Redirect>组件使用其from属性来匹配。一个没有path的<Route>或一个没有from的<Redirect>总是会匹配当前location。

当你在<Switch>中包含一个<Redirect>时,它可以使用任何<Route>的location匹配属性:path, exact, 以及strict。from只是path的别名。

如果一个location属性传递给<Switch>,将会重写要匹配子组件的location属性。

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>
);

<Route>

<Route>组件是React Router中要理解和掌握的最重要的组件。它最基本的功能就是当它的path匹配当前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
);

如果app的location是/,那UI可能是:

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

如果app的location是/news,那UI可能是:

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

“react-empty”注释是React渲染null的实现细节。从技术上讲,一个<Route>总是会被渲染,只不过path匹配当前URL时渲染它的子组件,也就是提供给路由的component;不匹配则渲染null。

如果同一个组件被用作多个<Route>的子组件,并且处在组件树的相同位置,React将会把它们视为同一个组件实例,当路由改变时,组件的state将会被保存。如果不想保存,那就给每个路由组件添加一个唯一的key,这样当路由改变时,React将会重新创建组件实例。

路由渲染方法

推荐的<Route>渲染方法是使用它的子组件,就像上面给出的示例那样。还有一些其他的渲染方法,这些方法主要是为了支持那些在hooks被引入之前的早期版本构建的应用。

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

上面的方法使用一种即可,下面会解释这些方法之间的差异。

路由 props

所有的渲染方法都会传递以下三个相同的路由props

  • match
  • location
  • history

component

只有当location匹配时,一个React组件才会被渲染,并且会传递路由 props。

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

// 所有的路由 props (match, location 及 history) 都可以访问
function User(props) {
  return <h1>Hello {props.match.params.username}!</h1>;
}

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

当你使用component而不是render或children,React Router将会使用React.createElement方法从给定的component创建一个React组件。所以如果你在component属性中提供一个内联的函数,每次渲染都会将会创建一个新的组件。这会导致已有的组件不会unmount而新的组件每次都会mount,而不是仅仅更新已有的组件。因此,如果想使用内联的函数来内联渲染,使用render或children属性。

render: func

这个属性允许内联渲染,而且不会导致上面提到的预期外的重新mount。

你可以传递一个函数给render,当location匹配时,会自动调用这个函数。在渲染函数中,依然可以访问路由props(match, location 及 history)。

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

// 内联渲染
ReactDOM.render(
  <Router>
    <Route path="/home" render={() => <div>Home</div>} />
  </Router>,
  node
);

// wrapping/composing
// 你可以使用rest参数接收route props然后用spread运算符添加到子组件的props中,使得渲染的组件内部也可以访问路由props
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

有时候不管路由的path是否匹配location,你都需要渲染。这时你可以使用children属性,它和render属性的作用基本相同,除了不管是否匹配都会被调用这一点不同。

children属性和component、render属性一样,都接收相同的路由props,只有一点不同:当route匹配失败,match的值为null。这让你可以基于router是否匹配动态调整UI。下面的例子中如果路由匹配了,会添加一个active class:

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总是会渲染,所以你可以使用生命周期来给子组件应用进入/离开的动画 */}
    <Animate>
      {match && <Something {...rest}/>}
    </Animate>
  )}
/>

path: string | string[]

任何有效的URL路径或path-to-regexp@^1.7.0可以理解的路径数组。

<Route path="/users/:id">
  <User />
</Route>
<Route path={["/users/:id", "/profile/:id"]}>
  <User />
</Route>

没有path的Route总是匹配的。

exact: bool

值为true时,只有path和location.pathname精确匹配时才会匹配成功。

<Route exact path="/one">
  <About />
</Route>
pathlocation.pathnameexact是否匹配?
/one/one/twotrue
/one/one/twofalse

strict: bool

值为true时,一个以斜线结尾的path只会匹配结尾有斜线的location.pathname。当location.pathname还有多余的片段,则不会受影响。

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

注意:strict可以用来强制使location.pathname不含结尾的斜线,为了实现效果,exact也必须设为true。

<Route exact strict path="/one">
  <About />
</Route>
pathlocation.pathname是否匹配?
/one/one
/one/one/
/one/one/two

location: object

一个<Route>组件会尝试匹配自己的path和当前的history location(通常是当前的浏览器URL)。可是一个有不同pathname的location照样可以用来匹配。

这在当你需要匹配一个<Route>到一个location而不是当前的history location时会很有用。

如果一个<Route>组件被包裹在一个<Switch>中,并且成功匹配到传递给<Switch>的location,或当前history location时,<Route>原本的location prop将会被<Switch>所使用的location重写。

sensitive: bool

值为true时,匹配路径会区分大小写。

<Route sensitive path="/one">
  <About />
</Route>
pathlocation.pathnamesensitive是否匹配?
/one/onetrue
/One/onetrue
/One/onefalse

<Redirect>

渲染一个<Redirect>将会导航到新的location,并重写history栈中当前location,就像服务端重定向(HTTP 3xx)那样。

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

to: string

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

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

to: object

要重定向到哪个location。pathname是path-to-regexp@^1.7.0可以理解的任何有效的URL路径。

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

state对象可以在重定向到的组件中通过this.props.location.state访问。比如这个示例中的referrer就可以在/login路径名对应的Login组件中通过this.props.location.state.referrer来访问。

push: bool

值为true时,重定向时将会在history栈中添加一个新的记录,而不是替换当前的记录。

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

from: string

一个路径名,表示从哪里重定向。from中所有匹配成功的URL参数都会提供给to,from的URL参数必须包含to中使用的所有参数,没用到的参数会被忽略。

注意:当在<Switch>中渲染一个<Redirect>时,这只能用来匹配一个location。详情可见<Switch children>

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

// 带有匹配参数的Redirect
<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id'/>
  <Route path='/users/profile/:id'>
    <Profile />
  </Route>
</Switch>

exact: bool

精确匹配from,等同于Route.exact

注意:当在<Switch>中渲染一个<Redirect>时,这个属性只能和from一起使用来精确匹配一个location。详情可见<Switch children>

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

strict: bool

严格匹配,同Route.strict

注意:当在<Switch>中渲染一个<Redirect>时,这个属性只能和from一起使用来严格匹配一个location。详情可见<Switch children>

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

sensitive: bool

匹配from时不忽略大小写,同Route.sensitive

<Prompt>

用来在离开一个页面前提示用户。当需要阻止用户离开时例如编辑页面的表单还未提交,这时可以渲染一个 <Prompt> 来提醒用户。

<Prompt
  when={formIsHalfFilledOut}
  message="你确定要离开吗?"
/>

message: string

显示的提示文字。

<Prompt message="你确定要离开吗?" />

message: func

调用时传递的参数是用户将要导航到目的地的location和action。 返回一个字符串来提示用户进行确认或返回true来允许跳转。

<Prompt
  message={(location, action) => {
    if (action === 'POP') {
      console.log("Backing up...")
    }

    return 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?" />

<Link>

在你的应用中提供一个声明式的、可访问的导航链接。

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

to: string

表示链接到的位置,由location的pathname、search、hash组成。

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

to: object

一个包含以下属性的对象:

  • pathname: 一个字符串,表示要链接到的路径
  • search: 一个字符串,表示查询参数
  • hash: URL中的hash,如 #a-hash
  • state: 保存在location里的state,可以用来向目标页面传递一些数据
<Link
  to={{
    pathname: "/courses",
    search: "?sort=name",
    hash: "#the-hash",
    state: { fromDashboard: true }
  }}
/>

to: function

一个函数 当前location作为参数传入,返回一个字符串或对象形式的location。

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

replace: bool

值为true时,点击链接将会替换history栈的当前记录,而不是添加一个。

<Link to="/courses" replace />

innerRef: function

允许访问组件内部的ref。

从React Router 5.1开始,如果你正在使用React 16,那你就不需要这个属性,因为我们已经转发了ref到内部的<a>标签,只使用普通的ref就好。

<Link
  to="/"
  innerRef={node => {
    // `node` 指向挂载的DOM元素,unmounted时指向null
  }}
/>

innerRef: RefObject

同上,只不过使用React.createRef来获取底层的ref。

let anchorRef = React.createRef()

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

component: React.Component

如果你想利用你自己的导航组件,只需要将它传递给组件的component属性即可。

const FancyLink = React.forwardRef((props, ref) => (
  <a ref={ref}>💅 {props.children}</a>
))

<Link to="/" component={FancyLink} />

其他

你还可以传递任何你想在<a>标签上使用的属性,例如title,id,className等等。

<NavLink>

一个特殊的<Link>,匹配当前URL时会自动添加样式相关的属性。

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

exact: bool

值为true时,只有精确匹配时才会应用active class/style。

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

strict: bool

值为true时,匹配时会将一个location的pathname末尾的/也会考虑进来,详情见<Route strict>

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

activeClassName: string

激活时添加的class。默认值给定的class是active。这个类名会和className属性合并。

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

activeStyle: object

激活时应用的样式。

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

isActive: func

一个函数,用来添加额外的逻辑来决定这个链接是否要被激活。用于如果你想做更多的事来验证这个link的pathname是否匹配当前URL的pathname。

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

    // 只考虑event id是偶数的event
    const eventID = parseInt(match.params.eventID);
    return !isNaN(eventID) && eventID % 2 === 1;
  }}
>
  Event 123
</NavLink>

location: object

通常匹配时比较的是history location(通常是当前浏览器URL),可以通过这个属性传递一个不同的location用于匹配时的比较。

aria-current: string

此属性的值在一个active链接上使用,可选的值有:

  • "page" - 默认值,用来表示一组分页链接中的一个链接
  • "step" - 用来表示步骤流程中的一个步骤
  • "location" - 用来表示一个流程图中当前组件的高亮的图像
  • "date" - 用来表示日历中的当前日期
  • "time" - 用来表示时间表中的当前时间
  • "true" - 用来表示NavLink是否是激活状态

基于 WAI-ARIA 1.1 规范

对象和方法

history

这篇文档所说的“history”和“history object”都指的是history库,这也是React Router除了React外两个主要依赖之一,这个库为在不同环境中使用js管理session history提供了几种不同的实现。

以下术语被用到:

  • “browser history”:一个DOM实现,用于支持HTML5 history API的浏览器
  • “hash history”:一个DOM实现,用于老浏览器
  • “memory history”:一个内存history实现,用于测试和非DOM环境如React Native

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

  • length - (number) 历史记录栈中的记录数
  • action - (string) 当前的动作(PUSH, REPLACE, 或 POP)
  • location - (object) 当前的location,可能包含以下属性:
    • pathname - (string) URL的path
    • search - (string) URL的查询字符串
    • hash - (string) URL的hash片段
    • state - (object) 特定于location的状态,提供给诸如当这个location入栈时push(path, state)。只在浏览器和内存的history中可用。
  • push(path, [state]) - (function) 将一个新的记录推入history栈
  • replace(path, [state]) - (function) 替换history栈的当前记录
  • go(n) - (function) 将history栈的指针移动n步
  • goBack() - (function) 等同于 go(-1)
  • goForward() - (function) 等同于 go(1)
  • block(prompt) - (function) 阻止导航(参看 history 文档)

history是可变的

history对象是可变的。因此推荐从<Route>的render props访问location,而不是从history.location。这样做确保了React处在正确的生命周期钩子,举个例子:

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

    // 不正确,结果总是false,因为history是可变的
    const locationChanged =
      this.props.history.location !== prevProps.history.location;
  }
}

<Route component={Comp} />;

更多属性查看history文档

location

location表明了当前app在哪儿,你想去哪儿,你想去的地方在哪儿,看起来就像这样:

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

可以在以下几个地方访问location对象:

  • Route component —— this.props.location
  • Route render —— ({ location }) => ()
  • Route children —— ({ location }) => ()
  • withRouter —— this.props.location

还可以在history.location中找到,但你不应该这么用,因为它是可变的。

一个location对象永远都是不可更改的,所以你可以在生命周期钩子中使用它来决定什么时候导航发生,这在获取数据和动画时非常有用:

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

你可以提供locations而不是字符串到不同的导航的地方:

  • Web Link to
  • Native Link to
  • Redirect to
  • history.push
  • history.replace

通常只用一个字符串,但是如果你需要添加一些“location state”来让app到达那个特定的location时可用,可以使用一个location对象。如果你想利用导航history而不是path来区分UI,这种方式会很有用。

// 通常的写法
<Link to="/somewhere"/>

// 但也可以使用一个location
const location = {
  pathname: '/somewhere',
  state: { fromDashboard: true }
}

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

可以向以下组件中传递一个location:

  • Route
  • Switch

这会阻止他们使用router的state中实际的location。这对于动画和处理中的导航很有用,或任何时候你想诱使组件在与实际location不同的location中进行渲染。

match

一个match对象包含了一个<Route path>怎样匹配了URL.match对象的信息,包括以下属性:

  • params - (object) 从URL解析的键值对,和path的动态片段一致
  • isExact - (boolean) 如果整个URL都匹配则为true
  • path - (string) 用来匹配的 path pattern,用于构建嵌套的<Route>
  • url - (string) 匹配的URL部分,用于构建嵌套的<Link>

你可以在这些地方访问match对象:

  • Route component —— this.props.match
  • Route render —— ({ match }) => ()
  • Route children —— ({ match }) => ()
  • withRouter —— this.props.match
  • matchPath —— 返回值
  • useRouteMatch —— 返回值

如果一个Route没有path并因此总是会匹配到,你会得到最近的上级匹配,withRouter也一样。

什么都没匹配到

一个使用了children prop的<Route>将会调用它的children function,即使path不匹配当前的location。这种情况下,match将会是null。当它匹配到了能够渲染一个<Route>的内容会很有用,但是会有其他问题。

解析URL的默认方式是将match.url字符串加入相对路径:

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

如果你在match为null的时候尝试这么做,你会得到一个TypeError。这意味着当使用children prop在一个<Route>里面添加相对路径被认为是不安全的。

当你在一个<Route>内使用一个没有path且match为null的<Route>时也会发生类似的情况。

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

无path的<Route>从父级继承match对象,如果父级的match是null,那他们的match也是null。这意味着:

  1. 任何child routes/links都必须是绝对的因为没有parent可以解析
  2. 一个无path的路由的父级的match可能是null的,将会需要使用children prop来渲染。

matchPath

这让你可以使用和<Route>相同的匹配代码,但不在正常渲染周期之内,例如在服务端渲染前收集数据依赖。

import { matchPath } from "react-router";

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

pathname

第一个参数是你相匹配的pathname。如果你在服务端使用的是Node.js,这个值就是req.path

props

第二个参数是匹配选项,和Route接收的匹配props相同。它也可以是一个字符串或一个字符串数组,作为{ path }的简写。

{
  path, // 如 /users/:id; 可以是一个字符串或字符串数组
  strict, // 可选, 默认false
  exact, // 可选, 默认false
}

返回值

当pathname和path属性匹配成功时返回一个对象。

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

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

当pathname和path属性不匹配时返回null。

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

//  null

withRouter

你可以通过withRouter高阶组件来访问history对象属性和最近的<Route>的match对象。withRouter会在被包裹的组件渲染时,将更新过的match, location, 和history作为props传递给该组件。

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>You are now at {location.pathname}</div>;
  }
}

// 创建了一个“连接”到router的新组件
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);

重要提醒:withRouter没有像React Redux的connect订阅state更新那样订阅location的变化。而是在location变化后从<Router>组件传播出来,重新渲染组件。这就意味着withRouter不会在路由过渡(transitions)时重新渲染,除非父组件重新渲染了。

被包裹组件的所有非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)} />
    );
  }
}

generatePath

这个方法可以用来给路由生成一个URL。内部使用了path-to-regexp库。

import { generatePath } from "react-router";

generatePath("/user/:id/:entity(posts|comments)", {
  id: 1,
  entity: "posts"
});
// 将返回 /user/1/posts

将path编译为正则表达式的结果将会被缓存,所以使用相同的pattern生成多个path不会产生额外的开销。

pattern: string

该方法有两个参数,第一个参数是一个就像Route组件的path属性值的pattern。

params: object

第二个参数是一个对象,提供了pattern要用到的路由参数。

如果提供的路由参数和path不匹配,将会报错:

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

References

  1. 官方文档:reactrouter.com/web/guides/…
  2. Github文档:github.com/ReactTraini…