積夢前端的路由方案 ruled-router

積夢(https://jimeng.io ) 是一個爲製造業製做的一個平臺.
積夢的前端基於 React 作開發的. 早期使用 React Router.
後來出現了一些 TypeScript 集成還有定製化的需求, 本身探索了一套方案.前端

使用 React Router 遇到的問題

React Router 自己是一個較爲穩妥並且全面的方案, 早期咱們使用了它.
後面隨着積夢數據平臺的頁面的重構, 遇到了一些問題.
積夢的管理界面從頂層往內存在多個層級, 複雜的狀況會出現五六層嵌套,git

導航欄 -> 子導航 -> 標籤頁 -> 功能 -> 子頁面

雖然通常的狀況只是三四個層級, 可是頁面的嵌套大量存在,
早期的咱們辦法是定義一個 basePath 變量用來表示外層路由,github

<Route
  path={`${this.props.basePath}/:page`}
  render={(props) => {
    return this.renderSubPage(props.match.params.page);
  }}
/>

而後在內部跳轉時, 也會使用 basePath 變量快速生成路徑,數組

<Redirect to={`${this.props.basePath}/${EWorkflowPage.Step}`} />

這樣手動傳遞偶爾會出錯, 特別是當頁面結構發生一些修改的時候.
通過一兩次導航的重構, 咱們在局部出現了一些代碼, 沒法正確跳轉.
雖然靠着測試逐步修復了問題, 可是隨着頁面增多, 這個問題不能輕視.性能優化

我以爲這個問題是兩部分,bash

一方面是 TypeScript 的類型檢查沒有幫助到的路由部分,
React Router 當中基本上經過字符串定義的路徑, 這些不容易被類型檢查.
特別是拼接的路由, 發生改變之後就難以準確追蹤了.函數

另外一方面, 我認爲 React Router 的規則也限制了 JavaScript 代碼的使用.
相對於 React Router 經過 Context 傳遞路由狀態的方案, 更傾向於代碼.
基於 switch/case 還有函數組成的控制流, 有更爲靈活的應對的辦法.工具

路由的解析 ruled-router

我同事和我都有一些使用基於路由配置生成路由的經驗, 商量後我打算嘗試.
個人想法是定義路由規則, 而後將路由解析稱爲對象, 而後經過代碼進行控制.性能

好比這樣一個路徑:測試

/plants/152883204915/qualityManagement/measurementData/components/21712526851768321/processes/39125230470234114

進行拆解之後我認爲就是幾個層級:

/plants/152883204915
 /qualityManagement
  /measurementData/components/21712526851768321/processes/39125230470234114

跟 React Router 直接用標籤作匹配的寫法不一樣, 我認爲路由應該先被解析,
該路由包含了頁面的信息, 也包含了響應的參數, 實際上對應一個鏈表, 用對象表示是:

{
  "name": "plants", // <--- 第一層路由
  "matches": true,
  "restPath": null,
  "data": {
    "plantId": "152883204915"
  },
  "query": {}, 
  "next": {
    "name": "qualityManagement", // <--- 第二層路由
    "matches": true,
    "restPath": null,
    "data": {},
    "query": {},
    "next": {
      "name": "measurementData", // <--- 第三層路由
      "matches": true,
      "restPath": null,
      "data": {
        "componentId": "21712526851768321",
        "processId": "39125230470234114"
      },
      "query": {},
      "next": null
    }
  }
}

這是一個比較清晰的層級的結構, 很容易用 switch/case 判斷渲染對應的子頁面.

而解析這個路由所須要的規則, 也能夠經過大體這樣的代碼定義出來.

let pageRules = [
  {
    path: "plants/:plantId",
    next: [
      {
        path: "qualityManagement",
        next: [
          {
            path: "measurementData/components/:componentId/processes/:processId"
          }
        ]
      }
    ]
  },
];

這樣基於路由規則和解析函數, 路由定位的方案就變成了:

  • 從 URL 改變的事件獲取到 location.hash 的字符串,
  • 用函數解析獲得路由信息的 JSON 樹,
  • 根據 JSON 逐級傳遞, 用 switch/case 跳轉到對應的頁面.

示例代碼好比:

render() {
  const nextRoute = this.props.route.next;

  switch (nextRoute && nextRoute.name) {
    case RouteOutgoing.Records:
      return <Records route={nextRoute.next} plantId={plantId} />;
    case RouteOutgoing.Settings:
      return <Settings route={nextRoute} />;
  }

  return (
    <Redirect
      to={router.getPath(RouteOutgoing.Records, {
        plantId,
      })}
    />
  );
}

解析的代碼在 ruled-router 能夠找到, 使用 TypeScript 開發, 有基礎的類型約束.

從代碼看, 因爲路由層級的顯式處理, 會存在很多的 .next 須要手工維護, 對於維護有些囉嗦.
固然這個寫法好的一面是路由信息隨時能夠打印和調試, 方便定位問題.

路由的跳轉(code generator)

在 React Router 當中路由的跳轉相對簡單, 提供路徑的字符串表示便可完成:

history.push('/a/b/${c}/d')

可是前面說了, 這樣沒法進行類型檢測, 沒法定位出現問題的路由位置.
咱們嘗試了幾個方案, 用比較多的一個方案是給路由定義惟一的 ID 的枚舉值, 而後查找枚舉值跳轉.
後來我從另外一個思路開始嘗試, 試着用不一樣的方案來搭配 TypeScript.

好比說這樣的一套規則, 定義 3 個頁面:

let routeRules = [
  { path: "home" },
  { path: "content" },
  { path: "else" },
  { path: "", name: "home" }
]

那麼對應這個路由我就生成響應的代碼, 這段代碼, 就是 TypeScript 能夠作類型檢查的了,

export let genRouter = {
  home: {
    name: "home",
    raw: "home",
    path: () => `/home`,
    go: () => switchPath(`/home`),
  },
  content: {
    name: "content",
    raw: "content",
    path: () => `/content`,
    go: () => switchPath(`/content`),
  },
  else: {
    name: "else",
    raw: "else",
    path: () => `/else`,
    go: () => switchPath(`/else`),
  },
  _: {
    name: "home",
    raw: "",
    path: () => `/`,
    go: () => switchPath(`/`),
  },
};

其中 .go() 方法用於跳轉, .path() 方法用於生成其餘組件須要的字符串形態.
固然, 維護這樣的一段代碼, 成本並不低, 可是好在這樣高度重複的代碼是能夠用代碼生成的,
因而咱們增長了 router-code-generator 這個腳本, 用於生成路由代碼.

這樣, 添加新路由的時候就須要,

  • 在 rules 當中添加路由規則,
  • 運行腳本生成路由的代碼,
  • 在須要跳轉的位置引用 genRouter 對象, 調用對應方法進行跳轉.

實際業務當中的代碼固然會複雜不少, 項目最終生成出來是兩千多行的路由文件,

export let genRouter = {
  plants_: {
    name: "plants",
    raw: "plants/:plantId",
    path: (plantId: Id) => `/plants/${plantId}`,
    go: (plantId: Id) => switchPath(`/plants/${plantId}`),
    information: {
      name: "information",
      raw: "information",
      path: (plantId: Id) => `/plants/${plantId}/information`,
      go: (plantId: Id) => switchPath(`/plants/${plantId}/information`),
      products: {
        name: "material.finished",
        raw: "products",
        path: (plantId: Id) => `/plants/${plantId}/information/products`,
        go: (plantId: Id) => switchPath(`/plants/${plantId}/information/products`),

實際項目當中的腳本生成也是個須要處理的地方, 咱們用 Webpack 將這部分代碼打包運行,
性能上還好, 關掉類型檢查的話幾秒鐘內能夠完成, 具體看示例的代碼:
https://github.com/jimengio/t...

類型檢查的覆蓋

前面的兩部分, 覆蓋了路由的解析, 還有路由的跳轉, 完成了基本的路由的功能.

路由解析部分, 路由規則能夠經過 JSON 結構定義, 基本能獲得 TypeScript 的提示.
路由的解析結果, 是一棵大的 JSON 的樹, 這中間有很多動態的部分, 須要開發時本身留意.
路由跳轉的代碼, 整個 Object 定義的結構能夠被 TypeScript 解析, 基本上有完整的補全.

雖然並不完美, 可是很大程度利用了 TypeScript 的自動補全以及類型檢查簡化了書寫.
當路由有增改時, 經過運行腳本還有執行類型檢查, 比較容易定位到發生改變的部分.

該方案的不足

  • 路由劫持等功能

React Router 提供的功能顯然遠不止解析和跳轉, 還有一些頁面跳轉相關的鉤子, 甚至漸變等效果.
ruled-router 的方案沒有去實現相關的功能.

  • 腳手架比較麻煩

從前面的描述也能看出來, 這一整套寫法, 特別是後面跳轉的寫法, 引入了大量腳手架.
須要專門寫一個 Webpack 配置來生成路由, 通常項目來講以爲很是繁瑣了.

實際在項目當中, 因爲咱們有着較深的路由層級, 實際代碼看上去又長又囉嗦:

genRouter.plants_.product.batch_.ooc.go(plantId, value);

genRouter.plants_.model.projects._status.go(this.props.plantId, record.id);

這代碼是靠着 VS Code 提供的代碼補全才能很快寫出來... 也就是和 TypeScript 以及 VS Code 等工具綁定死了.

結尾

除了上面介紹的, 其餘一些功能也在 ruled-router 方案裏作了一些支持:

  • Query 參數. 能夠被解析, 也能夠在跳轉代碼當中被生成出來. 基本可用. 只是類型有缺失.
  • 性能優化的問題, 須要配合 shouldComponentUpdate 或者 useMemo 來優化, 就用到易於匹配的字符串形態.

特別是隨着項目規模增長, 幾百個大小頁面的木有, 更多會須要類型檢查工具來幫助咱們作校驗.
固然目前的方案在開發當中依然有着細節上的各類須要優化的地方, 要後續再想辦法進一步優化.

其餘關於積夢前端的模塊和工具能夠查看咱們的 GitHub 主頁 https://github.com/jimengio .
招聘的計劃和條件也在 GitHub 上有給出 https://github.com/jimengio/h... .

相關文章
相關標籤/搜索