react-router v3 升級至 v6 探索小結

Grewer發表於2022-04-14

背景

在當前業務專案中使用的 react-router 版本為 3.x, 而當前主流使用的是 5.x 以上,
本文就來探究 react-router 升級的方案

當前情況

目前使用的是 react-router3.x 版本 再加上和 redux 的搭配庫 react-router-redux 一起使用的

4.x 5.x API 的變動

因為 4 和 5 之間差別不是很大, 所以就放一起講了
  1. 路由不能集中在一個檔案中
  2. <Router> 具象為某一類, 比如: <BrowserRouter> ,<HashRouter> 等等
  3. <Switch> 元件來匹配路由, 排他性路由
  4. <Link> 元件 , <NavLink> 元件
  5. 用exact屬性代替了 <IndexRoute>`
  6. react-router-dom 的出現, 只需要依賴此元件即可
  7. 支援 React 16 , 相容 React >= 15
  8. Route 元件 path 可以為陣列
  9. 如果沒有匹配的路由,也可通過 <Redirect>

6.x API 的變動

  1. <Switch>重新命名為<Routes> , 不再需要該exact。
  2. <Route>的新特性變更。
  3. 再度支援路由巢狀
  4. <Navigate>替代<Redirect>
  5. useNavigate代替useHistory
  6. 刪除 <Prompt> 元件
  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, 這種一般只會使用一次, 比如 <Router>, 只在路由配置頁面使用, 那我們升級的時候直接修改便可以了
  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';

// 省略 createReduxHistoryContext

const 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 = () => (
    <Provider store={store}>
        <Router history={history}>
            //.....
        </Router>
    </Provider>
);

router

新增新的庫:

  • react-router-dom^6.3.0
    react-router的依賴可以直接去掉

經過上面 redux 的替換, 我們已經擁有了 store, history, Router 等幾個重要屬性了

接下來只需要對 routes 進行控制即可:

<Routes>
    <Route path={url} element={<App/>}>
        <Route path={url2} element={<Foo/>} />
    </Route>
</Routes>
有一點需要注意, 不管在 <App> 元件還是 <Foo> 元件中都無法通過 props 來獲取路由物件了

想要在 <APP> 元件中顯示 <Foo> 元件, 則需要另一個操作:

import { Outlet } from "react-router-dom";

function App(props) {
    // 其中 Outlet 就是類似於 children 的佔位符
    return <>
        // ...
        <Outlet />
    </>
}

之後就是

在 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 (
      <Component
        {...props}
        router={{ location, navigate, params }}
      />
    );
  }

  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 === 'react-router') {
                    if (canReplace(path.node)) {
                        // 替換
                        path.replaceWithMultiple(replace(path.node.specifiers));
                    }
                }
            },
        },
    };
};

通過檢測 import {Link} from 'react-router'等語句, 將其替換成 react-router-dom 倉庫

方案小結

方案一, 能完美解決, 但是所花費的精力較多, 相對來說我們需要一個平滑的升級
方案二,三 雖然能解決問題, 但是依舊是短暫的, 不可持續的, 最終還是需要我們全面的替換

總結

針對 v3 升級的問題, v6 的變化過大, 升級到 v5 也是可以接受, 但是還是要關注最新版的情況

升級的訊息, 最好在官方網站上檢視, 避免遺漏一些細節
關於 API 的修改, 需要有方案來解決他, 比如 antd 大版本升級, 就會有一個相容包來遷移
當然也可以使用本文中的一些方法, 但是後面都要逐步替換
在替換時, 使用全域性的查詢功能, 避免遺漏的出現
對於三方庫的相容 也要進行關注, 尋找新版本的替代品, 如果找不到, 就需要自己來實現了

參考

相關文章