首页 > 其他分享 >4k字介绍 React Router 6.4 超大变化:引入 Data API。你不纯粹了!

4k字介绍 React Router 6.4 超大变化:引入 Data API。你不纯粹了!

时间:2022-12-05 15:01:20浏览次数:48  
标签:const loader React API Router element Data

背景

最近,我在着手重构我的​​《联机桌游合集》​​。后端逻辑已经用 Golang 重构完毕,正在重构前端代码。

之前我将每个桌游当作一个独立的前端项目,每个项目是通过我定义的脚手架模板生成的,并且通过 git submodules 复用公共组件和方法。这么做是为了方便各个桌游独立打包、部署,技术方案写到下面文章里了:

  • ​​《你有多个前端项目,他们目录结构很像?有公共组件函数?该怎么复用?》​​
  • ​​《5种前端代码共享方案:npm包、git submodules、脚手架模板生成、复制、UMD或模块联邦》​​

但是最近也遇到些不便之处,一些全局更新,需要多次部署。我将来一定会做10个以上的桌游,这个问题必须得到解决。

所以最近我在思考其它方案。核心思路就是把所有桌游都放进同一个 React 项目里,在路由上做好按需加载(节约用户访问时间)、按需编译(节约开发者编译时间)等。

我又一次打开 React Router 官方文档,看看有什么更新。

好家伙!这是我认知中的 React Router 吗?

我今年三月开发​​《联机桌游合集》​​时,在用 6.2 版本,那时候 v6 跟 v5 v4 相比,API 已经发生了比较大的变化,但我认可这些变化。

4k字介绍 React Router 6.4 超大变化:引入 Data API。你不纯粹了!_API

现在看完 6.4 版本文档, 我想吐槽。 我的核心观点是:React Router 6.4 不再是纯粹的路由组件了,它耦合了数据获取逻辑。

下面本文 客观介绍: React Router 6.4 引入的新功能 Data API,并在最后给 主观结论

1. 新增 createXXXXRouter API

1.1 介绍

在 React Router 6.4 中,新增了 3 个 ​​createXXXXRouter​​ API,用于支持 data API:

  • ​​createBrowserRouter​​
  • ​​createMemoryRouter​​
  • ​​createHashRouter​​

也就是说,如果你不用这3个API,而是像​​v6.0​​​-​​v6.3​​​一样,直接使用​​<BrowserRouter>​​等下面几个API,那么你享受不到 data API。

  • ​​<BrowserRouter>​​
  • ​​<MemoryRouter>​​
  • ​​<HashRouter>​​
  • ​​<NativeRouter>​​
  • ​​<StaticRouter>​​

1.2 createXXXXRouter 用法

必须结合​​<RouterProvider>​​一起使用。可以看到,它使用一个配置,定义路由。

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";


const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "team",
element: <Team />,
},
],
},
]);

ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);

1.3 也可用JSX定义路由

当然,如果你喜欢用JSX语法定义路由,像​​<BrowserRouter>​​一样:

<BrowserRouter>
<Routes>
<Route index element={<div />} />
</Routes>
</BrowserRouter>

React Router 6.4 也提供了JSX配置,参考​​createRoutesFromElements​​,它有另外一个名字叫​​createRoutesFromChildren​​。

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

2. 的变化

2.1 什么是 Data API?

当你使用​​createXXXXRouter​​​和​​<RouterProvider>​​时,你就可以使用 Data API。

说了这么多,什么是 Data API 呢?

其实就是允许你把「数据获取逻辑」写到路由定义中。每当路由切换到那里时,会自动获取数据。

我们从​​<Route>​​的变化就可以看出,它新增了3个相关的属性:

  • loader
  • action
  • errorElement

2.2 loader 属性

loader属性传入一个函数(允许是 async function),每次渲染「该路由对应的element」前执行函数。在「该路由对应的element」内,可以使用 hook ​​useLoaderData​​ (下文会介绍)来获取这个函数的返回值(通常是http请求的response)。

<Route
loader={async ({ request }) => {
// loaders can be async functions
const res = await fetch("/api/user.json", {
signal: request.signal,
});
const user = await res.json();
return user;
}}
element={<Xxxxxx />}
/>

2.2.1 loader 参数

loader属性传入的函数,允许有2个参数:

  • params: 如果Route中包含参数(例如path是​​/user/:userId​​​,参数就是​​:userId​​,可以通过params.userId获取到路由参数的值)。
  • request: 是 Web 规范中,Fetch API 的 ​​Request​​,代表一个请求。注意:这里指的不是你在 loader 内部发的 fetch 请求,而是当用户路由到当前路径时,发出的“请求”(其实在Single-Page App中,router已经拦截了这个真实的请求,只有Multi-Page App中才会有这个请求),这里是 React Router 6.4 为了方便开发者获取当前路径信息提供的参数,他们按照 Web规范,制造了一个假的 request。你可以通过 ​​request​​ 方便的获取当前页面的参数:
<Route
loader={async ({ request }) => {
const url = new URL(request.url);
const searchTerm = url.searchParams.get("q");
return searchProducts(searchTerm);
}}
/>

不要这个 request 参数行吗?不行,因为如果你用​​window.location​​​获取的信息是当前最新的值,如果用户快速的点击按钮,让页面路由到A,并立马路由到B,这时候路由A对应的Route的loader获取​​window.location​​时,就可能拿到错误的值。

注意,传递 request,还有个好处,它有个 request.signal,当用户快速的点击按钮,让页面路由到A,并立马路由到B,页面A的loader的请求应该被取消掉,可以通过 signal 实现,如下:

<Route
loader={async ({ request }) => {
return fetch("/api/teams.json", {
signal: request.signal,
});
}}
/>

2.2.2 loader 返回值

函数的返回值,将可以在element中通过hook ​​useLoaderData​​ (下文会介绍)来获取。你返回什么,它就拿到什么。

但是 React Router 官方建议,返回一个 Web规范 中的 Fetch API 的 ​​Response​​。

你可以直接 ​​return fetch(url, config);​​,也可以自己构造一个假的 Response:

function loader({ request, params }) {
const data = { some: "thing" };
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json; utf-8",
},
});
}
//...
<Route loader={loader} />

也可以通过 React Router 提供的 json 来构造:

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

function loader({ request, params }) {
const data = { some: "thing" };
return json(data, { status: 200 });
}
//...
<Route loader={loader} />
2.2.2.1 特殊返回值: redirect

在 loader 中,可能校验后需要重定向,React Router 不建议你用 useNavigation 完成,建议直接在 loader 中直接 return redirect,跳转到新的网址。

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

const loader = async () => {
const user = await getUser();
if (!user) {
return redirect("/login");
}
};

2.2.3 loader 内抛出异常

如果数据获取失败,或者其它任何原因,你认为不能让 Route 对应的 element 正常渲染了,你都可以在 loader 中 throw 异常。这时候,「errorElement」就会被渲染。

function loader({ request, params }) {
const res = await fetch(`/api/properties/${params.id}`);
if (res.status === 404) {
throw new Response("Not Found", { status: 404 });
}
return res.json();
}
//...
<Route loader={loader} />

注意:你可以抛出任何异常,都可以在 errorElement 内通过 hook ​​useRouteError​​ 来获取到异常。

但是,React Router 官方建议你 throw Response:

<Route
path="/properties/:id"
element={<PropertyForSale />}
errorElement={<PropertyError />}
loader={async ({ params }) => {
const res = await fetch(`/api/properties/${params.id}`);
if (res.status === 404) {
throw new Response("Not Found", { status: 404 });
}
const home = res.json();
return { home };
}}
/>

你依然可以用 React Router 提供的 json 方法,方便的构造个 Response:

throw json(
{ message: "email is required" },
{ status: 400 },
);

2.3 element 属性

这个不是新属性,即​​<Route​​被匹配后,渲染的内容。我想介绍它的变化:

2.3.1 内部可用 useLoaderData 获取 loader 返回值

注意,如果 loader 返回值是 Response,并且 Response 的 Content Type 是 application/json,React Router 内部会自动调用 .json() 方法,开发者不必写 .json() 了。

function Albums() {
const albums = useLoaderData();
return <div>{albums}</div>;
}

const router = createBrowserRouter([
{
path: "/",
loader: fetch("/api"),
element: <Albums />,
},
]);

ReactDOM.createRoot(el).render(
<RouterProvider router={router} />
);

2.3.2 内部可调用 useRouteLoaderData 获取 其它 Route 的 loader 返回值

React 组件可以嵌套,​​<Route>​​​也可以嵌套,这时可以通过该 hook 获取其它 ​​<Route>​​ 的 loader 的返回值。当然,你需要提供 id。

定义路由时:

createBrowserRouter([
{
path: "/",
loader: () => fetchUser(),
element: <Root />,
id: "root",
children: [
{
path: "jobs/:jobId",
loader: loadJob,
element: <JobListing />,
},
],
},
]);

​<JobListing />​​内部调用这个hook时:

const user = useRouteLoaderData("root");

2.4 errorElement 属性

当 loader 内抛出异常,​​<Route>​​就不渲染它的 element 了,而是渲染它的 errorElement。

2.4.1 异常可以冒泡

​<Route>​​ 是可以嵌套的,每一层都可以定义 errorElement,异常发生后,会找到最近的 errorElement,并渲染它,然后停止冒泡。

2.4.2 内部可用 useRouteError 获取异常

在 errorElement 内,可用 ​​useRouteError​​ 获取异常。

const error = useRouteError();

2.4.3 内部可用 isRouteErrorResponse 判断异常类型

React Router 给了一个函数 ​​isRouteErrorResponse​​,帮你在开发 errorElement 时,可以判断当前异常是否是 Response 异常。因为 Response 异常 通常是开发者自己抛出的,是可以展示原因的(包括后端接口返回错误码和错误提示文案,也可在这里处理)。其它异常,通常是未知的,就直接展示兜底的报错文案即可。

function RootBoundary() {
const error = useRouteError();

if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return <div>This page doesn't exist!</div>;
}
if (error.status === 503) {
return <div>Looks like our API is down</div>;
}
}

return <div>Something went wrong</div>;
}

2.5 action 属性

它很像 laoder,你看:

  • 它也有2个参数:params 和 request。定义跟 loader 一样。
  • 你可以 return 任何东西,同样 React Router 建议你 return Response。
  • 你也可以 return redirect,实现重定向。
  • 在element内,你可以用hook ​​useActionData​​​ 获取 action 返回值。(类似 ​​useLoaderData​​)

不同点在于,它们执行时机不同:

  • loader 是用户通过 GET 导航至某路由时,执行的。
  • action 是用户提交 form 时,做 POST PUT DELETE 等操作时,执行的。

以前写过​​<form>​​的都知道,它有 action 和 method 参数,在以前,提交表单也是在浏览器内做了一次改变URL的操作。使用React后,几乎没人这么做,大家都是AJAX或Fetch提交表单了。

现在,React Router 提供了 ​​<Form>​​​ 组件,并给 ​​<Route>​​​ 组件增加了 ​​action​​ 属性,让提交表单也变成一次路由。

实在是忍不住了,想发表个人观点:感觉没用,屁用没有

如果你想了解 Route 的 action 属性,一定要看 ​​React Router Form​​,注意 Form 里也有个 action 属性,不要搞混了。

3. React Router 6.4 其它特性

当然,React Router 6.4 不仅有 Data API 这一个特性,它另一个重大更新是:Deferred Data: ​​Deferred Data Guide​​。

再次忍不住发表个人观点:为什么要加这个功能?是为了给 Data API “擦屁股”。

由于引入了 loader,内部有 API 请求,必然导致路由切换时,页面需要时间去加载。加载时间长了怎么办?需要展示 Loading 态。

  • 解决方案一:不要在 loader 内发 API 请求,在 Route 对应的 element 里发请求,并展示 Loading 态。React Router 提供了贴心的 useFetcher,可以在element内发请求。
  • 解决方案二:针对 loader,提供一种配置方案,允许开发者定义 Loading 态。

React Router 这两种方案都提供了。方案一就是 ​​useFetcher​​。为了实现方案二,它引入了​​defer​​​函数和​​<Await>​​组件。

3.1 defer 函数

在 loader 内使用,表明这个 loader 需要展示 Loading 态。如果 loader 返回了 defer,那么就会直接渲染 ​​<Route>​​ 的 element。

<Route
loader={async () => {
let book = await getBook(); // 这个不会展示 Loading 态,因为它被 await 了,会等它执行完并拿到数据
let reviews = getReviews(); // 这个会展示 Loading 态
return defer({
book, // 这是数据
reviews, // 这是 promise
});
}}
element={<Book />}
/>;

3.2 组件

在 ​​<Route>​​​ 的 element 中使用,用于展示 Loading 态。需要结合​​<Suspense>​​​使用,Loading 态展示在​​<Suspense>​​ 的 fallback 中。

function Book() {
const {
book,
reviews, // this is the same promise
} = useLoaderData();
return (
<div>
<h1>{book.title}</h1>
<p>{book.description}</p>
<React.Suspense fallback={<ReviewsSkeleton />}>
<Await
// and is the promise we pass to Await
resolve={reviews}
>
<Reviews />
</Await>
/>
</React.Suspense>
</div>
);
}

等 loader 加载完毕,就会展示 Await 的 children 里的内容了。

3.2.1 组件的 children 属性

可以是函数,也可以是 React 组件。

如果是函数,Promise 结果就是参数:

<Await resolve={reviewsPromise}>
{(resolvedReviews) => <Reviews items={resolvedReviews} />}
</Await>

如果是组件,内部通过​​useAsyncValue​​ 获取 Promise 的结果。

<Await resolve={reviewsPromise}>
<Reviews />
</Await>;

function Reviews() {
const resolvedReviews = useAsyncValue();
return <div>{/* ... */}</div>;
}

个人观点

重申我的核心观点:React Router 6.4 不再是纯粹的路由组件了,它耦合了数据获取逻辑。

  • 如果一个庞大项目,一些数据获取逻辑在 Router 里,一些数据获取逻辑在内部组件。这不利于项目维护。
  • React Router 6.4 为了加个 Data API,增加了很多代码。v6.4 打包UMD production.min.js 体积(16.1KB) 是 v6.3 打包UMD production.min.js(6.75KB) 体积的 2.4 倍!

打包测试

公共依赖:

"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",

不引用 React Router

下面代码打包后,141199 B。

import React from 'react';
import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
<React.StrictMode>
<div />
</React.StrictMode>,
);

React Router v6.3

下面代码打包后,150266 B。

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route index element={<div />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
);

React Router v6.4 不用 Data API

代码跟上面一致,159758 B。

React Router v6.4 使用 Data API

下面代码打包后,196040 B。

import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom';
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
const router = createBrowserRouter([
{
index: true,
element: <div />,
},
]);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);

对比

打包物

体积(B)

增长的体积(B)

相对6.3增长倍数

React Router占总代码比例

无 React Router

141199

0

-

-

React Router 6.3

150266

9067

1倍

6%

React Router 6.4 不用 Data API

159758

18559

2.05倍

12%

React Router 6.4 使用 Data API

196040

54841

6.05倍

28%

结论

最终,我愿意使用 ​​react-router-dom=~6.3.0​​,即不更新到 6.4,永远使用 6.3.x。

毕竟,我的​​《联机桌游合集》​​里,没有http请求。我只想用一个纯粹的路由组件。而且6.4针对6.3的其它小feature,我也完全用不到。

  • React Router 最新版 文档链接: ​​reactrouter.com/en/main​​
  • React Router 6.3.0 文档链接: ​​reactrouter.com/en/v6.3.0​​

写在最后

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者​​HullQin​​授权。我独立开发了​​《联机桌游合集》​​,是个网页,可以很方便的跟朋友联机玩斗地主、​​五子棋​​、​​象棋​​等游戏,不收费无广告。还独立开发了​​《合成大西瓜重制版》​​。还开发了​​《Dice Crush》​​参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:​​《教你做小游戏》​​、​​《极致用户体验》​​。​

标签:const,loader,React,API,Router,element,Data
From: https://blog.51cto.com/hullqin/5912203

相关文章