React-Router-Dom 4 入门

刘阳舒
2023-12-01


一、快速开始

返回目录

首先要创建一个 Web App

#安装官方脚手架 create-react-app
npm install -g create-react-app
#创建项目
create-react-app demo-app
#进入项目目录
cd demo-app
#安装 react-router-dom
npm install react-router-dom

示例1: 基础路由

返回目录

在这个例子里,该路由匹配三个页面(主页、关于页、用户页)。当你点击不同的 <Link> 时,<Router> 会渲染与之匹配的 <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="/">主页</Link></li>
          <li><Link to="/about">关于页</Link></li>
          <li><Link to="/users">用户页</Link></li>
        </ul>
      </nav>
      
      {/* <Switch> 会查找与 URL 匹配的第一个 <Route> */}
      <Switch>
        <Route path="/about"><About /></Route>
        <Route path="/users"><Users /></Route>
        <Route path="/"><Home /></Route>
      </Switch>
    </div>
  </Router>);
}

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

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

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

示例2: 嵌套路由

返回目录

这个例子展示嵌套路由是怎么工作的。/topics的路由会加载主题页组件,并会渲染其页面下的路由,并展示符合条件的路由的 topicId 值

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="/">主页</Link></li>
          <li><Link to="/about">关于页</Link></li>
          <li><Link to="/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>主页</h2>;
}

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

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

  return (
    <div>
      <h2>主题页</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>
	  {/* 嵌套的<Switch>的路由 */}
      <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 里有三种重要的组件

  • 路由器(Router), 包括 <BrowserRouter> 和 <HashRouter>
  • 路由(Route), 包括 <Route> 和 <Switch>
  • 导航(Navigation), 包括 <Link>, <NavLink>, 和 <Redirect>

路由器

返回目录

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

路由

返回目录

有两个路由匹配组件:Switch 和 Route。渲染 <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>
        {/* 如果当前的 URL 是 /about, 这个路由将被渲染,而其余的被忽略 */}
        <Route path="/about"><About /></Route>

        {/* 请注意这两个路由的顺序。更具体的路径 path="/contact/:id" 将放在
            path="/contact" 之前。因为要查看个人的 contact 组件时,
            正确的路由将会渲染 */}
        <Route path="/contact/:id"><Contact /></Route>
        <Route path="/contact"><AllContacts /></Route>

        {/* 如果先前的路由都不渲染任何东西,这路由充当后备路由。

       		重要提示: path="/" 的路由将始终匹配,因为所有的 URL 都是 / 开头
            所以这就是我们为什么把它放在所有路由的最后 */}
        <Route path="/"><Home /></Route>
      </Switch>
    </div>
  );
}

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

需要注意一件重要的事是 <Route path> 匹配 URL 的开头,而不是整个开头。因此 <Route path=“/”> 将一直与 URL 匹配。因此,我们通常将此 <Route> 放置在 <Switch>里的最后面。另一种可能的解决方案是使用 <Route exact path="/">,它与整个 URL 匹配。

注意:尽管 React Router 确实支持在 <Switch> 之外渲染 <Route> 元素,但是从 5.1 版本开始,我们建议您改用 useRouteMatch。此外,我们不建议您渲染一个没有 path 的 <Route>,而是建议您使用 hook 来访问所需的任何变量。

导航

返回目录

React Router 提供 <Link> 组件来创建链接. 无论在哪使用 <Link>, 它将会变成一个 <a> 标签在 html 上。

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

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

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

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

// 当 URL 是其它时, 将会渲染:
// <a href="/react">React</a>

任何时候要强制导航,可以使用 <Redirect>。渲染 <Redirect>时,它将使用其其属性进行导航。

<Redirect to="/login" />

三、服务器端渲染

返回目录

Rendering on the server is a bit different since it’s all stateless. The basic idea is that we wrap the app in a stateless <StaticRouter> instead of a <BrowserRouter>. We pass in the requested url from the server so the routes can match and a context prop we’ll discuss next.
由于服务器都是无状态的,因此在服务器上的渲染有点不同。基本思想是,将应用程序包装在无状态 <StaticRouter> 中,而不是 <BrowserRouter> 中。我们从服务器传入请求的 url,以便路由可以匹配,然后我们将讨论 context 属性。

// 客户端
<BrowserRouter>
  <App/>
</BrowserRouter>

// 服务器端 (简略)
<StaticRouter
  location={req.url}
  context={context}
>
  <App/>
</StaticRouter>

When you render a <Redirect> on the client, the browser history changes state and we get the new screen. In a static server environment we can’t change the app state. Instead, we use the context prop to find out what the result of rendering was. If we find a context.url, then we know the app redirected. This allows us to send a proper redirect from the server.
当您在客户端上使用 <Redirect>时,浏览器历史记录将更改状态,我们将获得新屏幕。在静态服务器环境中,我们无法更改应用程序状态。相反,我们使用 context 属性来找出渲染的结果。如果找到 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 信息

返回目录

路由器只会添加 context.url。但是您可能希望将某些重定向重定向为301,将其他重定向重定向为302。或者,如果呈现了UI的某些特定分支,则可能要发送404响应,如果未授权,则要发送401。context 属性是您的,因此您可以对其进行更改。这是区分 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} />;
      }}
    />
  );
}

// 在你的app里的任意位置
function App() {
  return (
    <Switch>
      {/* some other routes */}
      <RedirectWithStatus status={301} from="/users" to="/profiles" />
      <RedirectWithStatus
        status={302}
        from="/courses"
        to="/dashboard"
      />
    </Switch>
  );
}

// 在服务器上
const context = {};

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

if (context.url) {
  // 可以使用 `context.status` that
  // we added in RedirectWithStatus
  redirect(context.status, context.url);
}

404, 401, 或其他任何状态

返回目录

我们可以做与上述相同的事情。创建一个添加一些 context 的组件,并将其使用在应用程序中的任何位置以获取不同的状态代码。

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

现在你可以把 在 staticContext 中添加代码,在应用程序中的任何位置展示出来

function NotFound() {
  return (
    <Status code={404}>
      <div>
        <h1>对不起, 不能找到它.</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软件包感兴趣,以通过静态路由配置帮助数据加载和服务器渲染。


四、代码分割

返回目录

One great feature of the web is that we don’t have to make our visitors download the entire app before they can use it. You can think of code splitting as incrementally downloading the app. To accomplish this we’ll use webpack, @babel/plugin-syntax-dynamic-import, and loadable-components.

webpack has built-in support for dynamic imports; however, if you are using Babel (e.g., to compile JSX to JavaScript) then you will need to use the @babel/plugin-syntax-dynamic-import plugin. This is a syntax-only plugin, meaning Babel won’t do any additional transformations. The plugin simply allows Babel to parse dynamic imports so webpack can bundle them as a code split. Your .babelrc should look something like this:

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

loadable-components is a library for loading components with dynamic imports. It handles all sorts of edge cases automatically and makes code splitting simple! Here’s an example of how to use 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 />;
  }
}

That’s all there is to it! Simply use LoadableDashboard (or whatever you named your component) and it will automatically be loaded and rendered when you use it in your application. The fallback is a placeholder component to show while the real component is loading.

这里的所有都是它的!只需使用 LoadableDashboard(或任何您命名的组件),当您在应用程序中使用它时,它将自动加载并呈现。回退是一个占位符组件,用于在加载实际组件时显示。

完整的文档点

代码分割和服务端渲染

返回目录

loadable-components includes a guide for server-side rendering.


五、Scroll Restoration

返回目录

In earlier versions of React Router we provided out-of-the-box support for scroll restoration and people have been asking for it ever since. Hopefully this document helps you get what you need out of the scroll bar and routing!

Browsers are starting to handle scroll restoration with history.pushState on their own in the same manner they handle it with normal browser navigation. It already works in chrome and it’s really great. Here’s the Scroll Restoration Spec.

Because browsers are starting to handle the “default case” and apps have varying scrolling needs (like this website!), we don’t ship with default scroll management. This guide should help you implement whatever scrolling needs you have.

Scroll to top

返回目录

Most of the time all you need is to “scroll to the top” because you have a long content page, that when navigated to, stays scrolled down. This is straightforward to handle with a <ScrollToTop> component that will scroll the window up on every navigation:

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

If you aren’t running React 16.8 yet, you can do the same thing with a React.Component subclass:

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

Then render it at the top of your app, but below Router

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

If you have a tab interface connected to the router, then you probably don’t want to be scrolling to the top when they switch tabs. Instead, how about a <ScrollToTopOnMount> in the specific places you need it?

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

Again, if you aren’t running React 16.8 yet, you can do the same thing with a React.Component subclass:

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

Generic Solution

返回目录

For a generic solution (and what browsers are starting to implement natively) we’re talking about two things:

  1. Scrolling up on navigation so you don’t start a new screen scrolled to the bottom
  2. Restoring scroll positions of the window and overflow elements on “back” and “forward” clicks (but not Link clicks!)

At one point we were wanting to ship a generic API. Here’s what we were headed toward:

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

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

First, ScrollRestoration would scroll the window up on navigation. Second, it would use location.key to save the window scroll position and the scroll positions of RestoredScroll components to sessionStorage. Then, when ScrollRestoration or RestoredScroll components mount, they could look up their position from sessionsStorage.

The tricky part was defining an “opt-out” API for when you don’t want the window scroll to be managed. For example, if you have some tab navigation floating inside the content of your page you probably don’t want to scroll to the top (the tabs might be scrolled out of view!).

When we learned that Chrome manages scroll position for us now, and realized that different apps are going to have different scrolling needs, we kind of lost the belief that we needed to provide something–especially when people just want to scroll to the top (which you saw is straight-forward to add to your app on your own).

Based on this, we no longer feel strongly enough to do the work ourselves (like you we have limited time!). But, we’d love to help anybody who feels inclined to implement a generic solution. A solid solution could even live in the project. Hit us up if you get started on it 


六、Philosophy

返回目录

This guide’s purpose is to explain the mental model to have when using React Router. We call it “Dynamic Routing”, which is quite different from the “Static Routing” you’re probably more familiar with.

Static Routing

返回目录

If you’ve used Rails, Express, Ember, Angular etc. you’ve used static routing. In these frameworks, you declare your routes as part of your app’s initialization before any rendering takes place. React Router pre-v4 was also static (mostly). Let’s take a look at how to configure routes in express:

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

app.listen();

Note how the routes are declared before the app listens. The client side routers we’ve used are similar. In Angular you declare your routes up front and then import them to the top-level AppModule before rendering:

// Angular Style 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 has a conventional routes.js file that the build reads and imports into the application for you. Again, this happens before your app renders.

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

export default Router;

Though the APIs are different, they all share the model of “static routes”. React Router also followed that lead up until v4.

To be successful with React Router, you need to forget all that! 

Backstory

返回目录

To be candid, we were pretty frustrated with the direction we’d taken React Router by v2. We (Michael and Ryan) felt limited by the API, recognized we were reimplementing parts of React (lifecycles, and more), and it just didn’t match the mental model React has given us for composing UI.

We were walking through the hallway of a hotel just before a workshop discussing what to do about it. We asked each other: “What would it look like if we built the router using the patterns we teach in our workshops?”

It was only a matter of hours into development that we had a proof-of-concept that we knew was the future we wanted for routing. We ended up with API that wasn’t “outside” of React, an API that composed, or naturally fell into place, with the rest of React. We think you’ll love it.

Dynamic Routing

返回目录

When we say dynamic routing, we mean routing that takes place as your app is rendering, not in a configuration or convention outside of a running app. That means almost everything is a component in React Router. Here’s a 60 second review of the API to see how it works:

First, grab yourself a Router component for the environment you’re targeting and render it at the top of your 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
);

Next, grab the link component to link to a new location:

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

Finally, render a Route to show some UI when the user visits /dashboard.

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

The Route will render <Dashboard {…props}/> where props are some router specific things that look like { match, location, history }. If the user is not at /dashboard then the Route will render null. That’s pretty much all there is to it.

Nested Routes

返回目录

Lots of routers have some concept of “nested routes”. If you’ve used versions of React Router previous to v4, you’ll know it did too! When you move from a static route configuration to dynamic, rendered routes, how do you “nest routes”? Well, how do you nest a div?

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

// when the url matches `/tacos` this component renders
const Tacos = ({ match }) => (
  // here's a nested div
  <div>
    {/* here's a nested Route,
        match.url helps us make a relative path */}
    <Route path={match.url + "/carnitas"} component={Carnitas} />
  </div>
);

See how the router has no “nesting” API? Route is just a component, just like div. So to nest a Route or a div, you just … do it.

Let’s get trickier.

Responsive Routes

返回目录

Consider a user navigates to /invoices. Your app is adaptive to different screen sizes, they have a narrow viewport, and so you only show them the list of invoices and a link to the invoice dashboard. They can navigate deeper from there.

Small Screen
url: /invoices

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

On a larger screen we’d like to show a master-detail view where the navigation is on the left and the dashboard or specific invoices show up on the right.

Large Screen
url: /invoices/dashboard

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

Now pause for a minute and think about the /invoices url for both screen sizes. Is it even a valid route for a large screen? What should we put on the right side?

Large Screen
url: /invoices
+----------------------+---------------------------+
|                      |                           |
|      Dashboard       |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 01      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 02      |             ???           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 03      |                           |
|                      |                           |
+----------------------+                           |
|                      |                           |
|      Invoice 04      |                           |
|                      |                           |
+----------------------+---------------------------+

On a large screen, /invoices isn’t a valid route, but on a small screen it is! To make things more interesting, consider somebody with a giant phone. They could be looking at /invoices in portrait orientation and then rotate their phone to landscape. Suddenly, we have enough room to show the master-detail UI, so you ought to redirect right then!

React Router’s previous versions’ static routes didn’t really have a composable answer for this. When routing is dynamic, however, you can declaratively compose this functionality. If you start thinking about routing as UI, not as static configuration, your intuition will lead you to the following code:

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

As the user rotates their phone from portrait to landscape, this code will automatically redirect them to the dashboard. The set of valid routes change depending on the dynamic nature of a mobile device in a user’s hands.

This is just one example. There are many others we could discuss but we’ll sum it up with this advice: To get your intuition in line with React Router’s, think about components, not static routes. Think about how to solve the problem with React’s declarative composability because nearly every “React Router question” is probably a “React question”.

七、Testing

返回目录

React Router relies on React context to work. This affects how you can test your components that use our components.

Context

返回目录

If you try to unit test one of your components that renders a <Link> or a <Route>, etc. you’ll get some errors and warnings about context. While you may be tempted to stub out the router context yourself, we recommend you wrap your unit test in one of the Router components: the base Router with a history prop, or a <StaticRouter>, <MemoryRouter>, or <BrowserRouter> (if window.history is available as a global in the test enviroment).

Using MemoryRouter or a custom history is recommended in order to be able to reset the router between tests.

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

Starting at specific routes

返回目录

<MemoryRouter> supports the initialEntries and initialIndex props, so you can boot up an app (or any smaller part of an app) at a specific location.

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

Navigating

返回目录

We have a lot of tests that the routes work when the location changes, so you probably don’t need to test this stuff. But if you need to test navigation within your app, you can do so like this:

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

Checking location in tests

返回目录

You shouldn’t have to access the location or history objects very often in tests, but if you do (such as to validate that new query params are set in the url bar), you can add a route that updates a variable in the test:

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

Alternatives

  1. You can also use BrowserRouter if your test environment has the browser globals window.location and window.history (which is the default in Jest through JSDOM, but you cannot reset the history between tests).
  2. Instead of passing a custom route to MemoryRouter, you can use the base Router with a history prop from the history package:
    // 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 Library

返回目录

See an example in the official documentation: Testing React Router with React Testing Library


八、集成 Redux

返回目录

Redux 是 React 生态系统的重要组成部分。我们希望使 React Router 和 Redux 的集成尽可能无缝,以供希望同时使用这两者的人使用。

阻止更新

返回目录

一般来说,React Router 和 Redux 一起工作得很好。
不过,有时应用程序可能会有一个组件,在位置更改时不会更新(子路由 或 active 的 nav link 不会更新)。

如果发生以下情况:

  1. 这个组件使用了 connect()(Comp).
  2. 这个组件不是路由组件,意思是它不是这样渲染的: <Route component={SomeConnectedThing}/>

The problem is that Redux implements shouldComponentUpdate and there’s no indication that anything has changed if it isn’t receiving props from the router. This is straightforward to fix. Find where you connect your component and wrap it in withRouter.
问题是 Redux 实现了 shouldComponentUpdate,如果它没有从 Router 那接收 prop,就没有任何迹象表明有任何变化。这很容易解决。找到 connect 组件的位置并用 withRouter 将其包起来。

// 之前
export default connect(mapStateToProps)(Something)

// 之后
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))

深度集成

返回目录

有些人想:

  1. 将路由数据与 store 同步,并从 store 中进行访问。
  2. 能够通过 dispatching actions 进行导航。
  3. 支持 Redux devtools 中路由更改的时差调试

所有这些都需要更深入的整合。

我们建议不要将您的路由保留在您的Redux store。原因:

  1. 路由数据已经是大多数关心它的组件的 prop。无论是来自 store 还是路由器,组件的代码基本上是相同的。
  2. 在大多数情况下,可以使用 Link、NavLink 和 Redirect 来进行导航操作。有时,在 action 启动的异步任务之后,可能还需要用编程的方式进行导航。例如,您可以在用户提交登录表单时dispatch 一个 action。 然后 thunk, saga 或其他异步处理程序验证凭据,如果成功,则需要以某种方式导航到新页面。这里的解决方案只是在 action 的 payload 中包含 history 对象(提供给所有路由组件),并且您的异步处理程序可以在适当的时候使用它来导航。
  3. Route changes are unlikely to matter for time travel debugging. The only obvious case is to debug issues with your router/store synchronization, and this problem goes away if you don’t synchronize them at all.
    对于时差调试来说,路由更改不太重要。唯一明显的情况是调试路由器/store 同步的问题,如果根本不同步,这个问题就会消失。

但是,如果您强烈希望将路由与 store 同步,则可能需要尝试连接的React Router,这是React Router v4 和 Redux 的第三方绑定。

九、静态路由

返回目录

React Router 之前的版本使用静态路由来配置应用程序的路由。这样可以在渲染之前检查和匹配路由。由于 v4 转移到动态组件而不是路由配置,因此以前的一些用例变得不那么好理解和复杂。

We are developing a package to work with static route configs and React Router, to continue to meet those use-cases. It is under development now but we’d love for you to try it out and help out.
我们正在开发一个可与静态路由配置和 React Router 配合使用的 package,以继续满足这些用例。现在正在开发中,但我们希望您能尝试一下并提供帮助。

React Router Config

 类似资料: