React Router

Table of Contents

react-router

1. 指南

1.1. 主要组件

React Router 主要分三类组件:

  • 路由器(routers),比如 <BrowserRouter><HashRouter>
  • 路由匹配,比如 <Route><Switch>
  • 导航(Navigation),比如 <Link>, <NavLink><Redirect> ,也成为路由变更

所有的组件,都需要从 react-router-dom 库中导入。

1.1.1. 路由器

路由组件的核心是 React Router,对于 Web 项目, react-router-dom 提供了 <BrowserRouter><HashRouter> 两种 routers。 主要区别在于他们存储 URL 和与服务器通信的方式。

  • <BrowserRouter> 使用正常的 URL 路径,要求正确的配置服务器地址
  • <HashRouter> 讲当前的路径存储再 hash 部分中,网址看起来是这样的 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")
);

1.1.2. 路由匹配

有两个路由匹配组件: SwitchRoute 。当 <Switch> 渲染的时候,它会搜索它的 <Route> 元素,找到与当前 URL 匹配的元素。 匹配到之后就会忽略其他的路由。也就意味着特殊的路径要放在不特殊路径的 前面

如果找不到匹配项, <Switch> 会什么都不渲染(null)。

值得注意的是, <Route path> 只会匹配 URL 的 开头 ,不是全部。所以 <Route path="/"> 总是会匹配所有的 URL。 因为这个原因,我们通常把它放在 <Switch> 的最后。另外一个解决办法是使用 exact 参数,全部匹配路径,比如: <Route exact path="/">

1.1.3. 路由变更(Nav)

React Router 提供了 <Link> 组件来创建链接。当使用 <Link> 的时候,会渲染成 anchor( <a> )。

<NavLink><Link> 的一种特殊类型,当其 prop 与当前位置匹配时,可以将其自身设置为「活动」。比如:

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

可以用于对当前选中的页面做一些特殊的样式设置之类的。

如果想要强制导航,你可以使用 <Redirect> 。当渲染的时候,它会使用 prop 进行导航。

1.2. TODO 服务端渲染(Server Rendering)

1.3. 代码拆分(Code Splitting)

代码拆分可以理解成代码按需加载。为了实现这个目标,需要使用 webpack, @babel/plugin-syntax-dynamic-import 和 loadable-components。

webpack 默认支持动态导入;但是,你如果使用了 Babel(比如:把 JSX 编译成 JavaScript),那么你就需要使用 @babel/plugin-syntax-dynamic-import 插件。 这只是一个语法插件,意味着 Babel 不会做任何额外的转换。该插件仅允许 Babel 解析动态导入,因此 webpack 可以将 bundle 他们作为代码拆分。你的 .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 />;
  }
}

当程序运行时,动态加载,fallback 是加载期间展示的组件。

1.4. 滚动恢复

1.4.1. 滚动到顶部

当有一个长页面时,经常有滚动到顶部的需求。使用 <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>
  );
}

如果你将标签页连接到路由器,那么当切换标签页时,你不希望滚动到顶部。而是滚动到特定位置。

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 以前的版本,可以这么做:

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.4.2. 通用的解决方案

对于通用的解决办法(浏览器已经开始原生实现),我们讨论两件事情:

  1. 向上滚动导航,这样就不会开始一个新的底部屏幕
  2. 恢复滚动位置,点击「后退」和「前进」溢出元素(但不是点击链接)

在这个点上,我们希望要有一个通用的 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 保存窗口滚动位置,并且 ScrollRestoration 的位置放在 sessionStorage 。 然后,当 ScrollRestoration 或者 RestoredScroll 组件挂载时,他们可以从 sessionStorage 中查找他们的位置。

说了半天,原来都是设想,不看了..

1.5. 原理

这份指南的目的是说明使用 React Router 时要具有的思维模型。我们称作「动态路由」,与你熟悉的「静态路由」完全不同 。

1.5.1. 静态路由

如果你用过 Rails,Express,Angular 等。那你就已经用过静态路由了。在这些框架中,所有的路由部分都要放在应用初始化任何渲染的前面。 React Router pre-v4 也是静态的(大部分)。让我们看一下在 Express 中是如何动态配置路由的:

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

app.listen();

注意,是在程序监听之前是如何生命路由的。客户端一侧的路由也是类似的。在 Angular 中,你也要提前声明好路由, 然后在顶部导入他们( AppModule 渲染之前):

// 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 使用了比较常规的 routes.js 文件,但也是在你的程序渲染之前做的:

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

尽管 API 不同,但他们都是「静态路由」模型,React Router 也一直跟进到 V4。

为了用好 React Router,你需要忘了这些。

1.5.2. 动态路由

所谓动态路由指的是在应用渲染的时候进行的路由,不是程序之外的配置或者约定。也就是说所有的路由都是 React Router 中的组件。 下面是 API 审查,帮助你了解其工作原理:

首先,在需要渲染应用的顶部添加 Router 组件:

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

然后,使用链接组件链接到新的位置:

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 是路由器相关的信息,比如 { match, location, history } ,如果用户不是 /dashboard 路径 会渲染成 null 。这基本上就是路由的全部了。

1.5.3. 嵌套路由

许多路由器都有嵌套路由的概念,如果你用了 V4 的 React Router,那么你就知道它也是这样的。只要嵌套 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>
);

1.5.4. 响应式路由

2. API

2.1. Hooks

React 16.8 版本之后,React Router 利用 hook 机制添加了几种常用的 hooks:

2.2. <BrowserRouter>

<Router> 使用 HTML5 history API 保证 UI 界面与 URL 同步。

<BrowserRouter
  basename={optionalString}
  forceRefresh={optionalBool}
  getUserConfirmation={optionalFunc}
  keyLength={optionalNumber}
>
  <App />
</BrowserRouter>
  • basename: string 所有路径的基准路径
  • getUserConfirmation: func
  • forceRefresh: bool true 时,新页面导航会强制刷新页面
  • keyLength: number location.key 的长度,默认是 6
  • children: node 要渲染的子元素

2.3. <HashRouter>

<Router> 使用 URL 中的 hash 部分(比如 window.location.hash )来保证你的 UI 与 URL 同步。

注意: hash history 不支持 location.key 或者 location.state 。建议服务器配置和 <BrowserHistory> 一起用。

<HashRouter
  basename={optionalString}
  getUserConfirmation={optionalFunc}
  hashType={optionalString}
>
  <App />
</HashRouter>
  • basename: string
  • getUserConfirmation: func
  • hashType: string window.location.hash 的编码类型,可选的值:
    • slash,类似 #/#/sunshine/lollipops 。 默认选项。
    • noslash,类似 ##sunshine/lollipops
    • hashbang,类似 #!/#!/sunshine/lollipops
  • children: node

2.4. <Link>

应用内部的导航(可路由的 URL)。

<Link to="/about">About</Link>
  • to 导航目标,支持几种类型
    • string 字符串将 location.pathname, search, hash 属性拼接起来

      <Link to="/courses?sort=name" />
      
    • object 包含一下任何属性的对象
      • pathname
      • search
      • hash
      • state

        <Link
          to={{
          pathname: "/courses",
          search: "?sort=name",
          hash: "#the-hash",
          state: { fromDashboard: true }
          }}
        />
        
    • function 当前位置作为参数传给函数,返回字符串形式的或者对象形式的位置

      <Link to={location => ({ ...location, pathname: "/courses" })} />
      <Link to={location => ({ ...location, pathname: "/courses" })} />
      
  • replace: bool true 会替换当前 history 堆栈中的链接,而不是重新添加一个
  • 其它,你也可以传递想要给 <a> 的项目,比如 title,id,className 等等。

2.5. <NavLink>

<Link> 的特殊版本,如果匹配当前的 URL 时,自动添加风格属性。

<NavLink to="/about">About</NavLink>
  • activeClassName: string 激活状态下给定元素的 class ,默认是 active ,或者 className 属性联合起来

    <NavLink to="/faq" activeClassName="selected">
      FAQs
    </NavLink>
    
  • activeStyle: object 激活状态下的元素样式,这是一个对象

    <NavLink
      to="/faq"
      activeStyle={{
        fontWeight: "bold",
        color: "red"
      }}
    >
      FAQs
    </NavLink>
    
  • exact: bool true 时,路径完全匹配 style 才生效
  • strict: bool true 时,匹配是否考虑路径名上的斜杠,更多可查看 <Route strict> 文档

    <NavLink strict to="/events/">
      Events
    </NavLink>
    
  • isActive: func 如果你不想使用默认的判断逻辑的话,可以用该函数自定义,参数是 (match, location)
  • location: object 一般是浏览器 URL,在自定义 isActive 的时候,会传入

2.6. <MemoryRouter>

将 URL 记录保留在内存中(不在地址栏读取或者写入),在测试和非浏览器环境中(React Native)很有用。

2.7. <Redirect>

重定向到另外一个位置,新的位置将覆盖历史记录堆栈中的当前位置,类似服务端的 redirects (HTTP 3xx) 干的事情。

  • to: string 字符串表达的位置
  • to: object 对象表达的位置,路径格式必须要被 path-to-regexp@^1.7.0 理解

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

    state 对象可以被重定向的对象直接使用,比如 this.props.location.state.referrer

  • push: bool 如果是 true 的话,重定向将在 history 中新增一条记录,而不是替换当前记录
  • from: string 重定向的来源 URL,参数会被传递到 to 中:

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

    其它的参数会被忽略掉。 注意: 只能用在 <Switch> 中的包含路径。

  • exact: bool 精确匹配,等价于 Route.exact 注意:from 结合使用的时候,只能用 <Switch> 中包含的路径。
  • strict: bool 严格匹配,等价于 Route.strict
  • sensitive: bool 大小写敏感匹配,等价于 Route.sensitive

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

推荐 <Route> 渲染的方法是使用子元素,就上面这样。但事实上,还有一些其它的方法来渲染。这些方法主要是为了兼容 hook 引入之前路由构建的应用程序。

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

你应该在 Route 上只使用上面的其中一个。上面三个渲染方法都会传递相同的 route props:

  • match
  • location
  • history

component

一个 React 组件:

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

当使用组件(而不是 renderchildren 的时候),router 根据给定的组件使用 React.createElement 创建一个新的 React 元素。 也就是说,如果向组件 prop 提供了一个内联函数,那么会在每次渲染的时候创建一个新的组件。会导致现有的组件卸载和新组件的装载,而不仅仅是更新现有组件。 使用内联函数渲染时,请使用 render 或者 children

render: func

可以方便的内联渲染和包装,无需上面说的不必要的重新安装。当匹配路径的时候,你可以传入一个函数。渲染函数与 component 具有相同的属性(match,location,history)。

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> ,所以不要同时使用。

children: func

TODO,看不懂。

支持的参数:

  • path: string | string[]

https://reacttraining.com/react-router/web/api/Route/path-string-string

First created: 2019-11-27 14:41:40
Last updated: 2022-12-11 Sun 12:49
Power by Emacs 29.0.91 (Org mode 9.6.6)