解读React Router V6官方文档(一)——主要概念篇

595 阅读9分钟

前文

花了两三天时间学习了最新的React Router V6官方文档,在这里归纳一下主要知识点。官网是全英文的技术文档,若有错误请大家指正。

主要概念

React Router不只是将url匹配到一个组件:它建立了一个多功能的用户接口映射到URL,相比日常的简单使用,它有着更多概念需要去了解。

我们将从三个主要方面去了解React Router如何工作的。

  • 订阅和操作history栈

  • URL和routes匹配

  • 渲染route UI

定义

在前后端框架中,对路由都有着许多不同的定义,在不同的环境下 也会有着不同的意思。

  • URL:地址栏中的URL

  • Location:React Router的特有对象,基于浏览器内置的window.location,代表着“用户在哪里”,它是与URL相关的对象,但包含了更多的信息

  • Location State:location的状态值,没有编码在url中。类似于hash、seartch参数(参数写在url中),区别在于存储在浏览器内存中且不可见

  • History Stack:浏览器通过追踪location栈来跳转。点击回退按钮按住不动就能看到历史记录栈

  • Client Side Routing(CSR):客户端路由可以让开发者在不请求服务端的情况下,操作浏览器的历史记录栈

  • History Action:POP、PUSH、REPLACE的一种,用户可以通过这些操作跳转到其他的URL

  • Push:当点击一个链接或者强制跳转就会将一个新的url push 到History Stack

  • Replace:替换掉当前的url

  • Pop:用户点击回退或者前进按钮发生的行为

  • Segment:分隔符/

  • Path Pattern:带有特殊字符用于匹配路由的表达式

  • Dynamic Segment:动态的path pattern,可以匹配多个值,比如/users/:userId可以配对/users/123,/users/223

  • URL Params:从匹配成功的url上解析的值

  • Router:带有状态的最高层级的组件,其他路由组件和hook基于它工作

  • Route:Route对象或者Route元素,格式可为{ path,element}或者,path是一个path表达式,当前url和path匹配,则会渲染对应的element

  • Nested Routes:嵌套路由

  • Relative Links:相对路径

  • Match:当路由配对后生成的对象,包含着匹配的路由信息如UR参数和path

  • Matchs: 和当前location匹配的一组路由或者route config分支,也是嵌套路由的结构

  • Parent Route:带有子路由的路由

  • Outlet:在所有配对的Matchs中渲染下一个match

  • Index Route:没有path的子路由

  • Layout Route:没有path的Parent Route,专用于形成带有特殊样式的子路由组

History和Location

React Route发挥作用前,必须订阅发生在浏览器history stack的变化。

当用户使用浏览器导航的时候,浏览器便维护着自己的history stack

例子,当用户

1.点击link到/lodashboard

2.点击link到/accounts

3.点击link到/customers/123

4.点击回退按钮

5.点击link到/dashboard

history stack如下

1./dashboard

2./dashboard,/accounts

3./dashboard,/accounts,/customers/123

4./dashboard,/accounts,/customers/123(这里有疑惑,点击回退应该是触发了pop为./dashboard,/accounts)

5./dashboard,/accounts,/dashboard

History对象

在客户端路由中,开发者操作浏览器的history stack

如下例子

<a
  href="/contact"
  onClick={(event) => {
    // stop the browser from changing the URL and requesting the new document
    event.preventDefault();
    // push an entry into the browser history stack and change the URL
    window.history.pushState({}, undefined, "/contact");
  }}
/>

================说明:不要在Raect路由中直接使用window.history.pushState============

以上代码改变了URL但是并没有改变UI,我们需要写另一段代码来改变某些地方的状态,已使得当前的页面改变UI

通过pop事件监听

window.addEventListener("popstate", () => {
  // URL changed!
});

但是pop事件只能用于前进或者回退按钮,没有事件与window.history.pushStatewindow.history.replaceState相关

这时候就需要React Route的特殊对象history大显身手了,它可在触发history action的时候监听url变化

let history = createBrowserHistory();
history.listen(({ location, action }) => {
    ...
})

不用专门设置hisory对象,这是Router的事情。它设置了这些对象,订阅了history stack的变化,并当URL变化的时候改变状态,使得可以重新渲染正确的UI界面。

Location对象

浏览器有一个window.location对象,它提供了url的信息和改变它的方法

// URL: https://reactrouter.com/docs/en/v6/getting-started/concepts#locations

window.location.pathname //getting-started/concepts 
window.location.hash; // #location
window.location.reload(); // 刷新


React Router也有location对象,看起来更简单

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

statekey是React Router特有的,其它属性也存在window.location中

  • Location Pathname

URL中路由匹配成功的那一部分字段

https://example.com/teams/hotspurs的pathname就是/teams/hotspurs
  • Location Search

在React Router中又被称为URLSearchParams

let location = {
  pathname: "/bbq/pig-pickins",
  search: "?campaign=instagram&popular=true",
  hash: "",
  state: null,
  key: "aefz24ie",
};

let searchParams = new URLSearchParams(location.search);
searchParams.get('campaign') // "instagram"
searchParams.get('popular') // "true"
params.toString();
  • Location State

浏览器允许我们通过pushState持久化存储信息

当用户点击back,history的state值会变为之前的

window.history.pushState('look ma!', undefined, '/contact');
window.history.state; // 'look ma'
// user clicks back
window.history.state // undefined

一个好的Location用例需要做到以下几点

  • 告知下一个页面,用户来自哪里并分配到不同的UI界面。比较常见的应用例子:在旧版INS上,如果用户单击了网络视图上的一个项目,就将记录显示在弹窗上,如果是直接通过URL访问,则记录显示在用户当前页面

  • 将部分记录发送给下一个界面,便于它立即渲染部分数据,并获取到剩下的数据

设置location的两种方式Linknavigate

<Link to='/users/123' state={{ fromDashboard: true }} />

useNavigate('/users/123',{ state: partialUser })

可以通过useLocation获取它

let location = useLocation();
location.state;
  • Location Key

每个location都有一个唯一的key,这对于基于位置的滚动管理、客户端数据缓存等情况非常有用。

有了唯一的key,可以建立一个纯对象new Map()或者locationStorage存储信息

例如,一个非常基础的客户端数据缓存可以通过location key存储数据,并且当用户点击返回的时候,可以跳过再次发送请求获取数据

let cache = new Map();

function useFakeFetch(URL) {
    let location = useLocation();
    let cacheKey = location.key + URL;
    let cached = cache.get(cacheKey);
    
    let [data, setData] = useState(() => {
        return cached || null;
    })
    
    // 用缓存数据即为done,否则为loading
    let [state, setState] = useState(() =>{
        return cached ? 'done' : 'loading'
    })
    
    useEffect(() => {
        if(state === 'loading') {
           // 请求数据,并保存在cache中
           fetch(URL).then(res =>{
               cache.set(cacheKey, data);
               setData(data);
           })
        }
    },[state, cacheKey])
    
    useEffect(() => {
        setState("loading")
    },[URL])
}

Matching匹配

在初始化渲染,history stack改变时候,React Router将会根据你的router config匹配location,并且生成一系列matches

Defining Routes

route config以树的形式展现routes结构

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>

生成的树形对象

let routes = [
  {
    element: <App />,
    path: "/",
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "teams",
        element: <Teams />,
        children: [
          {
            index: true,
            element: <LeagueStandings />,
          },
          {
            path: ":teamId",
            element: <Team />,
          },
          {
            path: ":teamId/edit",
            element: <EditTeam />,
          },
          {
            path: "new",
            element: <NewTeamForm />,
          },
        ],
      },
    ],
  },
  {
    element: <PageLayout />,
    children: [
      {
        element: <Privacy />,
        path: "/privacy",
      },
      {
        element: <Tos />,
        path: "/tos",
      },
    ],
  },
  {
    element: <Contact />,
    path: "/contact-us",
  },
];

除了使用还可以用useRoutes(routesGoHere)配置路由结构

Match Params

:teamId这种片段,就是用来动态匹配path的片段

如/team/123,/team/abc都可以匹配/team/:teamId,teamId可以是123,也可以是abc

Ranking Routes

收集所有的route config中的path匹配式子

可以得到以下的内容

[  "/",  "/teams",  "/teams/:teamId",  "/teams/:teamId/edit",  "/teams/new",  "/privacy",  "/tos",  "/contact-us",];

那么URL :/teams/new可以匹配哪些路由呢?

答案是

/teams/new /teams/:teamId

在这里React Route必须选择其中一个。这需要我们去为routes设置优先级来得到预期的结果

在React Router,它会根据分段数、静态分段、动态分段、star segment等对你的路由进行排序,并选择最具体的匹配。

所以这里会选择**/teams/new**

Pathless Routes

<Route index element={<Home />} />

<Route element={<PageLayout />} />

这些路由都没有path,Home是index路由, PageLayout是layout路由

Route Matches

当一个route匹配一个URL后,它可由一个match对象表示

路由匹配如

<Route path=":teamId" element={<Team />} />

得到的对象如下

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

pathname是URL匹配路由的那部分

params是动态分段匹配后解析的值,注意:这里的itemId变成了对象属性,值变成了对象的值

因为routes是一个树,一个URL可以匹配整棵树的分支

URL/teams/firebirds,可以生成如下路由分支

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>

React Route将会根据这些路由生成一个matches对象组

[
  {
    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",
    },
  },
];

Rendering渲染

观察下如果的app入口是这样的

const root = ReactDOM.createRoot(
  document.getElementById("root")
);
root.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />}>
        <Route index element={<Home />} />
        <Route path="teams" element={<Teams />}>
          <Route path=":teamId" element={<Team />} />
          <Route path="new" element={<NewTeamForm />} />
          <Route index element={<LeagueStandings />} />
        </Route>
      </Route>
      <Route element={<PageLayout />}>
        <Route path="/privacy" element={<Privacy />} />
        <Route path="/tos" element={<Tos />} />
      </Route>
      <Route path="contact-us" element={<Contact />} />
    </Routes>
  </BrowserRouter>
);

以**/teams/firebirds为例子,将会将locationroute config**匹配,得到一组matches集合

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

Outlets

Routes会默认渲染第一个元素(在我们案例中是App)

下一个配对元素是Teams,App可以用Outlet设置Teams的渲染位置

Outlet指的是下一个路由渲染位置

function App() {
  return (
    <div>
      <GlobalNav />
      <Outlet />
      <GlobalFooter />
    </div>
  );
}

Index Routes

/teams的route config如下

<Route path="teams" element={<Teams />}>
    <Route path=":teamId" element={<Team />} />
    <Route path="new" element={<NewTeamForm />} />
    <Route index element={<LeagueStandings} />
</Route>

如果URL是/teams/friebirds

matches树结构是

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

如果URL是/teams,matches树结构是

<App>
    <Teams>
        <LeagueStandings />
    </Teams>
</App>

为什么会有LeagueStandings?因为它是一个index route,当URL是父路由的路径,index route会作为父路由的Outlet

如果你没有匹配任何的子路由路径,那么Outlet将什么都不渲染。

<App>
    <Teams />
</App>

为了避免页面空白,需要一个index route作为默认的路由页面

Layout Routes

匹配/privacy

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>

最终渲染的组件树结构为

<PageLayout>
    <Privacy />
</PageLayout>

PageLayout并没有参与路由匹配(虽然其子节点参与)。它存在的目的是让多个子路由界面都使用用于的布局样式。如果不去设置它,就需要在应用中手动设置它们

Navigating导航

React Router中有两种导航方式

  • Link

  • navigate

Link

导航的主要方式,允许用户点击后改变URL。React Router会阻止浏览器的默认行为,并且将一个新的入口添加到history栈。location改变后,新的匹配路由也会渲染。

嵌套路由不只是渲染了布局;它们也允许"相对links"。

如之前的teams路由

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

组件可以这样渲染Links

<Link to="psg" />
<Link to="new" />

和它们对应的Links的完整路径是/teams/psg和/teams/new,它们继承了已经渲染的路由路径,使得你不必关心其他多余的路由路径是什么。

导航函数

这个函数是在useNavigate中返回的,方便在纯js中使用

let navigate = useNavigate();
useEffect(() => {
  setTimeout(() => {
    navigate("/logout");
  }, 30000);
}, []);

在使用navigate和Link之间是需要权衡的,官方并不推荐这样做

<li onClick={() => navigate("/somewhere")} />

除了Links和form,很少的交互需要改变URL,因为它带来了是否可访问和用户期望这些问题的复杂性。

Data Access数据获取

最后,应用需要从React Router中获取一些信息来建立完整的UI,为此,React Router提供了一些Hooks

let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();

Review总结回顾

综上所述,通过一个例子分析React Route的生成和渲染过程

  • 以这种方式渲染你的app

  • 创建了一个history对象,将初始location放入state中,并订阅URL

  • .递归子路由来建立route config,根据location匹配路由,建立了一些路由matches并且渲染第一个匹配的路由元素

  • 在每个父路由中渲染Outlet

  • outlet会渲染下一个即将匹配的路由

  • 用户点击链接Link

  • Link调用navigate()

  • history改变url并且通知

  • 再渲染,并回到第二步