

React [email protected] 升级到 @6.x 的实施方案 - 袋鼠云数栈前端
source link: https://www.cnblogs.com/dtux/p/17616258.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们是袋鼠云数栈 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>;
}
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
- name
- indexRoute ,采用布局 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 默认使用push逻辑,你可以通过 replaceProps 来更改它。
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
Recommend
-
15
缓存&PWA 实践 从上一篇《前端动画实现与原理分析》,我们从 Performance 进行动画的性能分析,并根据 Performance 分析来优化动画。但,前端不仅仅是实现流畅的...
-
12
欲语还休,欲语还休,却道天凉好个秋 ---- 《丑奴儿·书博山道中壁》辛弃疾 什么是 SSR ShadowsocksR?阴阳师?FGO? Server-side rendering (SSR)是应用程序通过在服务器上显示网页而...
-
6
在 7 月 28 日的袋鼠云 2022 产品发布会上,基于对现在与未来的畅想,袋鼠云产研负责人思枢正式发布了全新的四大产品体系。其中的数栈 DTinsight,相信大家都很熟悉了,不同于数驹这位新朋友,数栈作为袋鼠云和大家经常见面的 “老朋友”,在保持初心的同时,这...
-
20
SSO介绍 随着企业的发展,一个大型系统里可能包含 n 多子系统, 用户在操作不同的系统时,需要多次登录,很麻烦,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录。 web 系统 由单系统...
-
15
前段时间碰到了一个 Keybinding 相关的问题,于是探究了一番,首先大家可能会有两个问题:Monaco Editor 是啥?Keybinding 又是啥? Monaco Editor: 微软开源的一个代码编辑器,为 VS Code 的编辑器提供支持,Monaco Editor 核...
-
18
我的 React 最佳实践 There are a thousand Hamlets in a thousand people's eyes. ----- 威廉·莎士比...
-
8
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 兼容性问题 第三方依赖兼容问题
-
11
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。 本文作者:霜序(
-
9
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。 本文作者:木杪...
-
4
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 本文作者:佳岚
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK