1 引言
React Router v6 alpha 版本釋出了,本週通過 A Sneak Peek at React Router v6 這篇文章分析一下帶來的改變。
2 概述
更名為
一個不痛不癢的改動,使 API 命名更加規範。
// v5
import { BrowserRouter, Switch, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/profile">
<Profile />
</Route>
</Switch>
</BrowserRouter>
);
}
複製程式碼
在 React Router v6 版本里,直接使用 Routes
替代 Switch
:
// v6
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="profile/*" element={<Profile />} />
</Routes>
</BrowserRouter>
);
}
複製程式碼
升級
在 v5 版本立,想要給元件傳引數是不太直觀的,需要利用 RenderProps 的方式透傳 routeProps
:
import Profile from './Profile';
// v5
<Route path=":userId" component={Profile} />
<Route
path=":userId"
render={routeProps => (
<Profile {...routeProps} animate={true} />
)}
/>
// v6
<Route path=":userId" element={<Profile />} />
<Route path=":userId" element={<Profile animate={true} />} />
複製程式碼
而在 v6 版本中,render
與 component
方案合併成了 element
方案,可以輕鬆傳遞 props 且不需要透傳 roteProps
引數。
更方便的巢狀路由
在 v5 版本中,巢狀路由需要通過 useRouteMatch
拿到 match
,並通過 match.path
的拼接實現子路由:
// v5
import {
BrowserRouter,
Switch,
Route,
Link,
useRouteMatch
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/profile" component={Profile} />
</Switch>
</BrowserRouter>
);
}
function Profile() {
let match = useRouteMatch();
return (
<div>
<nav>
<Link to={`${match.url}/me`}>My Profile</Link>
</nav>
<Switch>
<Route path={`${match.path}/me`}>
<MyProfile />
</Route>
<Route path={`${match.path}/:id`}>
<OthersProfile />
</Route>
</Switch>
</div>
);
}
複製程式碼
在 v6 版本中省去了 useRouteMatch
這一步,支援直接用 path
表示相對路徑:
// v6
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router-dom";
// Approach #1
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="profile/*" element={<Profile />} />
</Routes>
</BrowserRouter>
);
}
function Profile() {
return (
<div>
<nav>
<Link to="me">My Profile</Link>
</nav>
<Routes>
<Route path="me" element={<MyProfile />} />
<Route path=":id" element={<OthersProfile />} />
</Routes>
</div>
);
}
// Approach #2
// You can also define all
// <Route> in a single place
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="profile" element={<Profile />}>
<Route path=":id" element={<MyProfile />} />
<Route path="me" element={<OthersProfile />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Profile() {
return (
<div>
<nav>
<Link to="me">My Profile</Link>
</nav>
<Outlet />
</div>
);
}
複製程式碼
注意 Outlet
是渲染子路由的 Element。
useNavigate 替代 useHistory
在 v5 版本中,主動跳轉路由可以通過 useHistory
進行 history.push
等操作:
// v5
import { useHistory } from "react-router-dom";
function MyButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return <button onClick={handleClick}>Submit</button>;
}
複製程式碼
而在 v6 版本中,可以通過 useNavigate
直接實現這個常用操作:
// v6
import { useNavigate } from "react-router-dom";
function MyButton() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return <button onClick={handleClick}>Submit</button>;
}
複製程式碼
react-router 內部對 history 進行了封裝,如果需要 history.replace
,可以通過 { replace: true }
引數指定:
// v5
history.push("/home");
history.replace("/home");
// v6
navigate("/home");
navigate("/home", { replace: true });
複製程式碼
更小的體積 8kb
由於程式碼幾乎重構,v6 版本的程式碼壓縮後體積從 20kb 縮小到 8kb。
3 精讀
react-router v6 原始碼中有一段比較核心的理念,筆者拿出來與大家分享,對一些框架開發是大有裨益的。我們看 useRoutes
這段程式碼節選:
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;
}
複製程式碼
可以看到,利用 React.Context
,v6 版本在每個路由元素渲染時都包裹了一層 RouteContext
。
拿更方便的路由巢狀來說:
在 v6 版本中省去了
useRouteMatch
這一步,支援直接用path
表示相對路徑。
這就是利用這個方案做到的,因為給每一層路由檔案包裹了 Context,所以在每一層都可以拿到上一層的 path
,因此在拼接路由時可以完全由框架內部實現,而不需要使用者在呼叫時預先拼接好。
再以 useNavigate
舉例,有人覺得 navigate
這個封裝僅停留在形式層,但其實在功能上也有封裝,比如如果傳入但是一個相對路徑,會根據當前路由進行切換,下面是 useNavigate
程式碼節選:
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;
}
複製程式碼
可以看到,利用 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 上:
const Input = () => {
// Input 元件在畫布中會自動生成一個 id,但這個 id 元件無法通過 props 拿到
};
複製程式碼
此時如果我們允許 Input 元件內部再建立一個子元素,又希望這個子元素的 id 是由 Input 推匯出來的,我們可能需要使用者這麼做:
const Input = ({ id }) => {
return <ComponentLoader id={id + "1"} />;
};
複製程式碼
這樣做有兩個問題:
- 將 id 暴露給 Input 元件,違背了之前設計的簡潔性。
- 元件需要對 id 進行拼裝,很麻煩。
這裡遇到的問題和 React Router 遇到的一樣,我們可以將程式碼簡化成下面這樣,但功能不變嗎?
const Input = () => {
return <ComponentLoader id="1" />;
};
複製程式碼
答案是可以做到,我們可以利用 Context 實現這種方案。關鍵點就在於,渲染 Input 但元件容器需要包裹一個 Provider:
const ComponentLoader = ({ id, element }) => {
<Context.Provider value={{ id }}>{element}</Context.Provider>;
};
複製程式碼
那麼對於內部的元件來說,在不同層級下呼叫 useContext
拿到的 id 是不同的,這正是我們想要的效果:
const ComponentLoader = ({id,element}) => {
const { id: parentId } = useContext(Context)
<Context.Provider value={{ id: parentId + id }}>
{element}
</Context.Provider>
}
複製程式碼
這樣我們在 Input
內部呼叫的 <ComponentLoader id="1" />
實際上拼接的實際 id 是 01
,而這完全拋到了外部引擎層處理,使用者無需手動拼接。
4 總結
React Router v6 完全利用 Hooks 重構後,不僅程式碼量精簡了很多,還變得更好用了,等發正式版的時候可以快速升級一波。
另外從 React Router v6 做的這些優化中,我們從原始碼中挖掘到了關於 Context 更巧妙的用法,希望這個方法可以幫助你運用到其他更復雜的專案設計中。
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)