2023-03-26 14:10:24 來(lái)源 : 騰訊云
每次打開(kāi) React Router 官方文檔,都會(huì)有驚嚇,API又又又變了!這次看看有什么更新。
(資料圖片僅供參考)
好家伙!這是我認(rèn)知中的 React Router 嗎?
我2022年3月開(kāi)發(fā)《聯(lián)機(jī)桌游合集》時(shí),在用 6.2 版本,那時(shí)候 v6 跟 v5 v4 相比,API 已經(jīng)發(fā)生了比較大的變化,但我認(rèn)可這些變化。
現(xiàn)在看完 6.4 版本文檔, 我想吐槽。 我的核心觀點(diǎn)是:React Router 6.4 不再是純粹的路由組件了,它耦合了數(shù)據(jù)獲取邏輯。
下面本文 客觀介紹: React Router 6.4 引入的新功能 Data API,并在最后給 主觀結(jié)論。
createXXXXRouter
API在 React Router 6.4 中,新增了 3 個(gè) createXXXXRouter
API,用于支持 data API:
createBrowserRouter
createMemoryRouter
createHashRouter
也就是說(shuō),如果你不用這3個(gè)API,而是像v6.0
-v6.3
一樣,直接使用
等下面幾個(gè)API,那么你享受不到 data API。
createXXXXRouter
用法必須結(jié)合
一起使用。可以看到,它使用一個(gè)配置,定義路由。
import * as React from "react";import * as ReactDOM from "react-dom";import { createBrowserRouter, RouterProvider,} from "react-router-dom";const router = createBrowserRouter([ { path: "/", element: , children: [ { path: "team", element: , }, ], },]);ReactDOM.createRoot(document.getElementById("root")).render( );
當(dāng)然,如果你喜歡用JSX語(yǔ)法定義路由,像
一樣:
} />
React Router 6.4 也提供了JSX配置,參考createRoutesFromElements,它有另外一個(gè)名字叫createRoutesFromChildren。
const router = createBrowserRouter( createRoutesFromElements( }> } /> } /> ));
的變化當(dāng)你使用createXXXXRouter
和
時(shí),你就可以使用 Data API。
說(shuō)了這么多,什么是 Data API 呢?
其實(shí)就是允許你把「數(shù)據(jù)獲取邏輯」寫(xiě)到路由定義中。每當(dāng)路由切換到那里時(shí),會(huì)自動(dòng)獲取數(shù)據(jù)。
我們從
的變化就可以看出,它新增了3個(gè)相關(guān)的屬性:
loader 屬性loader屬性傳入一個(gè)函數(shù)(允許是 async function),每次渲染「該路由對(duì)應(yīng)的element」前執(zhí)行函數(shù)。在「該路由對(duì)應(yīng)的element」內(nèi),可以使用 hookuseLoaderData
(下文會(huì)介紹)來(lái)獲取這個(gè)函數(shù)的返回值(通常是http請(qǐng)求的response)。
{ // loaders can be async functions const res = await fetch("/api/user.json", { signal: request.signal, }); const user = await res.json(); return user; }} element={ }/>
loader屬性傳入的函數(shù),允許有2個(gè)參數(shù):
params: 如果Route中包含參數(shù)(例如path是/user/:userId
,參數(shù)就是:userId
,可以通過(guò)params.userId獲取到路由參數(shù)的值)。request: 是 Web 規(guī)范中,F(xiàn)etch API 的 Request,代表一個(gè)請(qǐng)求。注意:這里指的不是你在 loader 內(nèi)部發(fā)的 fetch 請(qǐng)求,而是當(dāng)用戶路由到當(dāng)前路徑時(shí),發(fā)出的“請(qǐng)求”(其實(shí)在Single-Page App中,router已經(jīng)攔截了這個(gè)真實(shí)的請(qǐng)求,只有Multi-Page App中才會(huì)有這個(gè)請(qǐng)求),這里是 React Router 6.4 為了方便開(kāi)發(fā)者獲取當(dāng)前路徑信息提供的參數(shù),他們按照 Web規(guī)范,制造了一個(gè)假的 request。你可以通過(guò) request
方便的獲取當(dāng)前頁(yè)面的參數(shù): { const url = new URL(request.url); const searchTerm = url.searchParams.get("q"); return searchProducts(searchTerm); }}/>
不要這個(gè) request 參數(shù)行嗎?不行,因?yàn)槿绻阌?code>window.location獲取的信息是當(dāng)前最新的值,如果用戶快速的點(diǎn)擊按鈕,讓頁(yè)面路由到A,并立馬路由到B,這時(shí)候路由A對(duì)應(yīng)的Route的loader獲取window.location
時(shí),就可能拿到錯(cuò)誤的值。
注意,傳遞 request,還有個(gè)好處,它有個(gè) request.signal,當(dāng)用戶快速的點(diǎn)擊按鈕,讓頁(yè)面路由到A,并立馬路由到B,頁(yè)面A的loader的請(qǐng)求應(yīng)該被取消掉,可以通過(guò) signal 實(shí)現(xiàn),如下:
{ return fetch("/api/teams.json", { signal: request.signal, }); }}/>
函數(shù)的返回值,將可以在element中通過(guò)hook useLoaderData
(下文會(huì)介紹)來(lái)獲取。你返回什么,它就拿到什么。
但是 React Router 官方建議,返回一個(gè) Web規(guī)范 中的 Fetch API 的 Response。
你可以直接 return fetch(url, config);
,也可以自己構(gòu)造一個(gè)假的 Response:
function loader({ request, params }) { const data = { some: "thing" }; return new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json; utf-8", }, });}//...
也可以通過(guò) React Router 提供的 json 來(lái)構(gòu)造:
import { json } from "react-router-dom";function loader({ request, params }) { const data = { some: "thing" }; return json(data, { status: 200 });}//...
redirect
在 loader 中,可能校驗(yàn)后需要重定向,React Router 不建議你用 useNavigation 完成,建議直接在 loader 中直接 return redirect,跳轉(zhuǎn)到新的網(wǎng)址。
import { redirect } from "react-router-dom";const loader = async () => { const user = await getUser(); if (!user) { return redirect("/login"); }};
如果數(shù)據(jù)獲取失敗,或者其它任何原因,你認(rèn)為不能讓 Route 對(duì)應(yīng)的 element 正常渲染了,你都可以在 loader 中 throw 異常。這時(shí)候,「errorElement」就會(huì)被渲染。
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();}//...
注意:你可以拋出任何異常,都可以在 errorElement 內(nèi)通過(guò) hook useRouteError
來(lái)獲取到異常。
但是,React Router 官方建議你 throw Response:
} errorElement={ } 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 方法,方便的構(gòu)造個(gè) Response:
throw json( { message: "email is required" }, { status: 400 },);
element 屬性這個(gè)不是新屬性,即
useLoaderData
獲取 loader 返回值注意,如果 loader 返回值是 Response,并且 Response 的 Content Type 是 application/json,React Router 內(nèi)部會(huì)自動(dòng)調(diào)用 .json() 方法,開(kāi)發(fā)者不必寫(xiě) .json() 了。
function Albums() { const albums = useLoaderData(); return {albums};}const router = createBrowserRouter([ { path: "/", loader: fetch("/api"), element: , },]);ReactDOM.createRoot(el).render( );
useRouteLoaderData
獲取 其它 Route 的 loader 返回值React 組件可以嵌套,
也可以嵌套,這時(shí)可以通過(guò)該 hook 獲取其它
的 loader 的返回值。當(dāng)然,你需要提供 id。
定義路由時(shí):
createBrowserRouter([ { path: "/", loader: () => fetchUser(), element: , id: "root", children: [ { path: "jobs/:jobId", loader: loadJob, element: , }, ], },]);
內(nèi)部調(diào)用這個(gè)hook時(shí):
const user = useRouteLoaderData("root");
errorElement 屬性當(dāng) loader 內(nèi)拋出異常,
就不渲染它的 element 了,而是渲染它的 errorElement。
是可以嵌套的,每一層都可以定義 errorElement,異常發(fā)生后,會(huì)找到最近的 errorElement,并渲染它,然后停止冒泡。
useRouteError
獲取異常在 errorElement 內(nèi),可用 useRouteError
獲取異常。
const error = useRouteError();
isRouteErrorResponse
判斷異常類型React Router 給了一個(gè)函數(shù) isRouteErrorResponse
,幫你在開(kāi)發(fā) errorElement 時(shí),可以判斷當(dāng)前異常是否是 Response 異常。因?yàn)?Response 異常 通常是開(kāi)發(fā)者自己拋出的,是可以展示原因的(包括后端接口返回錯(cuò)誤碼和錯(cuò)誤提示文案,也可在這里處理)。其它異常,通常是未知的,就直接展示兜底的報(bào)錯(cuò)文案即可。
function RootBoundary() { const error = useRouteError(); if (isRouteErrorResponse(error)) { if (error.status === 404) { return This page doesn"t exist!; } if (error.status === 503) { return Looks like our API is down; } } return Something went wrong;}
action 屬性它很像 laoder,你看:
它也有2個(gè)參數(shù):params 和 request。定義跟 loader 一樣。你可以 return 任何東西,同樣 React Router 建議你 return Response。你也可以 return redirect,實(shí)現(xiàn)重定向。在element內(nèi),你可以用hookuseActionData
獲取 action 返回值。(類似 useLoaderData
)不同點(diǎn)在于,它們執(zhí)行時(shí)機(jī)不同:
loader 是用戶通過(guò) GET 導(dǎo)航至某路由時(shí),執(zhí)行的。action 是用戶提交 form 時(shí),做 POST PUT DELETE 等操作時(shí),執(zhí)行的。以前寫(xiě)過(guò)的都知道,它有 action 和 method 參數(shù),在以前,提交表單也是在瀏覽器內(nèi)做了一次改變URL的操作。使用React后,幾乎沒(méi)人這么做,大家都是AJAX或Fetch提交表單了。
現(xiàn)在,React Router 提供了 組件,并給
組件增加了 action
屬性,讓提交表單也變成一次路由。
實(shí)在是忍不住了,想發(fā)表個(gè)人觀點(diǎn):感覺(jué)沒(méi)用,屁用沒(méi)有。
如果你想了解 Route 的 action 屬性,一定要看 React Router Form,注意 Form 里也有個(gè) action 屬性,不要搞混了。
當(dāng)然,React Router 6.4 不僅有 Data API 這一個(gè)特性,它另一個(gè)重大更新是:Deferred Data: Deferred Data Guide。
再次忍不住發(fā)表個(gè)人觀點(diǎn):為什么要加這個(gè)功能?是為了給 Data API “擦屁股”。
由于引入了 loader,內(nèi)部有 API 請(qǐng)求,必然導(dǎo)致路由切換時(shí),頁(yè)面需要時(shí)間去加載。加載時(shí)間長(zhǎng)了怎么辦?需要展示 Loading 態(tài)。
解決方案一:不要在 loader 內(nèi)發(fā) API 請(qǐng)求,在 Route 對(duì)應(yīng)的 element 里發(fā)請(qǐng)求,并展示 Loading 態(tài)。React Router 提供了貼心的 useFetcher,可以在element內(nèi)發(fā)請(qǐng)求。解決方案二:針對(duì) loader,提供一種配置方案,允許開(kāi)發(fā)者定義 Loading 態(tài)。React Router 這兩種方案都提供了。方案一就是 useFetcher。為了實(shí)現(xiàn)方案二,它引入了defer
函數(shù)和
組件。
在 loader 內(nèi)使用,表明這個(gè) loader 需要展示 Loading 態(tài)。如果 loader 返回了 defer,那么就會(huì)直接渲染
的 element。
{ let book = await getBook(); // 這個(gè)不會(huì)展示 Loading 態(tài),因?yàn)樗?await 了,會(huì)等它執(zhí)行完并拿到數(shù)據(jù) let reviews = getReviews(); // 這個(gè)會(huì)展示 Loading 態(tài) return defer({ book, // 這是數(shù)據(jù) reviews, // 這是 promise }); }} element={ }/>;
組件在
的 element 中使用,用于展示 Loading 態(tài)。需要結(jié)合
使用,Loading 態(tài)展示在
的 fallback 中。
function Book() { const { book, reviews, // this is the same promise } = useLoaderData(); return ( {book.title}
{book.description}
}> /> );}
等 loader 加載完畢,就會(huì)展示 Await 的 children 里的內(nèi)容了。
組件的 children 屬性可以是函數(shù),也可以是 React 組件。
如果是函數(shù),Promise 結(jié)果就是參數(shù):
{(resolvedReviews) => }
如果是組件,內(nèi)部通過(guò)useAsyncValue
獲取 Promise 的結(jié)果。
;function Reviews() { const resolvedReviews = useAsyncValue(); return {/* ... */};}
重申我的核心觀點(diǎn):React Router 6.4 不再是純粹的路由組件了,它耦合了數(shù)據(jù)獲取邏輯。
如果一個(gè)龐大項(xiàng)目,一些數(shù)據(jù)獲取邏輯在 Router 里,一些數(shù)據(jù)獲取邏輯在內(nèi)部組件。這不利于項(xiàng)目維護(hù)。React Router 6.4 為了加個(gè) 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",
下面代碼打包后,141199 B。
import React from "react";import ReactDOM from "react-dom/client";const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);root.render( ,);
下面代碼打包后,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( } /> ,);
代碼跟上面一致,159758 B。
下面代碼打包后,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: , },]);root.render( ,);
打包物 | 體積(B) | 增長(zhǎng)的體積(B) | 相對(duì)6.3增長(zhǎng)倍數(shù) | React Router占總代碼比例 |
---|---|---|---|---|
無(wú) 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,永遠(yuǎn)使用 6.3.x。
畢竟,我的《聯(lián)機(jī)桌游合集》里,沒(méi)有http請(qǐng)求。我只想用一個(gè)純粹的路由組件。而且6.4針對(duì)6.3的其它小feature,我也完全用不到。
React Router 最新版 文檔鏈接: https://reactrouter.com/en/mainReact Router 6.3.0 文檔鏈接: https://reactrouter.com/en/v6.3.0我是HullQin,公眾號(hào)線下聚會(huì)游戲的作者(歡迎關(guān)注我,交個(gè)朋友)。轉(zhuǎn)發(fā)本文前需獲得作者HullQin授權(quán)。我獨(dú)立開(kāi)發(fā)了《聯(lián)機(jī)桌游合集》,是個(gè)網(wǎng)頁(yè),可以很方便的跟朋友聯(lián)機(jī)玩UNO、斗地主、五子棋、飛行棋、一夜狼、象棋、德國(guó)心臟病、達(dá)芬奇密碼等游戲,不收費(fèi)無(wú)廣告。還開(kāi)發(fā)了《Dice Crush》參加Game Jam 2022。喜歡可以關(guān)注我噢~我有空了會(huì)分享做游戲的相關(guān)技術(shù),會(huì)在這個(gè)專欄里分享:《教你做小游戲》。
標(biāo)簽: