React Router | 阅读文档后的一些理解

673 阅读6分钟

react-router

  • 为什么一个项目只有一个html文件?可不可以多几个html文件?

    之前我的一个朋友问我这个问题,我的回答是不清楚,没遇到过需要多个html文件的需求。

    这显然是忘记了“路由”,准确来说是客户端路由。client side routing 的目的就是解决请求多个html文件的需求,因为每次请求过来的html文件都需要现渲染,用户体验很差,而使用路由则不需要刷新页面并更新内容。路由并不是向web服务器请求html文件,而是本地渲染UI。

React Router 有一些概念:

Main Concepts v6.10.0

全貌了解

假设你构建了如下路由:

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

你开始渲染你的app

  • <BrowserRouter> 创建了一个 history ,并且将初始的 location 放入 state 中,然后订阅URL的变化
  • <Routes> 递归它的子路由以构建一个 route config,与 location 一起匹配这些路由,生成路由匹配数组,数组中每个成员都代表当前路径分别匹配的组件,但是只渲染第一个匹配成员
  • 如果在渲染某个组件时遇到了 <Outlet/>,则会接着渲染匹配数组中的下一个成员
  • 如果用户的交互有导致url改变,比如点击链接、登录跳转、编辑后跳转…..
  • history 便会通知 <BrowserRouter> ,然后重新渲染,即再重复一次该流程

Router

Picking a Router

在React Router v6中建议使用 data APIs

  • createBrowserRouter
  • createMemoryRouter
  • createHashRouter
  • createStaticRouter

Elements style routes 使用 data APIs


对于一些使用非data APIs的老项目而言,更新很简单,这些老项目配置routes的方式是Elements的形式,而dataAPIs接受参数为routes array,只需要使用createRoutesFromElements 即可

import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter(
  **createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="dashboard" element={<Dashboard />} />
      {/* ... etc. */}
    </Route>
  )**
);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);
  • 为什么非要使用data APIs?有什么好处吗

    只有data APIs产生的routes才能使用 data router,如 route.action route.errorElement route.loader.….

一般使用 createBrowserRouter 即可,如果是不能使用完整URL的情况则使用 createHashRouter

routes

全貌

routes 可以说是 React Router 总重要的部分了。它们将 URL segments 与 components 、data loading 、data mutation 结合。

routes 简单地说就是传给 router 创建函数的对象们

来看看对 route 对象的定义

interface RouteObject {
  path?: string;
  index?: boolean;
  children?: React.ReactNode;
  caseSensitive?: boolean;
  id?: string;
  loader?: LoaderFunction;
  action?: ActionFunction;
  element?: React.ReactNode | null;
  Component?: React.ComponentType | null;
  errorElement?: React.ReactNode | null;
  ErrorBoundary?: React.ComponentType | null;
  handle?: RouteObject["handle"];
  shouldRevalidate?: ShouldRevalidateFunction;
  lazy?: LazyRouteFunction<RouteObject>;
}
✨ 路径区分大小写
  • Path

对于一条 URL 而言,可以被分成多段匹配结果,比如 /temps/rocket/123 可以匹配到 /temps /temps/rocket /temps/rocket/123 ,而每一结果中的 URL片段就是 path,也就指向一个组件,也就是说例子匹配到了三个组件

  • Layout Routes

没有 path,但是有 chidren 的route

  • Dynamic Segments

以 : 开头的 Path Segments 就是 Dynamic Segments

在路径匹配阶段,它们会被解析,然后以“params”的形式传给 router APIs,比如 loader action useParams

  • Optional Segments

以 ?结尾的 Path Segments 就是 Optional Segments

它们是可选的,就是对于最终的 URL 而言这段片段可以跳过

无论静态片段还是动态片段都可以是可选片段

action

action 的作用就是将 data mutation 的工作交给了 route

调用 action 的方法是任何发送给 route 的 non-get submission

// forms
<Form method="post" action="/songs" />;
<fetcher.Form method="put" action="/songs/123/edit" />;

// imperative submissions
let submit = useSubmit();
submit(data, {
  method: "delete",
  action: "/songs/123",
});
fetcher.submit(data, {
  method: "patch",
  action: "/songs/123/edit",
});

request

action function 会接受参数 request,它是 Fetch Request 的实例,一般是使用其中的 formData

<Route
  action={async ({ request }) => {
    let formData = await request.formData();
    // ...
  }}
/>

errorElement

只要是在渲染该路由的 loading action component 时抛出了错误,就会转而渲染路由对应的 errorElement,如果路由没有指定 errorElement ,则会冒泡到父级路由

errorElement 其实也是一个组件,既然是想编写一个跟错误有关的组件,那肯定需要知道错误是什么,使用 useRouteError Hook 即可

loader

loader 让路由对应组件渲染前能够拿到所需数据

一般是 loader function 内请求数据,返回数据,组件内使用 useLoaderData Hook 拿到数据

request

loader function 的任务是获取数据,为什么还会接受参数 request 呢

以超链接为例

<a
  href={props.to}
  onClick={(event) => {
    event.preventDefault();
    navigate(props.to);
  }}
/>

如果没有js,默认行为是发送get请求给web服务器,而 React Router 阻止了默认行为,并传递 request 给 loader function

最常见的用法就是创建一个 URL 并读取它的 URLSearchParams,比如搜索栏:搜索的目的就是根据条件筛选数据来显示,所以搜索行为肯定需要再次触发 loader function

function loader({ request }) {
  const url = new URL(request.url);
  const searchTerm = url.searchParams.get("q");
  return searchProducts(searchTerm);
}

Components

Form

GET submissons


Form 的默认请求方法是 get,而 get 请求就相当于一次普通的导航(navigation)比如用户点击超链接。

在进行 GET 请求时,参数可以通过 URL 的查询字符串(query string)来传递。查询字符串是 URL 中的一部分,以 "?" 开头,然后是一系列以 "&" 分隔的键值对,例如:

http://example.com/api/users?name=John&age=30

在上面的例子中,查询字符串是 "?name=John&age=30",其中包含两个键值对,分别是 name=John 和 age=30。

Form 的默认行为是,发出请求时会序列化表单内容,添加到 URLSearchParams 对象中,然后与action指定路径合并,形成完整的URL,发送 get 请求

而接受 request 的函数可通过 new URL(request.url).searchParams.get(’q’) 的方式拿到查询字符串

Mutation submissions


除 get 以外的请求方式都成为 Mutation submissions ,而它们的 request 会被传递给 action function ,form 的prop action 指定路由的 action function。

表单内容会被序列化为 FormData 放进 request 中

useSubmit Hook 配合 Form 组件使用,即编程角度的提交

utilities

defer

  • 为什么要用 defer

    当匹配到路由时,需要等待其 loader function 执行完毕才能渲染组件,如果 loader function 执行时间过长,则会出现页面尝试无响应的状态

    这时如果给出正在加载之类的提示信息,则会提高用户体验

    但是往常的解决办法是在组件内使用 [isLoading,setIsLoading] = useState(true) 这样 state 控制 jsx 的形式,然而组件根本不能渲染,这个方案就不可能实现

    或者让父级路由,使用 useNavigation Hook,指定判断路由对应路径,这样过于复杂

    const navigation = useNavigation();
    navigation.state === ‘loading’ 
    ? navigation.location.pathname === ‘yourPath’ 
    ? ‘loading’ 
    : <Out/>
    : <Out/>
    

    而 defer 配合 <React.Suspense/> <Awiat/> ,则可以在 loader function 执行过程中渲染组件中不需要用到 loader 返回 data 的部分,以及需要使用 data 的部分渲染 <React.Suspense/> prop fallback 的内容。

    如下例,loader 执行过程中,会展示 Loading…. 和 lalala

    export async function loader() {
        const res = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('klns');
            }, 3000);
        })
        return defer({
            res: res
        });
    }
    
    export function Profile() {
        const data = useLoaderData();
        return (
            <div className="profile">
                <React.Suspense
                    fallback={<p>Loading...</p>}
                >
                    <Await
                        resolve={data.res}
                        errorElement={
                            <p>Error loading!</p>
                        }
                    >
                        {(res) => (
                            <div>{res}</div>
                        )}
                    </Await>
                </React.Suspense>
                <p>lallala</p>
            </div>
        )
    }