手把手教你快速升級將React Router v5 路由器更新到v6


手把手教你快速升級將React Router v5 路由器更新到v6


1 引言

React Router v6 alpha 版本發佈了,本週通過 A Sneak Peek at React Router v6 這篇文章分析一下帶來的改變。

React Router v6:https://github.com/ReactTraining/react-router


A Sneak Peek at React Router v6 :https://alligator.io/react/react-router-v6/

2 概述

更名為

一個不痛不癢的改動,使 API 命名更加規範。

<code>// v5
import { BrowserRouter, Switch, Route } from "react-router-dom";

function App() {
return (
<browserrouter>
<switch>
<route>
<home>
/<route>
<route>
<profile>
/<route>
/<switch>
/<browserrouter>
);
}/<code>

在 React Router v6 版本里,直接使用 Routes 替代 Switch:

<code>// v6
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
return (
<browserrouter>
<routes>
<route>} />
<route>} />
/<routes>
/<browserrouter>
);
}/<code>

升級

在 v5 版本立,想要給組件傳參數是不太直觀的,需要利用 RenderProps 的方式透傳 routeProps:

<code>import Profile from './Profile';

// v5
<route>
<route> path=":userId"
render={routeProps => (
<profile>
)}
/>

// v6
<route>} />
<route>} />/<route>/<code>

而在 v6 版本中,render 與 component 方案合併成了 element 方案,可以輕鬆傳遞 props 且不需要透傳 roteProps 參數。

更方便的嵌套路由

在 v5 版本中,嵌套路由需要通過 useRouteMatch 拿到 match,並通過 match.path 的拼接實現子路由:

<code>// v5
import {
BrowserRouter,
Switch,
Route,
Link,

useRouteMatch
} from "react-router-dom";

function App() {
return (
<browserrouter>
<switch>
<route>
<route>
/<switch>
/<browserrouter>
);
}

function Profile() {
let match = useRouteMatch();

return (



<switch>
<route>
<myprofile>
/<route>
<route>
<othersprofile>
/<route>
/<switch>

);
}/<code>

在 v6 版本中省去了 useRouteMatch 這一步,支持直接用 path 表示相對路徑:

<code>// v6
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router-dom";

// Approach #1
function App() {
return (
<browserrouter>

<routes>
<route>} />
<route>} />
/<routes>
/<browserrouter>
);
}

function Profile() {
return (



<routes>
<route>} />
<route>} />
/<routes>

);
}

// Approach #2
// You can also define all
// <route> in a single place
function App() {
return (
<browserrouter>
<routes>
<route>} />
<route>}>
<route>} />
<route>} />
/<routes>/<browserrouter>/<route>


);
}

function Profile() {
return (



<outlet>

);
}/<code>

注意 Outlet 是渲染子路由的 Element。

useNavigate 替代 useHistory

在 v5 版本中,主動跳轉路由可以通過 useHistory 進行 history.push 等操作:

<code>// v5
import { useHistory } from "react-router-dom";

function MyButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return <button>Submit/<button>;
}/<code>

而在 v6 版本中,可以通過 useNavigate 直接實現這個常用操作:

<code>// v6
import { useNavigate } from "react-router-dom";

function MyButton() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return <button>Submit/<button>;
}/<code>

react-router 內部對 history 進行了封裝,如果需要 history.replace,可以通過 { replace: true } 參數指定:

<code>// v5 

history.push("/home");
history.replace("/home");

// v6
navigate("/home");
navigate("/home", { replace: true });/<code>

更小的體積 8kb

由於代碼幾乎重構,v6 版本的代碼壓縮後體積從 20kb 縮小到 8kb。

3 精讀

react-router v6 源碼中有一段比較核心的理念,筆者拿出來與大家分享,對一些框架開發是大有裨益的。我們看 useRoutes 這段代碼節選:

<code>export function useRoutes(routes, basename = "", caseSensitive = false) {
let {
params: parentParams,
pathname: parentPathname,
route: parentRoute
} = React.useContext(RouteContext);

if (warnAboutMissingTrailingSplatAt) {
// ...
}

basename = basename ? joinPaths([parentPathname, basename]) : parentPathname;

let navigate = useNavigate();
let location = useLocation();
let matches = React.useMemo(
() => matchRoutes(routes, location, basename, caseSensitive),
[routes, location, basename, caseSensitive]
);

// ...

// Otherwise render an element.
let element = matches.reduceRight((outlet, { params, pathname, route }) => {
return (
<routecontext.provider> children={route.element}
value={{

outlet,
params: readOnly({ ...parentParams, ...params }),
pathname: joinPaths([basename, pathname]),
route
}}
/>
);
}, null);

return element;
}/<routecontext.provider>/<code>

可以看到,利用 React.Context,v6 版本在每個路由元素渲染時都包裹了一層 RouteContext。

拿更方便的路由嵌套來說:

在 v6 版本中省去了 useRouteMatch 這一步,支持直接用 path 表示相對路徑。

這就是利用這個方案做到的,因為給每一層路由文件包裹了 Context,所以在每一層都可以拿到上一層的 path,因此在拼接路由時可以完全由框架內部實現,而不需要用戶在調用時預先拼接好。

再以 useNavigate 舉例,有人覺得 navigate 這個封裝僅停留在形式層,但其實在功能上也有封裝,比如如果傳入但是一個相對路徑,會根據當前路由進行切換,下面是 useNavigate 代碼節選:

<code>export function useNavigate() {
let { history, pending } = React.useContext(LocationContext);
let { pathname } = React.useContext(RouteContext);

let navigate = React.useCallback(
(to, { replace, state } = {}) => {
if (typeof to === "number") {
history.go(to);

} else {
let relativeTo = resolveLocation(to, pathname);

let method = !!replace || pending ? "replace" : "push";
history[method](relativeTo, state);
}
},
[history, pending, pathname]
);

return navigate;
}/<code>

可以看到,利用 RouteContext 拿到當前的 pathname,並根據 resolveLocation 對 to 與 pathname 進行路徑拼接,而 pathname 就是通過 RouteContext.Provider 提供的。

巧用多層 Context Provider

很多時候我們利用 Context 停留在一個 Provider,多個 useContext 的層面上,這是 Context 最基礎的用法,但相信讀完 React Router v6 這篇文章,我們可以挖掘出 Context 更多的用法:多層 Context Provider。

雖然說 Context Provider 存在多層會採取最近覆蓋的原則,但這不僅僅是一條規避錯誤的功能,我們可以利用這個功能實現 React Router v6 這樣的改良。

為了更仔細說明這個特性,這裡再舉一個具體的例子:比如實現搭建渲染引擎時,每個組件都有一個 id,但這個 id 並不透出在組件的 props 上:

<code>const Input = () => {
// Input 組件在畫布中會自動生成一個 id,但這個 id 組件無法通過 props 拿到
};/<code>

此時如果我們允許 Input 組件內部再創建一個子元素,又希望這個子元素的 id 是由 Input 推導出來的,我們可能需要用戶這麼做:

<code>const Input = ({ id }) => {
return <componentloader>;
};/<code>

這樣做有兩個問題:

  1. 將 id 暴露給 Input 組件,違背了之前設計的簡潔性。
  2. 組件需要對 id 進行拼裝,很麻煩。

這裡遇到的問題和 React Router 遇到的一樣,我們可以將代碼簡化成下面這樣,但功能不變嗎?

<code>const Input = () => {
return <componentloader>;
};/<code>

答案是可以做到,我們可以利用 Context 實現這種方案。關鍵點就在於,渲染 Input 但組件容器需要包裹一個 Provider:

<code>const ComponentLoader = ({ id, element }) => {
<context.provider>{element}/<context.provider>;
};/<code>

那麼對於內部的組件來說,在不同層級下調用 useContext 拿到的 id 是不同的,這正是我們想要的效果:

<code>const ComponentLoader = ({id,element}) => { 

const { id: parentId } = useContext(Context)

<context.provider>
{element}
/<context.provider>
}/<code>

這樣我們在 Input 內部調用的 <componentloader> 實際上拼接的實際 id 是 01,而這完全拋到了外部引擎層處理,用戶無需手動拼接。

4 總結

React Router v6 完全利用 Hooks 重構後,不僅代碼量精簡了很多,還變得更好用了,等發正式版的時候可以快速升級一波。

另外從 React Router v6 做的這些優化中,我們從源碼中挖掘到了關於 Context 更巧妙的用法,希望這個方法可以幫助你運用到其他更復雜的項目設計中。

轉載鏈接:https://zhuanlan.zhihu.com/p/120712831


分享到:


相關文章: