>我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:景明 ## 升級背景 目前公司產品有關 react 的工具版本普遍較低,其中 react router ...
我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。
本文作者:景明
升級背景
目前公司產品有關 react 的工具版本普遍較低,其中 react router 版本為 3.x(是的,沒有看錯,3.x 的版本)。而最新的 react router 已經到了 6.x 版本。
為了能夠跟上路由的腳步,也為了使用 router 相關的 hooks 函數,一次必不可少的升級由此到來!
版本確定
react-touter 6.x 版本,只對 react 和 react-dom 版本有要求,我們的項目滿足條件。
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
確定使用 react-router-dom: 6.11.1 為目標升級版本。是的,跳過了v4/v5 版本,直接上 v6 一步到位
React Router 使用場景以及變化介紹
組件引用
在 v6 版本,分為了 3 個包(PS:相容包不算)
- react-router : 核心包,只提供核心的路由和 hook 函數,不會直接使用
- react-router-dom :供瀏覽器/Web 應用使用的 API。依賴於 react-router, 同時將 react-router 的 API 重新暴露出來
- react-router-native :供 React Native 應用使用的 API。同時將 react-router 的 API 重新暴露出來(無 native 相關項目,與我們無關不管)
從 V6 開始,只需要使用 react-router-dom 即可,不會直接使用 react-router。
對應的是組件引用的變更,如下:
// v3 版本
import { Link } from 'react-router'
// v6 版本後
import { Link } from 'react-router-dom';
路由
interface RouteObject {
path?: string;
index?: boolean; // 索引路由
children?: React.ReactNode; // 子路由
caseSensitive?: boolean; // 區分大小寫
id?: string;
loader?: LoaderFunction; // 路由元素渲染前執行
action?: ActionFunction;
element?: React.ReactNode | null;
Component?: React.ComponentType | null;
errorElement?: React.ReactNode | null; // 在 loader / action 過程中拋出異常展示
ErrorBoundary?: React.ComponentType | null;
handle?: RouteObject["handle"];
lazy?: LazyRouteFunction<RouteObject>;
}
path
v6 中使用簡化的路徑格式。<Route path>
在 v6 中僅支持 2 種占位符:動態:id
參數和*
通配符。通配符*
只能用在路徑的末尾,不能用在中間。
// 有效地址
/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*
// 無效地址
/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*
index
判斷該路由是否為索引路由(預設的子路由)。
<Route path="/teams" element={<Teams />}>
<Route index element={<TeamsIndex />} />
<Route path=":teamId" element={<Team />} />
</Route>
設置了 index 的 route 不允許存在子路由
loader
在路由組件渲染前執行並傳遞數據,組件可通過 useLoaderData 獲取 loader 的返回值。
createBrowserRouter([
{
element: <Teams />,
path: "/",
// 打開配置將造成死迴圈,因為 /view 也會觸發 / 的 loader
// loader: async () => {
// return redirect('/view');
// },
children: [
{
element: <Team />,
path: "view",
loader: async ({ params }) => {
return fetch(`/api/view/${params.id}`);
},
},
],
},
]);
需要註意的是,loader 是並行觸發,匹配多個 route,這些 route 上如果都存在 loader,都會執行。
想要針對特定的路由,可以採用如下寫法:
export const loader = ({ request }) => {
if (new URL(request.url).pathname === "/") {
return redirect("/view");
}
return null;
};
element/Component
// element?: React.ReactNode | null;
<Route path="/a" element={<Properties />} />
// Component?: React.ComponentType | null;
<Route path="/a" Component={Properties} />
與 v3 相比,v6 是大寫開頭的 Component。
v6 更推薦採用 element
的方式,可以非常方便的傳遞 props
中心化配置
在 v6 版本支持中心化配置,可以通過 createHashRouter 進行配置。
使用如下,結構就是 route 的定義:
export const getRoutes = createHashRouter([
{
path: '/',
Component: AuthLayout,
children: [
...commonRouteConfig,
{
Component: SideLayout,
children: [
{
path: 'metaDataCenter',
Component: MetaDataCenter,
},
{
path: 'metaDataSearch',
Component: MetaDataSearch,
},
{
path: 'metaDataDetails',
Component: MetaDataDetails,
},
{
path: 'dataSourceDetails',
Component: MetaDataDetails,
},
}
]
}
]
引入如下:
import { RouterProvider } from 'react-router-dom';
<RouterProvider router={getRoutes} />
與 v3 相比:
- component -> Component
- childRoutes -> children
- 增加 loader
nameindexRoute,採用佈局 route- 在佈局組件中,使用
進行占位展示,而不是 children - 在 v3 中路徑前帶 /代表絕對路徑,在 v6 中不管帶不帶都是相對父級的路徑,推薦不帶 /
- 配合 RouterProvider 使用
組件化路由
在組件內使用:
- Routes: 當地址發生變化,Routes 會在 Route 中進行匹配(原v5 中 Switch)
- Route:子路由信息
// This is a React Router v6 app
import {
BrowserRouter,
Routes,
Route,
Link,
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="users/*" element={<Users />} />
</Routes>
</BrowserRouter>
);
}
function Users() {
return (
<div>
<nav>
<Link to="me">My Profile</Link>
</nav>
<Routes>
<Route path=":id" element={<UserProfile />} />
<Route path="me" element={<OwnUserProfile />} />
</Routes>
</div>
);
}
<Route path>
和<Link to>
是相對父元素的地址。- 你可以把 Route 按你想要的任何順序排列,Routes 會根據當前路由信息進行生成權重,進行排序,在匹配最佳路由
// 動態路由權重,比如 /foo/:id
const dynamicSegmentValue = 3;
// 索引路由權重,也就是加了 index 為 true 屬性的路由
const indexRouteValue = 2;
// 空路由權重,當一段路徑值為空時匹配,只有最後的路徑以 / 結尾才會用到它
const emptySegmentValue = 1;
// 靜態路由權重
const staticSegmentValue = 10;
// 路由通配符權重,為負的,代表當我們寫 * 時實際會降低權重
const splatPenalty = -2;
路由跳轉
useNavigate
declare function useNavigate(): NavigateFunction;
interface NavigateFunction {
(
to: To,
options?: {
replace?: boolean;
state?: any;
relative?: RelativeRoutingType;
}
): void;
(delta: number): void;
}
在組件內原本採用 history 進行跳轉,在 V6 修改成使用 navigate 進行跳轉。
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
如果需要替換當前位置而不是將新位置推送到歷史堆棧,請使用 navigate(to, { replace: true })
。 如果你需要增加狀態,請使用 navigate(to, { state })
如果當前正在使用 history 中的 go、goBack 或 goForward 來向後和向前導航,則還應該將它們替換為 navigate 的第一個數字參數,表示在歷史堆棧中移動指針的位置
// v3 -> v6
go(-2)} -> navigate(-2)
goBack -> navigate(-1)
goForward -> navigate(1)
go(2) -> navigate(2)
Navigate
declare function Navigate(props: NavigateProps): null;
interface NavigateProps {
to: To;
replace?: boolean;
state?: any;
relative?: RelativeRoutingType;
}
如果你更喜歡使用聲明式 API 進行導航( v5 的 Redirect),v6 提供了一個 Navigate 組件。像這樣使用它:
import { Navigate } from "react-router-dom";
function App() {
return <Navigate to="/home" replace state={state} />;
}
註意:v6
history
history 庫是 v6 的直接依賴項,在大多數情況下不需要直接導入或使用它。應該使用 useNavigate 鉤子進行所有導航。
然而在非 tsx 中,如 redux 、 ajax 函數中。我們是無法使用react hooks的。
這個時候可以使用 location ,或者 history 進行跳轉。
history.push("/home");
history.push("/home?the=query", { some: "state" });
history.push(
{
pathname: "/home",
search: "?the=query",
},
{
some: state,
}
);
history.go(-1);
history.back();
location
採用 window.location 對象進行跳轉。
window.location.hash = '/'
傳參
query
// V3
type Location = {
pathname: Pathname;
search: Search;
query: Query;
state: LocationState;
action: Action;
key: LocationKey;
};
// V6
type Location = {
pathname: Pathname;
search: Search;
state: LocationState;
key: LocationKey;
};
在 v3 中,我們可以通過 location.query 進行 Url 的參數獲取或設置,而在 v6 中是不支持的。
在使用 useNavigate 時,接收一個完整的 pathname,如:/user?name=admin
在我們自己的工具庫 dt-utils 中,新增 getUrlPathname 方法用來生成 pathname。
getUrlPathname(pathname: string, queryParams?: {}): string
// example
DtUtils.getUrlPathname('/metaDataSearch', { metaType, search })
獲取時使用 getParameterByName 進行獲取單個 query param。也新增了 getUrlQueryParams 方法獲取所有的 query params
// getParameterByName(name: string, url?: string): string | null
// 需要註意 getParameterByName 返回的是 null。在多數情況下,需要轉成 undefined
const standardId = DtUtils.getParameterByName('standardId') || undefined;
// getQueryParams(url: string): Record<string, string>
const query = DtUtils.getUrlQueryParams(location.search);
params
通過 useParams 獲取到路由上的參數。
import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';
function ProfilePage() {
// Get the userId param from the URL.
let { userId } = useParams();
// ...
}
function App() {
return (
<Routes>
<Route path="users">
<Route path=":userId" element={<ProfilePage />} />
</Route>
</Routes>
);
}
state
在進行路由跳轉時可以通過傳遞 state 狀態進行傳參。
// route 傳遞
<Route path="/element" element={<Navigate to="/" state={{ id: 1 }} />} />
// link 傳遞
<Link to="/home" state={state} />
// 跳轉傳遞
navigate('/about', {
state: {
id: 1
}
})
// 獲取 state
export default function App() {
// 通過 location 中的 state 獲取
let location = useLocation();
const id = location.state.id
return (
<div className="App">
<header>首頁</header>
<p>我的id是:{id}</p>
</div>
);
}
Outlet
可通過 useOutletContext 獲取 outlet 傳入的信息。
function Parent() {
const [count, setCount] = React.useState(0);
return <Outlet context={[count, setCount]} />;
}
import { useOutletContext } from "react-router-dom";
function Child() {
const [count, setCount] = useOutletContext();
const increment = () => setCount((c) => c + 1);
return <button onClick={increment}>{count}</button>;
}
路由跳轉前攔截
在 v3 中使用 setRouteLeaveHook 進行路由的攔截,在 v6 被移除了。
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (!this.state.finishRule) {
return '規則還未生效,是否確認取消?';
}
return true;
});
在 V6 中採用 usePrompt 進行組件跳轉攔截。
需要註意的是,由於 usePrompt 在各瀏覽器中交互可能不一致。
目前可攔截前進,後退,正常跳轉。
刷新頁面不可攔截。
/**
* Wrapper around useBlocker to show a window.confirm prompt to users instead
* of building a custom UI with useBlocker.
*
* Warning: This has *a lot of rough edges* and behaves very differently (and
* very incorrectly in some cases) across browsers if user click addition
* back/forward navigations while the confirm is open. Use at your own risk.
*/
declare function usePrompt({ when, message }: {
when: boolean;
message: string;
}): void;
export { usePrompt as unstable_usePrompt };
針對這個功能,封裝了一個 usePrompt
import { unstable_usePrompt } from 'react-router-dom';
import useSyncState from '../useSyncState';
/**
* 攔截路由改變
* @param {boolean} [initWhen = true] 是否彈框
* @param {string} [message = ''] 彈框內容
* @returns {(state: boolean, callback?: (state: boolean) => void) => void}
*/
const usePrompt = (initWhen = true, message = '') => {
const [when, setWhen] = useSyncState(initWhen);
unstable_usePrompt({ when, message });
return setWhen;
};
export default usePrompt;
// example
import usePrompt from 'dt-common/src/components/usePrompt';
const EditClassRule = (props: EditClassRuleProps) => {
const setWhen = usePrompt(
checkAuthority('DATASECURITY_DATACLASSIFICATION_CLASSIFICATIONSETTING'),
'規則還未生效,是否確認取消?'
);
return (
<EditClassRuleContent {...(props as EditClassRuleContentProps)} setFinishRule={setWhen} />
);
};
router Props 註入
路由註入
在 V3 中 router 會給每一個匹配命中的組件註入相關的 router props
- location: 當前 url 的信息
- params: 路由參數,刷新不會重置
- route:所有路由配置信息
- routerParams: 路由參數,刷新後重置
- router:router 實例,可以調用其中的各種方法,常用的有:push、go、goBack
- routes:當前路由麵包屑
註入 props 在 V6 是沒有的。
withRouter 註入
v3 中的 withRouter 將 react-router 的 history、location、match 三個對象傳入props對象上。
在 v6 上 withRouter 這個方法也是沒有的。
實現 withRouter
在 v6 中,提供了大量 hooks 用於獲取信息。
獲取 location 的 useLocation。獲取路由 params 的 useParams,獲取 navigate 實例的 useNavigate 等。
實現了一個 withRouter 的高階函數,用於註入這 3 個 props。
這裡沒有直接傳入,採用 router 對象的原因是:
- 考慮 props 的覆蓋,像 params 都是大概率出現的名字。
- 原有使用中,大部分都是直接從 this.props 點出來,可以與升級前有個區分,避免混淆。
import React from 'react';
import {
useNavigate,
useParams,
useLocation,
Params,
NavigateFunction,
Location,
} from 'react-router-dom';
export interface RouterInstance {
router: {
params: Readonly<Params<string>>;
navigate: NavigateFunction;
location: Location;
};
}
function withRouter<P extends RouterInstance = any, S = any>(
Component: typeof React.Component<P, S>
) {
return (props: P) => {
const params = useParams();
const navigate = useNavigate();
const location = useLocation();
const router: RouterInstance['router'] = {
params,
navigate,
location,
};
return <Component {...props} router={router} />;
};
}
export default withRouter;
// example
export default withRouter<IProps, IState>(Sidebar);
總結
該篇文章中主要記錄了,我們項目從 [email protected] 升級到 @6.x 遇到的一些問題以及相關的解決方案,也簡單講解了 v3 與 v6 的部分差異,歡迎大家討論提出相關的問題~
最後
歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star
- 大數據分散式任務調度系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大數據領域的 SQL Parser 項目——dt-sql-parser
- 袋鼠雲數棧前端團隊代碼評審工程實踐文檔——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko