react-router|react-router v3 升级至 v6 探索小结

背景 在当前业务项目中使用的 react-router 版本为 3.x, 而当前主流使用的是 5.x 以上,
本文就来探究 react-router 升级的方案
当前情况 目前使用的是 react-router3.x 版本 再加上和 redux 的搭配库 react-router-redux 一起使用的
4.x 5.x API 的变动

因为 4 和 5 之间差别不是很大, 所以就放一起讲了
  1. 路由不能集中在一个文件中
  2. 具象为某一类, 比如: , 等等
  3. 组件来匹配路由, 排他性路由
  4. 组件 , 组件
  5. 用exact属性代替了 `
  6. react-router-dom 的出现, 只需要依赖此组件即可
  7. 支持 React 16 , 兼容 React >= 15
  8. Route 组件 path 可以为数组
  9. 如果没有匹配的路由,也可通过
6.x API 的变动
  1. 重命名为 , 不再需要该exact。
  2. 的新特性变更。
  3. 再度支持路由嵌套
  4. 替代
  5. useNavigate代替useHistory
  6. 删除 组件
  7. 新钩子useRoutes代替react-router-config
  8. 大小减少:从20kb8kb
    9.增强的路径模式匹配算法。
小结 从 3 到 4, 5 之间有许多 break change, 同样地, 4,5 到 6 之间也是这样
所以当前项目如果是 3 的话, 我们就准备一口气升级到 6, 避免中间的多重更改
升级的痛点 API 修改:
一般来说, 唯一的难点在于旧 API 的语法, 调用发生了变化, 导致一旦升级, 所有的地方都要重新写一遍
API 的删除:
  • 有出现新 API 的替换 这种情况是和修改一样的
  • 单纯的删除, 这里的话也是需要所有的地方修改的, 但是这种情况比较少, 而且被删除的 API 用到的地方也很少
API 新增:
单纯的新增并不影响现有的升级
同时 API 我们需要有所区分
  1. 配置型 API, 这种一般只会使用一次, 比如 , 只在路由配置页面使用, 那我们升级的时候直接修改便可以了
  2. 使用型 API, 这类 api 覆盖比较广泛, 比如说 router.push 改成了 history.push
升级 现在开始我们的升级
redux 升级
需要升级 redux 相关库:
  • react-redux^6.0.0
  • redux-first-history
可以删除库: react-router-redux
connected-react-router 只支持 v4 和 v5, 这里我们使用 redux-first-history, 更小, 更快的替代方案
store.js:
import { createStore, combineReducers, applyMiddleware } from "redux"; import { composeWithDevTools } from "redux-devtools-extension"; import { createReduxHistoryContext } from "redux-first-history"; import { createBrowserHistory } from 'history'; // 原有 routerMiddleware 来自于 react-router-redux const { createReduxHistory, routerMiddleware, routerReducer } = createReduxHistoryContext({ history: createBrowserHistory(), //other options if needed }); export const store = createStore( combineReducers({ router: routerReducer //... reducers //your reducers! }), composeWithDevTools( applyMiddleware(routerMiddleware) ) ); export const history = createReduxHistory(store);

关于 redux-first-history 仓库, 如果有依赖 redux-devtools, redux-devtools-log-monitor 等库, 可以不使用它
这样使用:
import { compose, createStore, combineReducers, applyMiddleware } from 'redux'; import DevTools from '../utils/DevTools'; // 省略 createReduxHistoryContextconst enhancer = compose( applyMiddleware( // ...省略 logger, routerMiddleware ), DevTools.instrument({maxAge: 10}) ); export const store = createStore( combineReducers({ router: routerReducer // ...省略 }), enhancer );

app.js:
import { Provider } from "react-redux"; import { HistoryRouter as Router } from "redux-first-history/rr6"; import { store, history } from "./store"; const App = () => ( //..... );

router
添加新的库:
  • react-router-dom^6.3.0
    react-router的依赖可以直接去掉
经过上面 redux 的替换, 我们已经拥有了 store, history, Router 等几个重要属性了
接下来只需要对 routes 进行控制即可:
} />

有一点需要注意, 不管在 组件还是 组件中都无法通过 props 来获取路由对象了
想要在 组件中显示 组件, 则需要另一个操作:
import { Outlet } from "react-router-dom"; function App(props) { // 其中 Outlet 就是类似于 children 的占位符 return <> // ... }

之后就是
在 hooks 中的用法:
import { useNavigate } from "react-router-dom"; // hooks const navigate = useNavigate(); //这会将新路线推送到导航堆栈的顶部 navigate("/new-route"); //这会将当前路线替换为导航堆栈中的新路由 navigate("/new-route", { replace: true });

api 的改动 从 v3 升级之后, 常用的 Link 会从 react-router 移除, 放进 react-router-dom 中, 那么怎么修改会比较方便呢
关于 withRouter
在 v6 中, 官方包不会自带这个组件了, 因为我们可以通过他的 api 自由组合:
import { useLocation, useNavigate, useParams, } from "react-router-dom"; function withRouter(Component) { function ComponentWithRouterProp(props) { let location = useLocation(); let navigate = useNavigate(); let params = useParams(); return ( ); }return ComponentWithRouterProp; }

方案一
直接全部替换, 但是这也会碰到我们的问题所在: 当这些 API, 在某一些子文件包, 或者第三方组件中的时候,
API 的更新就变得异常艰难了, 这也是直接修改的问题点所在
方案二
当前的一个思路就是, 使用 alias 加上文件的兼容来解决这个问题, 比如我在项目中新建文件:
routerProxy.js
import * as ReactRouter from '../node_modules/react-router'; import {Link} from 'react-router-dom'; function withRouter(Component) { //省略 }export * from '../node_modules/react-router'; export {Link,withRouter} export default ReactRouter;

搭配 webpack 配置:
alias: { 'react-router': path.resolve(__dirname, './source/react-router-proxy.js'), }

这样运行的时候, 引用 react-router 的东西都会走到此文件中, 而此文件中从 node_modules 中引入, 并且加上兼容, 最终完成升级的过度
方案三
使用 babel 的转换来解决:
module.exports = function ({ types: t }) { const namespace = __dirname + '/../node_modules/react-router/es/'; const canReplace = ({ specifiers }) => { return ( specifiers.length > 0 && specifiers.every((specifier) => { return ( t.isImportSpecifier(specifier) && (specifier.imported.name === 'Link' || specifier.imported.name === 'withRouter') ); }) ); }; const replace = (specifiers) => { return specifiers.map(({ local, imported }) => { if (imported.name === 'Link') { return t.importDeclaration( [t.importDefaultSpecifier(local)], t.stringLiteral(`react-router-dom/${imported.name}`), ); }return t.importDeclaration( [t.importDefaultSpecifier(local)], t.stringLiteral(`${namespace}${imported.name}`), ); }); }; return { visitor: { ImportDeclaration(path) { if (path.node.source.value =https://www.it610.com/article/=='react-router') { if (canReplace(path.node)) { // 替换 path.replaceWithMultiple(replace(path.node.specifiers)); } } }, }, }; };

通过检测 import {Link} from 'react-router'等语句, 将其替换成 react-router-dom 仓库
方案小结
方案一, 能完美解决, 但是所花费的精力较多, 相对来说我们需要一个平滑的升级
方案二,三 虽然能解决问题, 但是依旧是短暂的, 不可持续的, 最终还是需要我们全面的替换
总结 针对 v3 升级的问题, v6 的变化过大, 升级到 v5 也是可以接受, 但是还是要关注最新版的情况
【react-router|react-router v3 升级至 v6 探索小结】升级的消息, 最好在官方网站上查看, 避免遗漏一些细节
关于 API 的修改, 需要有方案来解决他, 比如 antd 大版本升级, 就会有一个兼容包来迁移
当然也可以使用本文中的一些方法, 但是后面都要逐步替换
在替换时, 使用全局的查找功能, 避免遗漏的出现
对于三方库的兼容 也要进行关注, 寻找新版本的替代品, 如果找不到, 就需要自己来实现了
参考
  • https://juejin.cn/post/696624...
  • https://reactrouter.com/docs/...
  • https://github.com/salvoravid...

    推荐阅读