React Router v6 alpha 版本發佈了,本週經過 A Sneak Peek at React Router v6 這篇文章分析一下帶來的改變。前端
一個不痛不癢的改動,使 API 命名更加規範。react
// 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
:git
// 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
:github
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
的拼接實現子路由:react-router
// 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。dom
在 v5 版本中,主動跳轉路由能夠經過 useHistory
進行 history.push
等操做:ide
// 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 });
複製代碼
因爲代碼幾乎重構,v6 版本的代碼壓縮後體積從 20kb 縮小到 8kb。
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
,多個 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"} />; }; 複製代碼
這樣作有兩個問題:
這裏遇到的問題和 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
,而這徹底拋到了外部引擎層處理,用戶無需手動拼接。
React Router v6 徹底利用 Hooks 重構後,不只代碼量精簡了不少,還變得更好用了,等發正式版的時候能夠快速升級一波。
另外從 React Router v6 作的這些優化中,咱們從源碼中挖掘到了關於 Context 更巧妙的用法,但願這個方法能夠幫助你運用到其餘更復雜的項目設計中。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)