淺談:前端路由原理解析及實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ed/ede5a4e4274dc2aa7e80c5f56f2861f9.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者|張小俊來源|","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/Yg8vDgT6oCgUkfzb11Ecvw","title":"","type":null},"content":[{"type":"text","text":"爾達 Erda 公衆號","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"導讀","attrs":{}},{"type":"text","text":":其實在前端領域,還有很多基礎的東西有待深入去做。不爲造輪子而造輪子,纔是在做有意義的事情。所以,我們決定撰寫《Erda 前端之聲》系列文章,深入剖析我們在前端探索過程中的一些落地經驗,以此助力在前端之路上奮進的開發者們,能夠早日發掘屬於自己的精彩。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"系列文章推薦:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://mp.weixin.qq.com/s?__biz=Mzg2MDYzNTAxMw==&mid=2247484732&idx=1&sn=52127f0ec99288f7b15f3676e97866a2&chksm=ce222fd6f955a6c02b0a547d74f9e7716593e5d409efc6d93d5fbc72e31d50d42d5ac5db4865&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"《靈魂拷問:我們該如何寫一個適合自己的狀態管理庫?》","attrs":{}}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"《淺談:前端路由原理解析及實踐》(本文)","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家好,這裏是 ","attrs":{}},{"type":"link","attrs":{"href":"https://link.zhihu.com/?target=https%3A//github.com/erda-project","title":"","type":null},"content":[{"type":"text","text":"Erda","attrs":{}}]},{"type":"text","text":" 技術團隊。作爲 Erda 項目的前端,","attrs":{}},{"type":"link","attrs":{"href":"https://link.zhihu.com/?target=https%3A//github.com/erda-project/erda-u","title":"","type":null},"content":[{"type":"text","text":"Erda-UI","attrs":{}}]},{"type":"text","text":" 項目從最初開發到現在開源,業務複雜度在不斷遞增,項目的代碼文件已經近 2000,項目內部的路由配置已經超過 500 個。本文會先簡單介紹一下前端路由原理,以及 React-Router 的基礎使用,接着會主要分享 Erda-UI 項目在路由上實踐的一些拓展功能。​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在單頁面應用(SPA)已經非常成熟的當下,路由也成了前端項目的主要配置,我們使用路由來管理項目頁面的組成結構,各大前端框架也都有各自成熟的路由解決方案(React: React-Router、Vue: Vue-Router)。而在複雜的業務系統中,往往存在很多跟路由相關的其他邏輯,比如權限、麪包屑等。我們希望這部分邏輯能整合到路由的配置當中,這樣能有效的減輕開發和維護的負擔。Erda-UI 項目使用 React 框架,所以下面的內容都基於 React-Router。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"路由原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由的基本原理,就是在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不刷新","attrs":{}},{"type":"text","text":"瀏覽器的情況下修改瀏覽器鏈接,同時","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"監聽鏈接的變化","attrs":{}},{"type":"text","text":"並找到匹配的組件渲染。滿足這兩個條件即可實現。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由的實現通常有以下兩種形式:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hash ( /#path )","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"history ( /path )","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hash 在瀏覽器中默認是作爲錨點來使用的,在 hash 模式中,url 裏始終會有 #,沒有傳統 url 寫法那麼美觀,所以在不考慮兼容性的情況下使用 history 的模式是更好的選擇。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"hash","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hash 模式下,url 中 # 後面的部分只是一個客戶端狀態,當這部分變化時,瀏覽器本身就不會刷新,天生具備第一個條件(即在不刷新瀏覽器的情況下修改瀏覽器鏈接),同時通過監聽 hashChange 事件或註冊 onhashchange 回調函數來監聽 url 中 hash 值的變化。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"window.addEventListener('hashchange', hashChangeHandler); \n// or window.onhashchange = hashChangeHandler;\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"history","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"history 模式,是利用了 HTML5 中 history 的 API,history.pushState 和 history.replaceState 這兩個方法,可以在不刷新頁面的情況下,操作瀏覽器的歷史記錄,前者爲新增一條記錄,後者爲替換最後一條記錄。同時通過監聽 popState 事件或註冊 onpopstate 回調函數來監聽 url 的變化。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"window.addEventListener('popState', locationChangeHandler); \n// or window.onpopstate = locationChangeHandler;\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是這裏有一點需要注意,history.pushState 和 history.replaceState 是不會自動觸發 popState 的。只有在做出瀏覽器動作時,纔會觸發該事件,比如用戶點擊瀏覽器的回退按鈕。通常路由庫裏會封裝一個監聽方法,不管是調用 history.pushState、history.replaceState,還是用戶觸發瀏覽器動作導致的路由變化,都能夠觸發監聽函數。以 react-router-dom 中的 listen(部分爲僞代碼)爲例:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function setState(nextState) {\n _extends(history, nextState);\n\n history.length = history.entries.length;\n // 將路由變化使用 state 管理,在變化時,通知所有監聽者\n transitionManager.notifyListeners(history.location, history.action);\n}\n\n// 封裝 push、replace 等方法\nfunction push(path, state) {\n // ...\n globalHistory.pushState({\n key: key,\n state: state\n }, null, href);\n // ...\n setState({ // 手動觸發監聽\n action: action,\n location: location\n })\n}\n\n// popState 事件監聽,監聽事件同時 setState,通知 transitionManager 中的 listeners;\nfunction handlePopState(location){\n // ...\n setState(location)\n // ...\n}\n\n// 封裝 listen。\nfunction listen(listener) {\n var unlisten = transitionManager.appendListener(listener);\n window.addEventListener('popState', handlePopState); // 監聽瀏覽器事件。\n // ...\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"React-Router 路由基礎","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了方便展開下面的內容探討,本章節先簡單介紹一下 React-Router 相關基礎。​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基礎庫","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React-Router 相關的庫主要有以下幾個:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"react-router 核心庫","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"react-router-dom 基於 DOM 的路由實現,內部包含 react-router 的實現,使用時無需再引 react-router","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"react-router-native 基於 React Native 的路由實現","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"react-router-redux 路由和 Redux 的集成,不再維護","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"react-router-config 用於配置靜態路由","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"react-router-dom","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對應了路由的兩種實現方式,react-router-dom 庫也提供了兩個路由組件:BrowserRouter、HashRouter。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Route : 路由單元,配置一個 path 以及對應的渲染組件,其中 exact 表示精確匹配","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Switch: 控制渲染第一個匹配的路由組件","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Link: 鏈接組件,相當於 標籤","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redirect: 重定向組件","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由基本的使用如下:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"import { BrowserRouter, Link, Route, Switch, Redirect } from 'react-router-dom'\n\nfunction App(){\n return (\n \n home\n About\n \n \n \n \n \n \n )\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,還可以嵌套使用,即在組件內部再配置路由。在路由過多的情況下,可以通過這種方式將 Router 拆分,這讓 Router 更具有一般組件的特性,可以隨意嵌套。而組件中可以得到一個 math 的 props 來獲取上級路由的相關信息。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"import { BrowserRouter, Link, Route, Switch, Redirect } from 'react-router-dom'\n\nfunction App(){\n return (\n \n home\n Settings\n \n \n \n \n \n )\n}\n\nconst Setting = (props) => {\n const matchPath = props.match.path;\n return (\n
\n a\n b\n \n \n \n \n
\n )\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,項目中的路由除了數量比較多外,通常還會有一些需要集中處理的邏輯,分散的路由配置方式顯然不太適合,而 react-router-config 爲我們提供了方便的靜態路由配置,其本質就是將一份 config 轉換爲 Route 組件,而在組件渲染的方法 render 中,則可以根據業務情況來做一些統一的處理。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function renderRoutes(routes, extraProps, switchProps) {\n // ...\n return routes ? React.createElement(reactRouter.Switch, switchProps, routes.map(function (route, i) {\n return React.createElement(reactRouter.Route, {\n key: route.key || i,\n path: route.path,\n exact: route.exact,\n strict: route.strict,\n render: function render(props) {\n return route.render ? route.render(_extends({}, props, {}, extraProps, {\n route: route\n })) : React.createElement(route.component, _extends({}, props, extraProps, {\n route: route\n }));\n }\n });\n })) : null;\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Erda-UI 項目路由實踐","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由配置","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const routers = {\n path: ':orgName',\n mark: 'org',\n breadcrumbName: '{orgName}'\n routes: [\n {\n path: 'workBench',\n breadcrumbName: 'DevOps平臺',\n mark: 'workBench',\n routes: [\n {\n path: 'projects/:projectId',\n breadcrumbName: '',\n mark: 'project',\n AuthContainer: ProjectAuth,\n routes: [\n {\n path: 'apps',\n pageTitle: '應用列表',\n getComp: cb => cb(import('/xx/xx')),\n routes: [\n {\n path: 'apps/:appId',\n mark: 'application',\n breadcrumbName: '應用',\n AuthContainer: AppAuth,\n }\n ]\n },\n ]\n }\n ],\n },\n ]\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上我們可以看到,在配置中除了 path 之外,其他的字段似乎都和 React-Router 沒什麼太大關係,這些字段也正是我們實現跟路由相關邏輯的配置,下面我們會一一介紹。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由狀態管理:routeInfoStore","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了拓展路由相關功能,我們首先需要有一個路由對象爲我們提供數據支持,之所以需要這個對象,是因爲單個的路由信息不足以實現其他相關邏輯,我們需要更多路由信息,比如路由層級上的鏈路記錄,前後路由的狀態對比等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們使用一個 routeInfoStore 對象來管理路由相關的數據和狀態。這個對象可以在組件之間共享路由狀態(類似 Redux 中 store)。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過在 browserHistory.listen 中監聽並調用 routeInfoStore 中處理路由變化的方法($_updateRouteInfo)來更新路由數據和狀態。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"browserHistory.listen((loc) => {\n // 監聽路由變化觸發 routerStore 的更新,類似 Redux 中 dispatch;\n // 此處使用發佈訂閱模式 來實現觸發調用事件\n emit('@routeChange', routerStore.reducers.$_updateRouteInfo(loc));\n});\n\n\n// routeStore 中的數據\nconst initRouteInfo: IRouteInfo = {\n routes: [], // 當前路由所經過的層級,若路由在子模塊,則改子模塊所有的父模塊也會被記錄在內\n params: {}, // 當前 url 中路徑裏的所有變量\n query: {}, // 當前 url 中 search(?後面)的參數\n currentRoute: {}, // 當前匹配上的路由配置\n routeMarks: [], // 標記了 mark 的路由層級\n isIn: () => false, // 擴展方法:用於判斷是否在當前路由內\n isMatch: () => false,// 擴展方法:用於判斷是否匹配當前路由\n isEntering: () => false,// 擴展方法:用於判斷是否正在進入當前路由\n isLeaving: () => false,// 擴展方法:用於判斷是否離開當前路由\n prevRouteInfo: {}, // 上一次路由的信息\n};\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由監聽擴展:mark","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我們需要監聽路由在進入或離開某個範圍內,自動進行的一些前置初始化操作,比如進模塊 A,首先要獲取模塊 A 的權限,或者模塊 A 的一些基礎信息。離開模塊 A 時,需要去清空相關的信息。爲了做到這些監聽和初始化,我們需要兩個條件:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標記範圍的字段。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在路由變化的時候,判斷路由是否離開或進入相應的範圍。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在路由配置中添加了 mark 字段,用於標記當前路由的範圍,類似路由範圍的 id,需要保證全局唯一。而上文有說到 routeInfoStore 中,routeMarks 中會記錄路由鏈路層級的 mark 集合,prevRouteInfo 會記錄上一次路由信息。藉此,我們可以在 routerInfoStore 裏添加一些路由範圍判斷的函數 isIn、isEntering、isLeaving、isMatch。​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"isIn($mark) => boolean","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表示當前路由是否在某個範圍內。傳入一個 mark 值,通過 routeInfoStore 中 routeMarks 中是否包含來判斷:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// routeMarks 內記錄了路由經過的所有 mark 標記,通過判斷 mark 是否被包含\nisIn: (mark: string) => routeMarks.includes(mark), \n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"isEntering($mark) => boolean","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表示當前路由正在進入某個範圍,區別於 isIn, 這是一個正在進行時的判斷,表示上一次路由並不在該範圍,而當前這次在該範圍內。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//通過判斷 mark 被包含,同時上一次的路由不被包含,判斷是正在進入當前 mark。\nisEntering: (mark: string) => routeMarks.includes(mark) && !prevRouteInfo.routeMarks.includes(mark),\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"isLeaving($mark) => boolean","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跟 isEntering 相反,isLeaving 表示上一次路由在範圍內,而下一次路由離開範圍,即正在離開。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//通過判斷 mark 不被包含,同時上一次的路由被包含,判斷是正在離開當前 mark。 \nisLeaving: (mark: string) => !routeMarks.includes(mark) && prevRouteInfo.routeMarks.includes(mark),\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"isMatch($pattern) => boolean","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳入一個正則,判斷路由是否匹配正則,一般用於對當前路由的直接判斷:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//通過正則判斷\nisMatch: (pattern: string) => !!pathToRegexp(pattern, []).exec(pathname),\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"註冊監聽","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們提供了一個監聽的方法,可以在項目啓動時,由各個模塊註冊自己的路由監聽函數,而監聽函數中,則可以方便使用以上方法判斷路由的範圍。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 路由監聽註冊\nexport const listenRoute = (cb: Function) => {\n // getState 返回routeInfoStore 對象,其中包含了以上的判斷方法\n cb(routeInfoStore.getState(s => s)); \n \n // 路由變化時,調用監聽方法\n on('@routeChange', cb);\n};\n\n\n// 模塊 A 註冊\nlistenRoute((_routeInfo) => {\n const { isEntering, isLeaving } = _routeInfo;\n \n if(isEntering('markA')){\n // 初始化模塊 A\n }\n \n if(isLeaving('markA')) {\n // 清除模塊 A 信息\n }\n})\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由拆分:toMark","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當路由數量過大,一份路由數據嵌套可能很深,因此必然需要支持路由配置的拆分。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們提供了路由註冊的方法 registerRouter,不同模塊可以只註冊自己的路由,然後通過 toMark 字段來建立路由之間的所屬關聯,toMark 的值是另一個路由的標記 mark 值。在 registerRouter 內部,將所有路由整合成一份完整的配置。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 註冊 org 路由\nregisterRouter({\n path: ':orgName',\n mark: 'org',\n breadcrumbName: '{orgName}'\n});\n\n// 註冊 workBench 路由\nregisterRouter({\n path: 'workBench',\n breadcrumbName: 'DevOps平臺',\n mark: 'workBench',\n toMark: 'org', // 配置 workBench 路由屬於 org 的子路由\n});\n\n// 註冊 project 路由\nregisterRouter({\n path: 'projects/:projectId',\n breadcrumbName: '',\n mark: 'project',\n toMark: 'workBench', // 配置 project 路由屬於 workBench 的子路由\n AuthContainer: ProjectAuth,\n routes: [\n {\n path: 'apps',\n pageTitle: '應用列表',\n getComp: cb => cb(import('/xx/xx')),\n },\n ]\n});\n\n// 註冊 application 路由\nregisterRouter({\n path: 'apps/:appId',\n mark: 'application',\n toMark: 'project', // 配置 application 路由屬於 project 的子路由\n breadcrumbName: '應用',\n AuthContainer: AppAuth,\n})\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由組件異步加載:getComp","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們使用 getComp 的方式給單個路由配置組件,getComp 是一個異步方法引入一個組件,然後我們通過一個異步加載的高階組件來實現路由組件的加載。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 重寫 render\nmap(router, route => {\n return {\n ...route,\n render: (props) => asyncComponent(()=>route.getComp());\n }\n})\n\n// 異步組件\nexport const asyncComponent = (getComponent: Function) => {\n return class AsyncComponent extends React.Component {\n static Component: any = null;\n\n state = { Component: AsyncComponent.Component };\n\n componentDidMount() {\n if (!this.state.Component) {\n getComponent().then((Component: any) => {\n AsyncComponent.Component = Component;\n this.setState({ Component });\n });\n }\n }\n\n render() {\n const { Component } = this.state;\n if (Component) { // 當組件加載完成後,渲染\n return ;\n }\n return null;\n }\n };\n};\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"麪包屑:breadcrumbName","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Erda-UI 的業務中,路由的配置是一個樹形結構,進入子模塊路由則一定經過了父模塊路由,通過對路由數據的解析,我們能得到從根路由到當前路由所經過的層級鏈路,而路由層級鏈路剛好映射了麪包屑的層級。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過在路由配置中添加 breadcrumbName 字段,並在 routeInfoStore 的 routes 存儲路由的層級鏈路數據。因此麪包屑的數據可以直接通過 routers 中得到。​","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/8063fc9bd90a7224c2c07b53134d4df5.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"map(routes, route => {\n return {\n name: route.breadcrumbName,\n path: route.path,\n }\n})\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在配置中, breadcrumbName 可以是文字,也可以是字符串模板 {temp} 。這裏是利用了另一份 store 的數據來管理所有字符串模板對應的數據,渲染的時候,通過匹配 key 值獲取相應的展示文字。​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"路由鑑權: AuthContainer","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在項目中,路由是否能訪問,往往需要對應一些條件判斷(用戶權限、模塊是否開放等)。不同路由的鑑權條件可能不一樣,而且鑑權失敗的提示也可能需要個性化,或者可能存在鑑權不通過後頁面需要重定向等場景。這些都需要路由上的鑑權能個性化。就如 react-router-config 中的一樣,我們可以通過調整 Route 組件的 render 函數來達到這個目的。​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過在路由上配置 AuthContainer 組件來給路由做權限攔截,大致過程分兩步:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供一個鑑權組件 AuthComp,內部封裝鑑權相關邏輯及提示。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在渲染路由前,獲取這個鑑權組件 AuthComp,並重寫 render。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// AuthComp \nconst AuthComp = (props) => {\n const { children } = props;\n const [auth, setAuth] = React.useState(undefined);\n useMount(()=>{\n doSomeAuthCheck().then(()=>{\n setAuth(true)\n })\n })\n \n if( auth === undefined ){\n return
加載中
\n }\n return auth ? children :
您無權訪問,請聯繫管理員...
\n}\n\n// 重寫 render\nmap(router, route => {\n return {\n ...route,\n render: (props) => {\n const AuthComp = route.AuthContainer;\n const Comp = route.components;\n return (\n // 添加路由鑑權攔截\n {Comp ? : Comp }\n \n )\n }\n }\n})\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結及後續思考","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Erda-UI 項目中,我們通過以上的一些配置擴展,來集中管理所有的路由。這種方式可以簡單高效的維護路由本身以及擴展關聯業務邏輯。除此之外還可以做一些更靈活的事情,比如通過分析整個路由結構,生成可視化的路由樹,支持路由的動態調整等等。經過漫長的業務演進和內容完善,我們驗證了這種方式帶來的好處。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時我們也在不斷思考還可以改進的地方,比如:​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在有鏈路層級的模塊之間,路由的監聽如何做到異步串聯?","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如:模塊 A 包含模塊 B,在模塊 A 中註冊監聽初始化方法 initA,在模塊 B 中註冊 initB,如何控制 initB 在 initA 完成之後執行(若 initB 中需要使用到 initA 返回的結果時,則需要嚴格控制執行順序)。​","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"結語","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文中的內容都是很常見的一些場景,爲了貼合業務的需要,Erda 項目也在不斷更新迭代。我們也會時刻保持對社區的關注以及對自身業務發展的分析,將這一塊做到更好,也","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"歡迎大家添加小助手微信(Erda202106)進入交流羣討論","attrs":{}},{"type":"text","text":"!​","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Erda Github 地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/erda-project/erda","title":"","type":null},"content":[{"type":"text","text":"https://github.com/erda-project/erda","attrs":{}}]}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Erda Cloud 官網:","attrs":{}},{"type":"link","attrs":{"href":"https://www.erda.cloud/","title":"","type":null},"content":[{"type":"text","text":"https://www.erda.cloud/","attrs":{}}]}]}]}],"attrs":{}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章