React Router 6

  路由的概念,可以想像一下路由器,当来了一个请求时,路由器做了什么事情?它会把请求的IP地址和路由表进行匹配,匹配成功后,进行转发,直到目标主机。可以看到路由有三部分组成,一个是请求,一个是路由表,一个是匹配转发。对应到前端路由也是一个道理,只不过前端路由是拦截请求,显示不同的页面内容。首先要发起请求,说明你要到哪里去,React Router中定义了<Link>。其次要定义一个路由表,列出匹配规则和匹配成功后要显示什么,就是React Router中一条条的<Route>。最后就是来了请求时进行动态匹配和转发,React提供了<BrowserRouter>和<Routes>。<BrowserRouter> 把<Routes>包起来,<Routes>把<Route>包起来,来了请求,它就能匹配路由表。

  <Link> 有一个to属性,就是标明去哪里

<Link to="/home">Home</Link>

  <Route> 有两个属性,一个是path,就是列出匹配规则,一个是element,就是匹配成功后要显示什么内容。如果请求‘/home’,就显示<Home>组件内容,就可以如下定义

<Route path=’/home’, element={<Home/>} /> // 要写好Home 组件。

  要注意的是elemet 接受的真是React Element。这里只是定义一条路由,路由表里的一条记录。当要匹配其它请求时,还要再写路由, 比如About

<Route path=’/about’, element={<About/>} />

  要匹配多少请求,就要写多少条路由。这样一条条的路由就定义好了,放到<BrowserRouter>和<Routes>下面,只要来了请求,就能进行动态匹配。<BrowserRouter>提供页面的URL(Provides the cleanest URLs)。<Routes>进行动态匹配。

  使用create-react-app创建项目router-tutorial,然后cd router-tutorial 并npm install react-router-dom。 在index.js中引入BrowserRouter 和<Routes>, BrowserRouter把Route包起来。整个index.js如下

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home, About, Contact, Products, Events } from "./pages";

ReactDOM.render(
  <BrowserRouter>
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='/about' element={<About />} />
      <Route path='/contact' element={<Contact />} />
      <Route path='/products' element={<Products />} />
      <Route path='/events' element={<Events />} />
    </Routes>
  </BrowserRouter>,
  document.getElementById('root')
);

  为了让路由起作用,还要创建Home, About 等组件。在src目录下,新建一个pages.js文件,内容如下:

import React from 'react'

// 首页内容
export const Home = () => (
  <section className="home">
    <h1>企业网站</h1>
    <p>首页内容</p>
  </section>
)

// 企业事件内容
export const Events = () => (
  <section className="events">
    <h1>企业大事件</h1>
  </section>
)

// 公司产品 
export const Products = () => (
  <section className="products">
    <h1>公司产品:手机、电脑</h1>
  </section>
)

// 联系我们
export const Contact = () => (
  <section className="contact">
    <h1>联系我们</h1>
    <p>公司电话:0755 - 12345678</p>
  </section>
)

// 关于我们
export const About = () => (
  <section className="about">
    <h1>公司理念</h1>
    <p>公司以人为本</p>
  </section>
)

  npm start,localhost:3000

  怎么访问其它页面的内容呢?使用<Link />, <Link>表示要到哪里去,只要设置它的to属性和Route中的path属性一一对应就可以了,如<Link to=’/about’>关于我们</Link>,就表示要到/about下面。Home组件中增加四个<Link>

import { Link } from "react-router-dom";
export const Home = () => (
  <section className="home">
    <h1>企业网站</h1>
    <p>首页内容</p>
    <nav>
      {/* 添加了四个导航组件Link */}
      <Link to='/about'>关于我们</Link>
      <Link to='/events'>企业事件</Link>
      <Link to='/products'>公司产品</Link>
      <Link to='/contact'>联系我们</Link>
    </nav>
  </section>
)

  这时,点击不同的link 就去到不同页面,同时它还会改变地址栏,这时如要在地址栏中随便输入一个路径,页面一片空白,因为没一个路由和它匹配。最好写一个匹配不成功的路由,来处理一下这种情况。那路由的path怎么写?用“*”。要显示的组件可以随便写一下,在pages.js 下面再写一个组件, 

export const NotFound404 = () =>(
  <div className="whoops-404">
      <h1>没有页面可以匹配</h1>
  </div>
)

  路由就是

<Route path='*' element={<NotFound404/>}></Route>

  和普通路由一样,把它加到<Routes>组件下面。*表示,其它路由都不匹配的时候,才匹配它。

<BrowserRouter>
    <Routes>
      <Route path='/' element={<Home/>} />
      <Route path='/about' element={<About />} />
      <Route path='/contact' element={<Contact />} />
      <Route path='/products' element={<Products/>} />
      <Route path='/events' element={<Events/>} />
      <Route path='*' element={<NotFound404/>}></Route>
    </Routes>
  </BrowserRouter>

   此时,路由有一个问题,那就是点击<About>之后,回不去了,只能点击浏览器的回退按钮,要是页面始终展示导航条就好了。由于导航条在Home组件,也就是说Home组件始终要显示。About等组件的内容都是在点击之后才会显示,也就是先显示Home组件,再显示 About组件,只有先导航到home路由,才有机会导航到about的路由, home路由是about路由的父路由,在React Router 6中,Route可以嵌套,只要一个<Route>包含其它Route,它就是父路由,那么路由就可以这么写

<BrowserRouter>
    <Routes>
      <Route path='/' element={<Home />} >
        <Route path='about' element={<About />} />
        <Route path='contact' element={<Contact />} />
        <Route path='products' element={<Products />} />
        <Route path='events' element={<Events />} />
      </Route>
      <Route path='*' element={<NotFound404 />}></Route>
    </Routes>
  </BrowserRouter>

  子路由前面的/可以去掉,React会自动组合(父路由/+子路由"about")。那子路由匹配成功后,要展示内容放到什么地方? 由于是子路由,肯定要先匹配父路由,只有父路由匹配成功了,才能匹配子路由。也就是只有父路由对应的组件展示出来了,才有机会展示子路由对应的内容,子路由的内容应该放到父路由的对应的组件里面,也就是About等组件要放到Home组件里面 。那具体怎么写呢?React Router 提供了<Outlet>组件,只要子路由匹配成功,<Outlet />组件可以动态成渲染子路由定义的组件内容。<Outlet />放到Home组件中,具体放到哪里,就看业务需要,比如与导航条并列

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

// 首页内容
export const Home = () => (
  <section className="home">
    <h1>企业网站</h1>
    <p>首页内容</p>
    <nav>
      {/* 添加了四个导航组件Link */}
      <Link to='/about'>关于我们</Link>
      <Link to='/events'>企业事件</Link>
      <Link to='/products'>公司产品</Link>
      <Link to='/contact'>联系我们</Link>
    </nav>
    <Outlet />
  </section>
)

  点击每一个<Link>, 和它匹配成功的子路由所定义组件,都会正确地渲染,并且是渲染在<Outlet> 位置,正确的组件替换掉了<Outlet>。

  给整个组件添加点样式,pages.css 内容如下,并在index.js中引用

html, body, #root {
  height: 100%;
}
h1 {
  font-size: 3em;
  color: slategray;
}
/* home 组件 */
.home {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.home > nav {
  display: flex;
  justify-content: space-around;
  padding: 1em;
  width: calc(100% - 2em);
  border-top: dashed 0.5em ghostwhite;
  border-bottom: dashed 0.5em ghostwhite;
  background-color: slategray;
}

.home > nav a {
  font-size: 2em;
  color: ghostwhite;
  flex-basis: 200px;
}

/* 其它组件 */
section.events,
section.products,
section.contact {
  flex-grow: 1;
  margin: 1em;
  display: flex;
  justify-content: center;
  align-items: center;
}
/* 404页面 */
.whoops-404 {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99;
  display: flex;
  width: 100%;
  height: 100%;
  margin: 0;
  justify-content: center;
  align-items: center;
  background-color: darkred;
  color: ghostwhite;
  font-size: 1.5em;
}

  整个页面如下展示

 

 

   这时也会发现一个问题,刚进入页面时,页面只展示了导航条, 没有显示实际的内容,只有点击某个导航后,才显示内容,这显然不合适,即使不点击,也要看到核心内容,比如公司产品。针对此种情况,React Router 提供了索引路由,

<Route path='/' element={<Home />} >
        {/* 索引路由 */}
        <Route index element={<Products/>} /> 
        <Route path='about' element={<About />} />
        <Route path='contact' element={<Contact />} />
        <Route path='products' element={<Products />} />
        <Route path='events' element={<Events />} />
</Route>

  索引路由和父路由共享路径。当localhost:3000时,path='/'匹配成功,渲染<Home>组件,渲染过程中有<Outlet>组件,就要去匹配子路由,由于索引路由的路径也是父路由的路径,此时URL就是父路由的路径,匹配成功,显示Products组件,也可以把索引路由看作是默认的子路由,当没有其它子路由匹配的时候,就渲染它,总要显示一个子路由吗?稍微修饰一下公司产品模块,列出几个产品,比如手机,电脑等,

export const Products = () => (
  <section className="products">
    <Link to='/details/telphone'>手机</Link>
        &nbsp;
        <Link to='/details/computer'>电脑</Link> 
  </section>
)

  当点击某个产品,进入产品详情页面,在pages.js中加一个详情组件

// 产品详情组件
export const Details = () => {
  return <p>详情内容</p>
}

  问题是当跳转到产品详情页面时,它怎么知道是哪个产品呢?匹配的路由怎么写?路由的格式就是路径后面加上冒号 ,再加参数,比如details/:type,组件中可以使用useParams获取。为了展示,把路由写到根路由index.js下

<Route path='products' element={<Products />} />
<Route path='details/:type' element={<Details />} />
<Route path='events' element={<Events />} />

  同时Details组组件改为

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

export const Details = () => {
  let params = useParams();
  return <p>产品: {params.type}</p>
}

  还有 一个小问题,导航条能不能提示位于哪个导航上?React Router 提供了NavLink 组件, 它和Link功能是一样的,都是标识请求,只是使用的场景不一样。navLink 提供了一个isActive 属性,可以设置高亮样式。

 <NavLink to='/about' className={({ isActive }) => isActive ? "selectedStyle" : ""}>关于我们</NavLink>

  当选中之后,isActive是true。

  除了这种声明式的路由,也可以使用编程式路由,核心就是useNavigate. 在Detail里面,添加button,跳转到Product。

import { useParams, useNavigate } from "react-router-dom";
export const Details = () => {
  let params = useParams();
  let navigate = useNavigate();
  return (
    <p>
      产品: {params.type}
      <br />
      <button onClick={() => {
        navigate("/products");
      }}>后退</button>
    </p>
  );
}

  React Router 主要概念

  React Router三个主要作用:监听(订阅)和操作浏览器的历史记录,匹配URL到你配置的路由,从匹配的路由中渲染嵌套的UI(完整的UI)

  location是基于浏览器内置的window.location对象,React Router 自己拥有的一个特殊的对象。它表示用户在哪里,几乎就是URL的对象表示,但包含的信息比URL多。Location State则是存在Location对象上的一个值,但它并没有在URL中显示。它存在浏览器的内存中,并不可见,样子像hash或搜索参数。当用户在网页中导航时,浏览器持续追踪每一个location,形成浏览器的历史记录。

  history也是一个对象,它使React Router 可以订阅URL的变化,并提供API来操作浏览器的历史记录。

  segment:URL和路径模式中两个/之间的部分,比如'/user/123', user和123都是segment。路径模式很像URL,但它包含特殊字符,比如 "/users/:userId"和 "/docs/*" ,一个包含:,一个包含*。路径模式下“:userId” 又称为动态segment,因为它能匹配任意值。URL params就是匹配动态segment成功的值。/users/123 匹配/users/:userId, 123就是url params。

  Match也是一个对象,它包含了URL和路由匹配成功的信息,比发path和URL Params。Matches是和当前URL匹配成功的一组路由。

  客户端路由时,开发者可以通过API,如history.pushState(),来操作浏览器的历史记录,而不会发送服务器请求,但它仅仅是改变了URL,并没有改变UI。我们需要做的是改变URL的同时,改变UI。问题是浏览器并没有提供一个方法来监听URL,从而订阅变化。为此,React Router创建了histroy 对象,来监听URL的变化。<Router>,就是<BrowserRouter>,它会创建history对象,并订阅浏览器历史记录的变化,当URL变化时,它会更新history对象的state,从而引起APP的重新渲染,正确的UI显示出来。history的state是一个location对象,如下所示

{
  pathname: "/bbq/pig-pickins",
  search: "?campaign=instagram",
  hash: "#menu",
  state: null,
  key: "aefz24ie"
}

  pathname,路由匹配的部分,路由进行匹配的时候,只匹配pathname。state,来自于history.pushState()。pushState()第一个参数就是state, 但它不会改变URL。React Router 进行抽象,把state放到了location对象中。改变location 的state有两种方式

<Link to="/pins/123" state={{ fromDashboard: true }} />;

let navigate = useNavigate();
navigate("/users/123", { state: partialUser });

  在跳转到的page,可以使用useLocation获取到

let location = useLocation();
location.state;

  只要改变URL,就会改变location 对象,想要获取URL的信息,就使用location对象。当URL改变后,React Router就用location来匹配你配置的路由,然后把匹配成功的拿出来,进行渲染。配置的路由就是<Routes>和<Route>组件。

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path="new" element={<EditTeam />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
  </Route>
</Routes>

  <Routes>会递归遍历它的children,也就是<Route>,把属性拿出来,形成一个对象。

let routes = [
  {
    element: <App />,
    path: "/",
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: "teams",
        element: <Teams />,
        children: [
          {
            path: ":teamId",
            element: <Team />
          },
          {
            path: "new",
            element: <NewTeamForm />
          }
        ]
      }
    ]
  },
  {
    element: <PageLayout />,
    children: [
      {
        element: <Privacy />,
        path: "/privacy"
      }
    ]
  }
];

  形成一个总的路由配置表

[
  "/",
  "/teams",
  "/teams/:teamId",
  "/teams/new",
  "/privacy"
];

  如果现在有一个URL是/teams/new,哪个路由会匹配它,有两个

/teams/new
/teams/:teamId

  这时React Router 必须做决定,因为只能有一条路由匹配,React Router会对你路由进行排名,依据就是路由中的segment, 静态,动态,数量等等,找出最精准匹配的那一条路由。在这里,就是 /teams/new 这条路由。当一条路由成功匹配到URL后,它会以match对象的方式进行展示。匹配到<Route path=":teamId" element={<Team/>}/>这条路由,将会生成下面这个对象

{
  pathname: "/teams/firebirds",
  params: {
    teamId: "firebirds"
  },
  route: {
    element: <Team />,
    path: ":teamId"
  }
}

  由于路由是树形结构,一个URL可能匹配到树的整个分支。/teams/firebirds

<Routes>
  <Route path="/" element={<App />}>
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
    </Route>
  </Route>
</Routes>

  React Router从这些路由和URL中,会创建一组匹配的路由,从而渲染出嵌套的UI来匹配嵌套的路由。

[
  {
    pathname: "/",
    params: null,
    route: {
      element: <App />,
      path: "/"
    }
  },
  {
    pathname: "/teams",
    params: null,
    route: {
      element: <Teams />,
      path: "teams"
    }
  },
  {
    pathname: "/teams/firebirds",
    params: {
      teamId: "firebirds"
    },
    route: {
      element: <Team />,
      path: ":teamId"
    }
  }
];

  有了匹配的路由,渲染React Element树就简单了

<App>
  <Teams>
    <Team />
  </Teams>
</App>

   看一下"/privacy",它匹配的路由是

<Route path="/" element={<App />}>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
  </Route>
</Route>

  要渲染的React Element树

<App>
  <PageLayout>
    <Privacy />
  </PageLayout>
</App>

  PageLayout路由有点奇怪,没有path属性,只有Element属性,称为布局路由,仅仅用来做布局的。

  V5 和V6的不同

  1,React Router v6 使用了大量的React Hooks,因此升级V6前,要先升级React到16.8以上。

  2,使用<Routes> 代替<Switch>,<Routes>使用的是最佳匹配路由算法,并且路由能嵌套

  3,组件内部有<Link>和<Route>时,<Link>的to属性和Route>的path属性,不用再手动构建,而是直接写

<Route path={`${match.path}/:id`}> -> <Route path=":id" element={<UserProfile />} />

  此时<Route path>和<Link to> 是相对路由和Link,它们自动构建在父路由path和URL上。对应的,当路由有后代路由,且这些路由是定义在其它组件中,路由的path要用*,表示深度匹配。

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path=":id" element={<UserProfile />} />
        <Route path="me" element={<OwnUserProfile />} />
      </Routes>
    </div>
  );
}

  4,<Route>使用element,而不是compoent, render属性,element的取值也是React Element,好处就是可以像普通的React Element传递属性

// animate 是自定义属性
<Route path=":userId" element={<Profile animate={true} />} />
// 组件使用
function Profile({ animate }) {
  let params = useParams();
  let location = useLocation();
}

  5, <Route> 的path属性不再接受正则表达式,/users/:id? 无效了。

  6,react-router-config包里的功能都集成到V6中,使有useRoute,而不是react-router-config

function App() {
  let element = useRoutes([
    // These are the same as the props you provide to <Route>
    { path: "/", element: <Home /> },
    {
      path: "invoices",
      element: <Invoices />,
      // Nested routes use a children property, which is also
      // the same as <Route>
      children: [
        { path: ":id", element: <Invoice /> },
        { path: "sent", element: <SentInvoices /> }
      ]
    },
    // Not found routes work as you'd expect
    { path: "*", element: <NotFound /> }
  ]);

  // The returned element will render the entire element
  // hierarchy with all the appropriate context it needs
  return element;
}

  当进行服务端渲染时,要用matchRoutes 

  7,useNavigate 代替了useHistory , Navigate 组件代替了Redirect 组件

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

function App() {
  return <Navigate to="/home" replace state={state} />;
}

  8, <Link>没有了component属性,只能渲染成标签。

  9,<NavLink/> 去掉了activeClassName 和 activeStyle, 要使用isActive

  10,StaticRouter 称动了react-router-dom/server.

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

 

posted @ 2022-02-15 14:05  SamWeb  阅读(1319)  评论(0编辑  收藏  举报