2023-03-26 14:10:24 來源 : 騰訊云
每次打開 React Router 官方文檔,都會有驚嚇,API又又又變了!這次看看有什么更新。
(資料圖片僅供參考)
好家伙!這是我認知中的 React Router 嗎?
我2022年3月開發《聯機桌游合集》時,在用 6.2 版本,那時候 v6 跟 v5 v4 相比,API 已經發生了比較大的變化,但我認可這些變化。
現在看完 6.4 版本文檔, 我想吐槽。 我的核心觀點是:React Router 6.4 不再是純粹的路由組件了,它耦合了數據獲取邏輯。
下面本文 客觀介紹: React Router 6.4 引入的新功能 Data API,并在最后給 主觀結論。
createXXXXRouter
API在 React Router 6.4 中,新增了 3 個 createXXXXRouter
API,用于支持 data API:
createBrowserRouter
createMemoryRouter
createHashRouter
也就是說,如果你不用這3個API,而是像v6.0
-v6.3
一樣,直接使用
等下面幾個API,那么你享受不到 data API。
createXXXXRouter
用法必須結合
一起使用。可以看到,它使用一個配置,定義路由。
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( );
當然,如果你喜歡用JSX語法定義路由,像
一樣:
} />
React Router 6.4 也提供了JSX配置,參考createRoutesFromElements,它有另外一個名字叫createRoutesFromChildren。
const router = createBrowserRouter( createRoutesFromElements( }> } /> } /> ));
的變化當你使用createXXXXRouter
和
時,你就可以使用 Data API。
說了這么多,什么是 Data API 呢?
其實就是允許你把「數據獲取邏輯」寫到路由定義中。每當路由切換到那里時,會自動獲取數據。
我們從
的變化就可以看出,它新增了3個相關的屬性:
loader 屬性loader屬性傳入一個函數(允許是 async function),每次渲染「該路由對應的element」前執行函數。在「該路由對應的element」內,可以使用 hookuseLoaderData
(下文會介紹)來獲取這個函數的返回值(通常是http請求的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屬性傳入的函數,允許有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
方便的獲取當前頁面的參數: { 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 實現,如下:
{ return fetch("/api/teams.json", { signal: request.signal, }); }}/>
函數的返回值,將可以在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", }, });}//...
也可以通過 React Router 提供的 json 來構造:
import { json } from "react-router-dom";function loader({ request, params }) { const data = { some: "thing" }; return json(data, { status: 200 });}//...
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"); }};
如果數據獲取失敗,或者其它任何原因,你認為不能讓 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();}//...
注意:你可以拋出任何異常,都可以在 errorElement 內通過 hook useRouteError
來獲取到異常。
但是,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 方法,方便的構造個 Response:
throw json( { message: "email is required" }, { status: 400 },);
element 屬性這個不是新屬性,即
useLoaderData
獲取 loader 返回值注意,如果 loader 返回值是 Response,并且 Response 的 Content Type 是 application/json,React Router 內部會自動調用 .json() 方法,開發者不必寫 .json() 了。
function Albums() { const albums = useLoaderData(); return {albums};}const router = createBrowserRouter([ { path: "/", loader: fetch("/api"), element: , },]);ReactDOM.createRoot(el).render( );
useRouteLoaderData
獲取 其它 Route 的 loader 返回值React 組件可以嵌套,
也可以嵌套,這時可以通過該 hook 獲取其它
的 loader 的返回值。當然,你需要提供 id。
定義路由時:
createBrowserRouter([ { path: "/", loader: () => fetchUser(), element: , id: "root", children: [ { path: "jobs/:jobId", loader: loadJob, element: , }, ], },]);
內部調用這個hook時:
const user = useRouteLoaderData("root");
errorElement 屬性當 loader 內拋出異常,
就不渲染它的 element 了,而是渲染它的 errorElement。
是可以嵌套的,每一層都可以定義 errorElement,異常發生后,會找到最近的 errorElement,并渲染它,然后停止冒泡。
useRouteError
獲取異常在 errorElement 內,可用 useRouteError
獲取異常。
const error = useRouteError();
isRouteErrorResponse
判斷異常類型React Router 給了一個函數 isRouteErrorResponse
,幫你在開發 errorElement 時,可以判斷當前異常是否是 Response 異常。因為 Response 異常 通常是開發者自己拋出的,是可以展示原因的(包括后端接口返回錯誤碼和錯誤提示文案,也可在這里處理)。其它異常,通常是未知的,就直接展示兜底的報錯文案即可。
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個參數:params 和 request。定義跟 loader 一樣。你可以 return 任何東西,同樣 React Router 建議你 return Response。你也可以 return redirect,實現重定向。在element內,你可以用hookuseActionData
獲取 action 返回值。(類似 useLoaderData
)不同點在于,它們執行時機不同:
loader 是用戶通過 GET 導航至某路由時,執行的。action 是用戶提交 form 時,做 POST PUT DELETE 等操作時,執行的。以前寫過的都知道,它有 action 和 method 參數,在以前,提交表單也是在瀏覽器內做了一次改變URL的操作。使用React后,幾乎沒人這么做,大家都是AJAX或Fetch提交表單了。
現在,React Router 提供了 組件,并給
組件增加了 action
屬性,讓提交表單也變成一次路由。
實在是忍不住了,想發表個人觀點:感覺沒用,屁用沒有。
如果你想了解 Route 的 action 屬性,一定要看 React Router Form,注意 Form 里也有個 action 屬性,不要搞混了。
當然,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
函數和
組件。
在 loader 內使用,表明這個 loader 需要展示 Loading 態。如果 loader 返回了 defer,那么就會直接渲染
的 element。
{ let book = await getBook(); // 這個不會展示 Loading 態,因為它被 await 了,會等它執行完并拿到數據 let reviews = getReviews(); // 這個會展示 Loading 態 return defer({ book, // 這是數據 reviews, // 這是 promise }); }} element={ }/>;
組件在
的 element 中使用,用于展示 Loading 態。需要結合
使用,Loading 態展示在
的 fallback 中。
function Book() { const { book, reviews, // this is the same promise } = useLoaderData(); return ( {book.title}
{book.description}
}> /> );}
等 loader 加載完畢,就會展示 Await 的 children 里的內容了。
組件的 children 屬性可以是函數,也可以是 React 組件。
如果是函數,Promise 結果就是參數:
{(resolvedReviews) => }
如果是組件,內部通過useAsyncValue
獲取 Promise 的結果。
;function Reviews() { const resolvedReviews = useAsyncValue(); return {/* ... */};}
重申我的核心觀點: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",
下面代碼打包后,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) | 增長的體積(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 最新版 文檔鏈接: https://reactrouter.com/en/mainReact Router 6.3.0 文檔鏈接: https://reactrouter.com/en/v6.3.0我是HullQin,公眾號線下聚會游戲的作者(歡迎關注我,交個朋友)。轉發本文前需獲得作者HullQin授權。我獨立開發了《聯機桌游合集》,是個網頁,可以很方便的跟朋友聯機玩UNO、斗地主、五子棋、飛行棋、一夜狼、象棋、德國心臟病、達芬奇密碼等游戲,不收費無廣告。還開發了《Dice Crush》參加Game Jam 2022。喜歡可以關注我噢~我有空了會分享做游戲的相關技術,會在這個專欄里分享:《教你做小游戲》。
標簽: